Compare commits

...
14 Commits
Author SHA1 Message Date
moloch-- 1f255c7010 Merge master into wasm/wasi-ext
Preserve WASM networking and decode-limit regression tests alongside master's deterministic encoder coverage. Align merged memory-filesystem directory assertions with this branch's basename entries.
2026-11-06 20:57:17 -07:00
moloch-- 9f742634f5 Merge remote-tracking branch 'origin/master' into wasm/wasi-ext 2026-10-26 08:14:06 -07:00
moloch-- 1aafbc7793 Merge master into wasm/wasi-ext 2026-10-23 09:18:05 -07:00
moloch-- f2952d7702 Keep generation tests enabled for large PR diffs 2026-10-22 10:38:52 -07:00
moloch-- 4dffd11945 Fix WASI filesystem and networking regressions 2026-10-22 10:19:01 -07:00
moloch-- 979d5b8809 Merge branch 'master' into wasm/wasi-ext 2026-09-15 16:23:10 -07:00
moloch-- 3f1103ef1b Fix large-PR lint and archive extraction 2026-08-18 14:30:26 -07:00
moloch-- ec4d561762 Merge remote-tracking branch 'origin/master' into feat/pr-2314-fix 2026-08-18 13:50:35 -07:00
Joe 6767493833 Merge branch 'master' into wasm/wasi-ext 2026-08-18 13:35:15 -07:00
moloch-- 3b7168ac23 Fix Windows memfs passthrough tests 2026-08-05 10:59:57 -07:00
moloch-- 83ea905ce3 Fix WASI extension CI 2026-08-05 10:34:38 -07:00
moloch-- 42e51c4843 Merge master 2026-08-05 10:01:49 -07:00
moloch-- ccb5dd5ebb Expand memfs support 2026-08-01 11:19:42 -07:00
moloch-- cf6eeaf142 Add wasm networking 2026-07-24 16:39:52 -07:00
282 changed files with 26630 additions and 10387 deletions

No files matched your search

+4 -2
View File
@@ -59,7 +59,8 @@ jobs:
set -euo pipefail
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
# Consume the full diff so pipefail cannot mistake SIGPIPE for no matching changes.
if git diff --name-only "$base_sha" "$head_sha" | grep -E '^(implant/|server/gogo/|server/generate/)' >/dev/null; then
echo "args=" >> "$GITHUB_OUTPUT"
else
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
@@ -141,7 +142,8 @@ jobs:
set -euo pipefail
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
# Consume the full diff so pipefail cannot mistake SIGPIPE for no matching changes.
if git diff --name-only "$base_sha" "$head_sha" | grep -E '^(implant/|server/gogo/|server/generate/)' >/dev/null; then
echo "args=" >> "$GITHUB_OUTPUT"
else
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
+3 -2
View File
@@ -35,6 +35,7 @@ ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ASSET_BUILD_SOURCES = $(filter-out %_test.go,$(wildcard util/assets/*.go util/cmd/assets/*.go util/cmd/sliver-wasm-go/*.go util/cmd/sliver-wasm-go/overlay/*.go.txt))
.PHONY: lint
lint:
@@ -199,7 +200,7 @@ clean-all: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets: util/assets/constants.go util/assets/garble.go
.downloaded_assets: $(ASSET_BUILD_SOURCES)
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
@@ -324,7 +325,7 @@ clean-all: clean
clean:
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
.downloaded_assets: util/assets/constants.go util/assets/garble.go
.downloaded_assets: $(ASSET_BUILD_SOURCES)
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
@type NUL > .downloaded_assets
@@ -14,7 +14,7 @@ Traffic encoders are implemented in Wasm, and must export the following function
decode(ptr, size uint32) (ptrSize uint64)
encode(ptr, size uint32) (ptrSize uint64)
malloc(size uint32) uint64
malloc(size uint32) uint32
free(ptr uint32, size uint32)
```
@@ -22,7 +22,7 @@ The `encode` function takes a `uint32` pointer to a buffer and the `uint32` size
The `decode` function takes a `uint32` pointer to a buffer and the `uint32` size of the buffer, and returns a `uint64` value where the upper 32 bits are a pointer to the address of the buffer and the lower 32 bits are the size of the buffer.
The `malloc` function takes a `uint64` size. The function should allocate a buffer of the given size in bytes, and return a pointer to.
The `malloc` function takes a `uint32` size. The function should allocate a buffer of the given size in bytes and return its `uint32` linear-memory pointer. For compatibility with older modules and documentation, Sliver also accepts an `i64` parameter or result when the value fits in 32-bit Wasm linear memory.
The `free` function takes a pointer to memory which should be freed and a size.
@@ -49,3 +49,17 @@ The `log` function takes a pointer to a buffer and the size of the buffer, and l
The `rand()` function returns 64-bits of cryptographically secure random data.
The `time()` function returns the current Unix Nano time as an `int64`.
### Go WASI networking
Sliver's server and implant traffic-encoder runtimes both provide the
versioned `sliver_wasi_net_v1` host. Go modules built with the standalone
`$SLIVER_ROOT_DIR/go/bin/sliver-wasm-go` wrapper can therefore use standard
library TCP, UDP, DNS, and HTTP APIs on both sides:
```console
$SLIVER_ROOT_DIR/go/bin/sliver-wasm-go build -buildmode=c-shared -o encoder.wasm .
```
The shared build mode creates a WASI reactor. Sliver detects and invokes its
`_initialize` export before calling `malloc`, `encode`, or `decode`.
+1 -1
View File
@@ -12,7 +12,7 @@ require (
github.com/miekg/dns v1.1.50
github.com/sliverarmory/reflektor v0.0.8
github.com/stretchr/testify v1.8.4
github.com/tetratelabs/wazero v1.7.1
github.com/tetratelabs/wazero v1.12.0
github.com/things-go/go-socks5 v0.0.4
github.com/yiya1989/sshkrb5 v0.0.0-20201110125252-a1455b75a35e
golang.org/x/crypto v0.52.0
+2 -2
View File
@@ -42,8 +42,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8=
github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0=
github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ=
github.com/yiya1989/sshkrb5 v0.0.0-20201110125252-a1455b75a35e h1:6SWomM8ezwUwnN3lqKxE4r7bYVwpY9514DQkwBV9IPI=
+9 -4
View File
@@ -2,10 +2,15 @@
## Overview
Programmable traffic encoders compiled into the implant for obfuscation. Contains compiler, interpreter, and test harness logic for traffic scripts. Runtime components handle compiler, interpreter, and traffic encoder for implant-side traffic features.
Programmable traffic encoders compiled into the implant for obfuscation.
## Go Files
- `compiler.go` – Compiles traffic encoder scripts into executable bytecode for implants.
- `interpreter.go` – Executes compiled traffic encoder bytecode at runtime.
- `traffic-encoder.go` – Provides high-level APIs to apply compiled traffic encoders to payloads.
- `create.go` - Selects wazero's native compiler when supported and its portable interpreter otherwise.
- `runtime.go` - Instantiates WASI, the legacy encoder host, and the shared `sliver_wasi_net_v1` networking host.
- `traffic-encoder.go` - Provides high-level APIs to apply compiled traffic encoders to payloads.
The implant runtime intentionally mirrors `util/encoders/traffic`: the same
Wasm artifact and the same `sliver_wasi_net_v1` imports run on both sides of
an HTTP C2 connection. See the server-side traffic runtime README for the Go
reactor build command and exported function ABI.
@@ -1,94 +0,0 @@
//go:build (linux && (386 || amd64)) || (darwin && (amd64 || arm64)) || (windows && amd64)
package traffic
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"sync"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
wasi "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
// CreateTrafficEncoder - Initialize an WASM runtime using the provided module name, code, and log callback
func CreateTrafficEncoder(name string, wasm []byte, logger TrafficEncoderLogCallback) (*TrafficEncoder, error) {
ctx := context.Background()
wasmRuntime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())
// Build the runtime and expose helper functions
_, err := wasmRuntime.NewHostModuleBuilder(name).
// Rand function
NewFunctionBuilder().WithFunc(func() uint64 {
buf := make([]byte, 8)
rand.Read(buf)
return binary.LittleEndian.Uint64(buf)
}).Export("rand").
// Time function
NewFunctionBuilder().WithFunc(func() int64 {
return time.Now().UnixNano()
}).Export("time").
// Log function
NewFunctionBuilder().WithFunc(func(_ context.Context, m api.Module, offset, byteCount uint32) {
buf, ok := m.Memory().Read(offset, byteCount)
if !ok {
logger(fmt.Sprintf("Log error: Memory.Read(%d, %d) out of range", offset, byteCount))
}
logger(string(buf))
}).Export("log").Instantiate(ctx)
if err != nil {
return nil, err
}
_, err = wasi.Instantiate(ctx, wasmRuntime)
if err != nil {
return nil, err
}
compiledMod, err := wasmRuntime.CompileModule(ctx, wasm)
if err != nil {
return nil, err
}
mod, err := wasmRuntime.InstantiateModule(ctx, compiledMod, wazero.NewModuleConfig())
if err != nil {
return nil, err
}
return &TrafficEncoder{
ctx: ctx,
runtime: wasmRuntime,
mod: mod,
lock: sync.Mutex{},
encoder: mod.ExportedFunction("encode"),
decoder: mod.ExportedFunction("decode"),
// These are undocumented, but exported. See tinygo-org/tinygo#2788
malloc: mod.ExportedFunction("malloc"),
free: mod.ExportedFunction("free"),
}, nil
}
+11
View File
@@ -0,0 +1,11 @@
// Package traffic provides WASM-based traffic encoding and decoding.
package traffic
import "github.com/tetratelabs/wazero"
// CreateTrafficEncoder initializes a WASM traffic-encoder runtime. Wazero
// selects its compiler when the host supports executable memory and otherwise
// falls back to the portable interpreter.
func CreateTrafficEncoder(name string, wasm []byte, logger TrafficEncoderLogCallback) (*TrafficEncoder, error) {
return createTrafficEncoder(name, wasm, logger, wazero.NewRuntimeConfig())
}
@@ -1,94 +0,0 @@
//go:build !((linux && (386 || amd64)) || (darwin && (amd64 || arm64)) || (windows && amd64))
package traffic
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"sync"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
wasi "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
// CreateTrafficEncoder - Initialize an WASM runtime using the provided module name, code, and log callback
func CreateTrafficEncoder(name string, wasm []byte, logger TrafficEncoderLogCallback) (*TrafficEncoder, error) {
ctx := context.Background()
wasmRuntime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())
// Build the runtime and expose helper functions
_, err := wasmRuntime.NewHostModuleBuilder(name).
// Rand function
NewFunctionBuilder().WithFunc(func() uint64 {
buf := make([]byte, 8)
rand.Read(buf)
return binary.LittleEndian.Uint64(buf)
}).Export("rand").
// Time function
NewFunctionBuilder().WithFunc(func() int64 {
return time.Now().UnixNano()
}).Export("time").
// Log function
NewFunctionBuilder().WithFunc(func(_ context.Context, m api.Module, offset, byteCount uint32) {
buf, ok := m.Memory().Read(offset, byteCount)
if !ok {
logger(fmt.Sprintf("Log error: Memory.Read(%d, %d) out of range", offset, byteCount))
}
logger(string(buf))
}).Export("log").Instantiate(ctx)
if err != nil {
return nil, err
}
_, err = wasi.Instantiate(ctx, wasmRuntime)
if err != nil {
return nil, err
}
compiledMod, err := wasmRuntime.CompileModule(ctx, wasm)
if err != nil {
return nil, err
}
mod, err := wasmRuntime.InstantiateModule(ctx, compiledMod, wazero.NewModuleConfig())
if err != nil {
return nil, err
}
return &TrafficEncoder{
ctx: ctx,
runtime: wasmRuntime,
mod: mod,
lock: sync.Mutex{},
encoder: mod.ExportedFunction("encode"),
decoder: mod.ExportedFunction("decode"),
// These are undocumented, but exported. See tinygo-org/tinygo#2788
malloc: mod.ExportedFunction("malloc"),
free: mod.ExportedFunction("free"),
}, nil
}
+142
View File
@@ -0,0 +1,142 @@
package traffic
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"runtime"
"time"
"github.com/bishopfox/sliver/implant/sliver/wasmnet"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
wasi "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
const trafficEncoderMemoryLimitPages = 4096 // 256 MiB
//nolint:gocyclo // Runtime setup is intentionally kept together so every partial initialization is cleaned up.
func createTrafficEncoder(
name string,
wasm []byte,
logger TrafficEncoderLogCallback,
runtimeConfig wazero.RuntimeConfig,
) (_ *TrafficEncoder, err error) {
if name == wasmnet.ModuleName {
return nil, fmt.Errorf("traffic encoder name %q is reserved", name)
}
ctx, stop := context.WithCancel(context.Background())
wasmRuntime := wazero.NewRuntimeWithConfig(
ctx,
runtimeConfig.
WithCloseOnContextDone(true).
WithMemoryLimitPages(trafficEncoderMemoryLimitPages),
)
network := wasmnet.New(ctx)
var mod api.Module
defer func() {
if err == nil {
return
}
stop()
_ = network.Close()
if mod != nil {
_ = mod.Close(context.Background())
}
_ = wasmRuntime.Close(context.Background())
}()
_, err = wasmRuntime.NewHostModuleBuilder(name).
NewFunctionBuilder().WithFunc(func() uint64 {
var buffer [8]byte
if _, readErr := rand.Read(buffer[:]); readErr != nil {
return 0
}
return binary.LittleEndian.Uint64(buffer[:])
}).Export("rand").
NewFunctionBuilder().WithFunc(func() int64 {
return time.Now().UnixNano()
}).Export("time").
NewFunctionBuilder().WithFunc(func(_ context.Context, module api.Module, offset, byteCount uint32) {
buffer, ok := module.Memory().Read(offset, byteCount)
if !ok {
logger(fmt.Sprintf("Log error: Memory.Read(%d, %d) out of range", offset, byteCount))
return
}
logger(string(buffer))
}).Export("log").
Instantiate(ctx)
if err != nil {
return nil, err
}
if _, err = network.Instantiate(ctx, wasmRuntime); err != nil {
return nil, err
}
if _, err = wasi.Instantiate(ctx, wasmRuntime); err != nil {
return nil, err
}
compiledModule, err := wasmRuntime.CompileModule(ctx, wasm)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
_ = compiledModule.Close(context.Background())
}
}()
if _, ok := compiledModule.ExportedMemories()["memory"]; !ok {
return nil, fmt.Errorf("traffic encoder must export memory")
}
moduleConfig := wazero.NewModuleConfig().
WithSysWalltime().
WithSysNanotime().
WithSysNanosleep().
WithOsyield(runtime.Gosched).
WithRandSource(rand.Reader)
if _, ok := compiledModule.ExportedFunctions()["_initialize"]; ok {
moduleConfig = moduleConfig.WithStartFunctions("_initialize")
}
mod, err = wasmRuntime.InstantiateModule(ctx, compiledModule, moduleConfig)
if err != nil {
return nil, err
}
encoder := mod.ExportedFunction("encode")
decoder := mod.ExportedFunction("decode")
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
for _, function := range []struct {
name string
function api.Function
params []api.ValueType
results []api.ValueType
}{
{"encode", encoder, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI64}},
{"decode", decoder, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI64}},
{"free", free, []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, nil},
} {
if err = validateTrafficEncoderFunction(function.name, function.function, function.params, function.results); err != nil {
return nil, err
}
}
if err = validateTrafficEncoderMalloc(malloc); err != nil {
return nil, err
}
return &TrafficEncoder{
ctx: ctx,
stop: stop,
runtime: wasmRuntime,
mod: mod,
network: network,
encoder: encoder,
decoder: decoder,
malloc: malloc,
free: free,
}, nil
}
@@ -21,9 +21,11 @@ package traffic
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"sync"
"github.com/bishopfox/sliver/implant/sliver/wasmnet"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
@@ -38,10 +40,15 @@ func CalculateWasmEncoderID(wasmEncoderData []byte) uint64 {
// TrafficEncoder - Implements the `Encoder` interface using a wasm backend
type TrafficEncoder struct {
ctx context.Context
stop context.CancelFunc
runtime wazero.Runtime
mod api.Module
network *wasmnet.Host
lock sync.Mutex
closeOnce sync.Once
closeErr error
// WASM functions
encoder api.Function
decoder api.Function
@@ -49,19 +56,80 @@ type TrafficEncoder struct {
free api.Function
}
func validateTrafficEncoderFunction(name string, function api.Function, params, results []api.ValueType) error {
if function == nil {
return fmt.Errorf("traffic encoder must export %s", name)
}
definition := function.Definition()
if !sameValueTypes(definition.ParamTypes(), params) || !sameValueTypes(definition.ResultTypes(), results) {
return fmt.Errorf("traffic encoder export %s has an incompatible function signature", name)
}
return nil
}
func validateTrafficEncoderMalloc(function api.Function) error {
if function == nil {
return fmt.Errorf("traffic encoder must export malloc")
}
definition := function.Definition()
params := definition.ParamTypes()
results := definition.ResultTypes()
if len(params) != 1 || len(results) != 1 ||
(params[0] != api.ValueTypeI32 && params[0] != api.ValueTypeI64) ||
(results[0] != api.ValueTypeI32 && results[0] != api.ValueTypeI64) {
return fmt.Errorf("traffic encoder export malloc has an incompatible function signature")
}
return nil
}
func sameValueTypes(left, right []api.ValueType) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
// Encode - Encode data using the wasm backend
func (t *TrafficEncoder) Encode(data []byte) ([]byte, error) {
return t.transform(t.encoder, data)
}
// Decode - Decode bytes using the wasm backend
func (t *TrafficEncoder) Decode(data []byte) ([]byte, error) {
return t.transform(t.decoder, data)
}
func (t *TrafficEncoder) transform(transformer api.Function, data []byte) (result []byte, err error) {
t.lock.Lock()
defer t.lock.Unlock()
// Allocate a buffer in the wasm runtime for the input data
size := uint64(len(data))
buf, err := t.malloc.Call(t.ctx, size)
if size > uint64(^uint32(0)) {
return nil, fmt.Errorf("traffic encoder input is too large: %d bytes", size)
}
allocationSize := size
if allocationSize == 0 {
allocationSize = 1
}
buf, err := t.malloc.Call(t.ctx, allocationSize)
if err != nil {
return nil, err
}
bufPtr := buf[0]
defer t.free.Call(t.ctx, bufPtr)
if bufPtr > uint64(^uint32(0)) {
return nil, fmt.Errorf("traffic encoder malloc returned an invalid pointer: %d", bufPtr)
}
defer func() {
if _, freeErr := t.free.Call(t.ctx, bufPtr, allocationSize); freeErr != nil {
err = errors.Join(err, fmt.Errorf("free traffic encoder input: %w", freeErr))
}
}()
// Copy input data into wasm memory
if !t.mod.Memory().Write(uint32(bufPtr), data) {
@@ -69,8 +137,7 @@ func (t *TrafficEncoder) Encode(data []byte) ([]byte, error) {
bufPtr, size, t.mod.Memory().Size())
}
// Call the encoder function
ptrSize, err := t.encoder.Call(t.ctx, bufPtr, size)
ptrSize, err := transformer.Call(t.ctx, bufPtr, size)
if err != nil {
return nil, err
}
@@ -78,51 +145,39 @@ func (t *TrafficEncoder) Encode(data []byte) ([]byte, error) {
// Read the output buffer from wasm memory
encodeResultPtr := uint32(ptrSize[0] >> 32)
encodeResultSize := uint32(ptrSize[0])
var encodeResult []byte
var ok bool
if encodeResult, ok = t.mod.Memory().Read(encodeResultPtr, encodeResultSize); !ok {
if encodeResultSize > 0 && uint64(encodeResultPtr) != bufPtr {
defer func() {
if _, freeErr := t.free.Call(t.ctx, uint64(encodeResultPtr), uint64(encodeResultSize)); freeErr != nil {
err = errors.Join(err, fmt.Errorf("free traffic encoder output: %w", freeErr))
}
}()
}
encodeResult, ok := t.mod.Memory().Read(encodeResultPtr, encodeResultSize)
if !ok {
return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d",
encodeResultPtr, encodeResultSize, t.mod.Memory().Size())
}
return encodeResult, nil
}
// Decode - Decode bytes using the wasm backend
func (t *TrafficEncoder) Decode(data []byte) ([]byte, error) {
t.lock.Lock()
defer t.lock.Unlock()
size := uint64(len(data))
buf, err := t.malloc.Call(t.ctx, size)
if err != nil {
return nil, err
}
bufPtr := buf[0]
defer t.free.Call(t.ctx, bufPtr)
if !t.mod.Memory().Write(uint32(bufPtr), data) {
return nil, fmt.Errorf("Memory.Write(%d, %d) out of range of memory size %d",
bufPtr, size, t.mod.Memory().Size())
}
// Call the decoder function
ptrSize, err := t.decoder.Call(t.ctx, bufPtr, size)
if err != nil {
return nil, err
}
decodeResultPtr := uint32(ptrSize[0] >> 32)
decodeResultSize := uint32(ptrSize[0])
var decodeResult []byte
var ok bool
if decodeResult, ok = t.mod.Memory().Read(decodeResultPtr, decodeResultSize); !ok {
return nil, fmt.Errorf("Memory.Read(%d, %d) out of range of memory size %d",
decodeResultPtr, decodeResultSize, t.mod.Memory().Size())
}
return decodeResult, nil
return append([]byte(nil), encodeResult...), nil
}
func (t *TrafficEncoder) Close() error {
return t.runtime.Close(t.ctx)
t.closeOnce.Do(func() {
if t.stop != nil {
t.stop()
}
t.lock.Lock()
defer t.lock.Unlock()
if t.network != nil {
t.closeErr = errors.Join(t.closeErr, t.network.Close())
}
if t.mod != nil {
t.closeErr = errors.Join(t.closeErr, t.mod.Close(context.Background()))
}
if t.runtime != nil {
t.closeErr = errors.Join(t.closeErr, t.runtime.Close(context.Background()))
}
})
return t.closeErr
}
// TrafficEncoderLogCallback - Callback function exposed to the wasm runtime to log messages
+49 -2
View File
@@ -11,7 +11,54 @@ Extension host runtime allowing implants to load optional capabilities. Manages
- `extension_linux.go` – Linux constructor for the shared Reflektor-backed Unix loader.
- `extension_unix.go` – CGO-free macOS/Linux loading, export invocation, and callback bridge.
- `extension_windows.go` – Windows-specific Reflektor integration for loading and invoking extensions.
- `memfs.go` – Implements the in-memory filesystem backing extension assets.
- `memfs_test.go` *(tests)* – Tests the in-memory filesystem behavior for extensions.
- `memfs.go` – Implements the writable in-memory filesystem backing extension assets and the unchanged host filesystem pass-through.
- `memfs_compat.go` – Retains the deprecated legacy MemFSNode API for source compatibility.
- `memfs*_test.go` *(tests)* – Exercise legacy compatibility, mutation, concurrency, resource limits, guest integration, and fuzz-seed behavior.
- `wasm.go` – Sets up the WASM runtime for implant extensions and handles module lifecycle.
- `memfs_wasi_integration_test.go` *(test)* – Runs a Go/WASI guest through create, read, write, append, rename, and removal operations.
- `wasm_network_integration_test.go` *(tests)* – Exercises the shared networking host with the wrapper-built HTTP example.
- `wasm_generic.go` – Provides WASM runtime glue for non-platform-specific builds.
## Go WASI networking
The Go standard library does not currently provide outbound networking on
`wasip1`. Sliver's bundled toolchain therefore installs
`$SLIVER_ROOT_DIR/go/bin/sliver-wasm-go`, a standalone wrapper that applies the
matching standard-library overlay and sets `GOOS=wasip1`, `GOARCH=wasm`, and
`CGO_ENABLED=0`.
Modules built by this wrapper import `sliver_wasi_net_v1` and must run in the
Sliver extension or traffic-encoder runtime. Both instantiate the shared host
implemented by `implant/sliver/wasmnet`, so TCP, UDP, DNS, deadlines, and
cancellation have the same behavior on the implant and server.
See `testdata/http-fetch` for an HTTPS extension example with an embedded
Mozilla public CA bundle. Traffic encoders use the same network imports but
must instead be built as WASI reactors with `-buildmode=c-shared` and export
the traffic-encoder ABI. The wrapper intentionally rejects `go run` because
the ordinary Go WASI runner does not provide Sliver's networking host.
## Go WASI memory filesystem
Extension modules see a persistent, concurrency-safe filesystem at `/memfs`.
The default is writable and supports regular and positioned I/O, sparse
writes, truncation, directories, metadata, rename and removal, hard links,
and relative symbolic links. State is shared by successive executions of the
same `WasmExtension`. Non-`/memfs` paths still use the historical read-only
host-root pass-through and are not made writable by this implementation.
`NewWasmExtension` remains signature-compatible; its `/memfs` default is now
writable. Hosts that require the old strict read-only behavior can use
`NewWasmExtensionWithOptions(..., WithReadOnlyMemFS())`. Code that needs the
filesystem independently can use `NewWasmMemoryFS`, optionally with
`WithWasmMemoryFSReadOnly()`. Read-only selection is currently a programmatic
host API; the existing console and RPC construction path keeps the writable
default. Initial maps and file contents are copied so guest mutations never
alias RPC input buffers. File modes are retained as metadata, but the virtual
filesystem does not model users or enforce Unix identity-based permissions.
The filesystem caps data at 256 MiB, directory entries at 65,536, and open
handles at 4,096 so sparse files and metadata cannot grow host memory without
bound.
See `testdata/memfs-rw` for a standalone Go/WASI module that exercises the
writable and read-only modes.
File diff suppressed because it is too large. Load diff
+222
View File
@@ -0,0 +1,222 @@
package extension
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"io/fs"
"path"
"strings"
"time"
)
var (
_ fs.File = MemFSNode{}
_ fs.FileInfo = MemFSNode{}
_ fs.DirEntry = MemFSNode{}
_ fs.ReadDirFile = MemFSNode{}
)
// MemFSNode is the node type exposed by the original read-only memory
// filesystem implementation. It is retained so code which used that API
// continues to compile; new code should use NewWasmMemoryFS and the standard
// io/fs or wazero experimental/sys interfaces instead.
//
// Deprecated: use NewWasmMemoryFS.
type MemFSNode struct {
fullName string
BaseName string
data *bytes.Buffer
isDir bool
parent *MemFSNode
Subdirs map[string]*MemFSNode
FileNodes map[string]*MemFSNode
}
// Exists reports whether segs identify an entry in this legacy tree.
//
// Deprecated: use fs.Stat with a WasmMemoryFS.
func (m *MemFSNode) Exists(segs []string) bool {
if m == nil {
return false
}
if len(segs) == 0 {
return true
}
if len(segs) == 1 {
if m.HasSubdir(segs[0]) {
return true
}
if _, ok := m.FileNodes[segs[0]]; ok {
return true
}
}
if m.HasSubdir(segs[0]) {
return m.Subdirs[segs[0]].Exists(segs[1:])
}
return false
}
// GetNode returns the legacy directory node identified by segs.
//
// Deprecated: use fs.Stat with a WasmMemoryFS.
func (m *MemFSNode) GetNode(segs []string) *MemFSNode {
if m == nil {
return nil
}
if len(segs) == 0 {
return m
}
if m.HasSubdir(segs[0]) {
return m.Subdirs[segs[0]].GetNode(segs[1:])
}
return nil
}
// HasSubdir reports whether name is an immediate child directory.
//
// Deprecated: use fs.Stat with a WasmMemoryFS.
func (m *MemFSNode) HasSubdir(name string) bool {
if m == nil {
return false
}
_, ok := m.Subdirs[name]
return ok
}
// Insert adds fileNode below the directory path in segs. Its map layout and
// generated names intentionally match the original MemFSNode implementation.
//
// Deprecated: create files through WasmMemoryFS.OpenFile.
func (m *MemFSNode) Insert(segs []string, fileNode *MemFSNode) {
if m == nil || fileNode == nil {
return
}
if len(segs) == 0 {
if m.FileNodes == nil {
m.FileNodes = map[string]*MemFSNode{}
}
m.FileNodes[fileNode.Name()] = fileNode
return
}
if m.Subdirs == nil {
m.Subdirs = map[string]*MemFSNode{}
}
if !m.HasSubdir(segs[0]) {
parentSegs := m.ParentSegs([]string{segs[0]})
m.Subdirs[segs[0]] = &MemFSNode{
fullName: "/" + strings.Join(parentSegs, "/"),
BaseName: segs[0],
parent: m,
isDir: true,
Subdirs: map[string]*MemFSNode{},
}
}
m.Subdirs[segs[0]].Insert(segs[1:], fileNode)
}
// ParentSegs prepends the path segments of this node's ancestors.
//
// Deprecated: use path manipulation with a WasmMemoryFS path.
func (m *MemFSNode) ParentSegs(segs []string) []string {
if m == nil || m.parent == nil {
return segs
}
return m.parent.ParentSegs(append([]string{m.BaseName}, segs...))
}
// Stat returns this legacy node as its file information.
func (m MemFSNode) Stat() (fs.FileInfo, error) {
return m, nil
}
// Info returns this legacy node as its file information.
func (m MemFSNode) Info() (fs.FileInfo, error) {
return m, nil
}
// Read reads from the legacy node's in-memory buffer.
func (m MemFSNode) Read(buffer []byte) (int, error) {
if m.data == nil {
return 0, fs.ErrInvalid
}
return m.data.Read(buffer)
}
// ReadDir returns this legacy directory's immediate children.
func (m MemFSNode) ReadDir(count int) ([]fs.DirEntry, error) {
if !m.isDir {
return nil, fs.ErrInvalid
}
entries := make([]fs.DirEntry, 0, len(m.Subdirs)+len(m.FileNodes))
for _, subdir := range m.Subdirs {
entries = append(entries, subdir)
}
for _, fileNode := range m.FileNodes {
entries = append(entries, fileNode)
}
if 0 <= count && count < len(entries) {
return entries[:count], nil
}
return entries, nil
}
// Close is a no-op retained for fs.File compatibility.
func (m MemFSNode) Close() error {
return nil
}
// Name returns the original memfs-qualified name of this node.
func (m MemFSNode) Name() string {
return path.Join("memfs", m.fullName)
}
// Size returns the unread bytes remaining in this legacy node's buffer.
func (m MemFSNode) Size() int64 {
if m.data == nil {
return 0
}
return int64(m.data.Len())
}
// Mode returns the mode exposed by the original MemFSNode API.
func (m MemFSNode) Mode() fs.FileMode {
return 0o444
}
// Type returns the type value exposed by the original MemFSNode API.
func (m MemFSNode) Type() fs.FileMode {
return 0o444
}
// ModTime returns the current time, matching the original MemFSNode API.
func (m MemFSNode) ModTime() time.Time {
return time.Now()
}
// IsDir reports whether this legacy node represents a directory.
func (m MemFSNode) IsDir() bool {
return m.isDir
}
// Sys returns no system-specific file information.
func (m MemFSNode) Sys() any {
return nil
}
@@ -0,0 +1,120 @@
package extension
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"errors"
"io"
"io/fs"
"testing"
)
func TestMemFSNodeCompatibilityTree(t *testing.T) {
root := &MemFSNode{
fullName: "",
isDir: true,
Subdirs: map[string]*MemFSNode{},
}
first := &MemFSNode{
fullName: "docs/first.txt",
BaseName: "first.txt",
data: bytes.NewBufferString("first"),
}
second := &MemFSNode{
fullName: "docs/second.txt",
BaseName: "second.txt",
data: bytes.NewBufferString("second"),
}
root.Insert([]string{"docs"}, first)
root.Insert([]string{"docs"}, second)
if !root.Exists(nil) || !root.Exists([]string{"docs"}) {
t.Fatal("legacy directory tree was not populated")
}
if root.Exists([]string{"missing"}) {
t.Fatal("missing legacy path unexpectedly exists")
}
docs := root.GetNode([]string{"docs"})
if docs == nil {
t.Fatal("GetNode did not find the inserted directory")
}
if got := docs.ParentSegs([]string{"child"}); len(got) != 2 || got[0] != "docs" || got[1] != "child" {
t.Fatalf("unexpected parent segments: %v", got)
}
if len(docs.FileNodes) != 2 {
t.Fatalf("expected two legacy file nodes, got %d", len(docs.FileNodes))
}
entries, err := docs.ReadDir(-1)
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 {
t.Fatalf("expected two directory entries, got %d", len(entries))
}
}
func TestMemFSNodeCompatibilityFileInterfaces(t *testing.T) {
node := MemFSNode{
fullName: "docs/file.txt",
BaseName: "file.txt",
data: bytes.NewBufferString("contents"),
}
info, err := node.Stat()
if err != nil {
t.Fatal(err)
}
if info.Name() != "memfs/docs/file.txt" {
t.Fatalf("unexpected legacy name: %q", info.Name())
}
if info.Size() != int64(len("contents")) || info.Mode() != 0o444 || info.IsDir() {
t.Fatalf("unexpected legacy file info: size=%d mode=%v dir=%v", info.Size(), info.Mode(), info.IsDir())
}
if entryInfo, err := node.Info(); err != nil || entryInfo.Name() != info.Name() {
t.Fatalf("unexpected directory entry info: info=%v err=%v", entryInfo, err)
}
contents, err := io.ReadAll(node)
if err != nil {
t.Fatal(err)
}
if string(contents) != "contents" {
t.Fatalf("unexpected legacy file contents: %q", contents)
}
if err := node.Close(); err != nil {
t.Fatal(err)
}
if node.Sys() != nil || node.Type() != fs.FileMode(0o444) {
t.Fatalf("unexpected legacy metadata: sys=%v type=%v", node.Sys(), node.Type())
}
}
func TestMemFSNodeCompatibilityNilData(t *testing.T) {
var node MemFSNode
if size := node.Size(); size != 0 {
t.Fatalf("unexpected nil-buffer size: %d", size)
}
if _, err := node.Read(make([]byte, 1)); !errors.Is(err, fs.ErrInvalid) {
t.Fatalf("expected invalid read error, got %v", err)
}
}
@@ -0,0 +1,630 @@
package extension
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"testing"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
func TestWasmMemoryFSEdgeRenameSamePath(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"seed.txt": []byte("seed")})
memFSTestRequireErrno(t, 0, memFS.Rename("seed.txt", "seed.txt"))
if got := string(memFSTestReadPath(t, memFS, "seed.txt")); got != "seed" {
t.Fatalf("same-path rename changed contents: %q", got)
}
if errno := memFS.Rename("missing.txt", "missing.txt"); errno != experimentalsys.ENOENT {
t.Fatalf("same-path rename of a missing file returned %v", errno)
}
readOnlyFS, err := NewWasmMemoryFS(
map[string][]byte{"seed.txt": []byte("seed")},
WithWasmMemoryFSReadOnly(),
)
if err != nil {
t.Fatalf("NewWasmMemoryFS(read-only): %v", err)
}
readOnly := &memFSTestFS{fs: readOnlyFS}
if errno := readOnly.Rename("seed.txt", "seed.txt"); errno != experimentalsys.EROFS {
t.Fatalf("same-path rename bypassed read-only mode: %v", errno)
}
}
func TestWasmMemoryFSEdgeCreateDirectoryFlagIsAtomic(t *testing.T) {
memFS := memFSTestNew(t, nil)
file, errno := memFS.OpenFile(
"invalid",
experimentalsys.O_CREAT|experimentalsys.O_DIRECTORY|experimentalsys.O_RDONLY,
0o700,
)
if file != nil {
_ = file.Close()
t.Fatal("O_CREAT|O_DIRECTORY returned a file handle")
}
if errno != experimentalsys.EINVAL {
t.Fatalf("O_CREAT|O_DIRECTORY returned %v", errno)
}
if _, errno := memFS.Lstat("invalid"); errno != experimentalsys.ENOENT {
t.Fatalf("invalid open left a filesystem entry behind: %v", errno)
}
}
func TestWasmMemoryFSEdgePositionedWriteOnAppendHandle(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"append.txt": []byte("a")})
file := memFSTestOpen(t, memFS, "append.txt", experimentalsys.O_WRONLY|experimentalsys.O_APPEND)
defer func() { _ = file.Close() }()
if n, errno := file.Pwrite([]byte("x"), 0); errno != experimentalsys.EINVAL || n != 0 {
t.Fatalf("Pwrite on an append handle: n=%d errno=%v", n, errno)
}
if got := string(memFSTestReadPath(t, memFS, "append.txt")); got != "a" {
t.Fatalf("rejected Pwrite changed contents: %q", got)
}
if n, errno := file.Write([]byte("b")); errno != 0 || n != 1 {
t.Fatalf("ordinary append after rejected Pwrite: n=%d errno=%v", n, errno)
}
if got := string(memFSTestReadPath(t, memFS, "append.txt")); got != "ab" {
t.Fatalf("ordinary append produced %q", got)
}
}
func TestWasmMemoryFSEdgeUtimensBothOmittedIsNoOp(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"time.txt": []byte("time")})
memFSTestRequireErrno(t, 0, memFS.Utimens("time.txt", 101, 202))
before, errno := memFS.Stat("time.txt")
memFSTestRequireErrno(t, 0, errno)
for range 8 {
memFSTestRequireErrno(t, 0, memFS.Utimens(
"time.txt",
experimentalsys.UTIME_OMIT,
experimentalsys.UTIME_OMIT,
))
}
afterPath, errno := memFS.Stat("time.txt")
memFSTestRequireErrno(t, 0, errno)
if afterPath.Atim != before.Atim || afterPath.Mtim != before.Mtim || afterPath.Ctim != before.Ctim {
t.Fatalf("path Utimens changed omitted timestamps: before=%+v after=%+v", before, afterPath)
}
file := memFSTestOpen(t, memFS, "time.txt", experimentalsys.O_RDONLY)
defer func() { _ = file.Close() }()
memFSTestRequireErrno(t, 0, file.Utimens(
experimentalsys.UTIME_OMIT,
experimentalsys.UTIME_OMIT,
))
afterHandle, errno := file.Stat()
memFSTestRequireErrno(t, 0, errno)
if afterHandle.Atim != before.Atim || afterHandle.Mtim != before.Mtim || afterHandle.Ctim != before.Ctim {
t.Fatalf("handle Utimens changed omitted timestamps: before=%+v after=%+v", before, afterHandle)
}
}
//nolint:gocyclo // Subtests intentionally cover each independent resource quota.
func TestWasmMemoryFSEdgeResourceQuotas(t *testing.T) {
t.Run("entries are bounded and reclaimed", func(t *testing.T) {
raw, err := NewWasmMemoryFS(nil, wasmMemoryFSEdgeLimits(32, 2, 8))
if err != nil {
t.Fatalf("NewWasmMemoryFS: %v", err)
}
memFS := &memFSTestFS{fs: raw}
memFSTestRequireErrno(t, 0, memFS.Mkdir("dir", 0o700))
file, errno := memFS.OpenFile("dir/file", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600)
memFSTestRequireErrno(t, 0, errno)
memFSTestRequireErrno(t, 0, file.Close())
if errno := memFS.Mkdir("overflow", 0o700); errno != experimentalsys.ERANGE {
t.Fatalf("entry quota returned %v", errno)
}
if _, errno := memFS.Stat("overflow"); errno != experimentalsys.ENOENT {
t.Fatalf("entry quota left a partial entry: %v", errno)
}
memFSTestRequireErrno(t, 0, memFS.Unlink("dir/file"))
memFSTestRequireErrno(t, 0, memFS.Mkdir("dir/reused", 0o700))
})
t.Run("implicit directories count during initialization", func(t *testing.T) {
if _, err := NewWasmMemoryFS(
map[string][]byte{"dir/file": []byte("x")},
wasmMemoryFSEdgeLimits(32, 1, 8),
); err == nil {
t.Fatal("constructor accepted a tree larger than the entry quota")
}
})
t.Run("open handles are bounded and reclaimed", func(t *testing.T) {
raw, err := NewWasmMemoryFS(
map[string][]byte{"seed.txt": []byte("seed")},
wasmMemoryFSEdgeLimits(32, 8, 1),
)
if err != nil {
t.Fatalf("NewWasmMemoryFS: %v", err)
}
legacy, err := raw.Open("memfs/seed.txt")
if err != nil {
t.Fatalf("legacy Open: %v", err)
}
if file, errno := raw.OpenFile("memfs/seed.txt", experimentalsys.O_RDONLY, 0); errno != experimentalsys.ERANGE {
if file != nil {
_ = file.Close()
}
t.Fatalf("open-handle quota returned %v", errno)
}
if err := legacy.Close(); err != nil {
t.Fatalf("legacy Close: %v", err)
}
file, errno := raw.OpenFile("memfs/seed.txt", experimentalsys.O_RDONLY, 0)
memFSTestRequireErrno(t, 0, errno)
memFSTestRequireErrno(t, 0, file.Close())
})
t.Run("unlinked open data remains charged", func(t *testing.T) {
raw, err := NewWasmMemoryFS(nil, wasmMemoryFSEdgeLimits(4, 8, 8))
if err != nil {
t.Fatalf("NewWasmMemoryFS: %v", err)
}
memFS := &memFSTestFS{fs: raw}
first, errno := memFS.OpenFile("first", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600)
memFSTestRequireErrno(t, 0, errno)
if n, errno := first.Write([]byte("1234")); errno != 0 || n != 4 {
t.Fatalf("fill byte quota: n=%d errno=%v", n, errno)
}
if n, errno := first.Pwrite([]byte("5"), 4); errno != experimentalsys.ERANGE || n != 0 {
t.Fatalf("byte quota extension: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, memFS.Unlink("first"))
second, errno := memFS.OpenFile("second", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600)
memFSTestRequireErrno(t, 0, errno)
if n, errno := second.Write([]byte("x")); errno != experimentalsys.ERANGE || n != 0 {
t.Fatalf("unlinked open data was not charged: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, first.Close())
if n, errno := second.Write([]byte("abcd")); errno != 0 || n != 4 {
t.Fatalf("released byte quota was not reusable: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, second.Close())
})
}
func TestWasmMemoryFSEdgeConcurrentChmodPreservesNodeTypes(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"file": []byte("data")})
memFSTestRequireErrno(t, 0, memFS.Mkdir("dir", 0o700))
file := memFSTestOpen(t, memFS, "file", experimentalsys.O_RDONLY)
directory := memFSTestOpen(t, memFS, "dir", experimentalsys.O_RDONLY|experimentalsys.O_DIRECTORY)
defer func() { _ = file.Close() }()
defer func() { _ = directory.Close() }()
const iterations = 1000
errCh := make(chan error, 4)
var wait sync.WaitGroup
wait.Add(4)
go func() {
defer wait.Done()
for index := 0; index < iterations; index++ {
perm := fs.FileMode(0o600 | index&0o77)
if errno := memFS.Chmod("file", perm); errno != 0 {
errCh <- fmt.Errorf("chmod file: %w", errno)
return
}
}
}()
go func() {
defer wait.Done()
for index := 0; index < iterations; index++ {
perm := fs.FileMode(0o700 | index&0o77)
if errno := memFS.Chmod("dir", perm); errno != 0 {
errCh <- fmt.Errorf("chmod directory: %w", errno)
return
}
}
}()
go func() {
defer wait.Done()
for range iterations {
isDirectory, errno := file.IsDir()
if errno != 0 || isDirectory {
errCh <- fmt.Errorf("regular file type changed: isDir=%t errno=%w", isDirectory, errno)
return
}
}
}()
go func() {
defer wait.Done()
for range iterations {
isDirectory, errno := directory.IsDir()
if errno != 0 || !isDirectory {
errCh <- fmt.Errorf("directory type changed: isDir=%t errno=%w", isDirectory, errno)
return
}
}
}()
wait.Wait()
close(errCh)
for err := range errCh {
t.Error(err)
}
}
func TestWasmMemoryFSEdgeCreateThroughDanglingSymlink(t *testing.T) {
memFS := memFSTestNew(t, nil)
memFSTestRequireErrno(t, 0, memFS.Mkdir("dir", 0o700))
memFSTestRequireErrno(t, 0, memFS.Symlink("target.txt", "dir/link.txt"))
file, errno := memFS.OpenFile("dir/link.txt", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o640)
memFSTestRequireErrno(t, 0, errno)
if n, errno := file.Write([]byte("created")); errno != 0 || n != len("created") {
t.Fatalf("write through dangling symlink: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, file.Close())
if got := string(memFSTestReadPath(t, memFS, "dir/target.txt")); got != "created" {
t.Fatalf("symlink target contents: %q", got)
}
if target, errno := memFS.Readlink("dir/link.txt"); errno != 0 || target != "target.txt" {
t.Fatalf("create replaced symlink: target=%q errno=%v", target, errno)
}
memFSTestRequireErrno(t, 0, memFS.Symlink("missing.txt", "dir/exclusive.txt"))
if file, errno := memFS.OpenFile(
"dir/exclusive.txt",
experimentalsys.O_CREAT|experimentalsys.O_EXCL|experimentalsys.O_NOFOLLOW|experimentalsys.O_RDWR,
0o600,
); errno != experimentalsys.EEXIST {
if file != nil {
_ = file.Close()
}
t.Fatalf("exclusive create of a symlink returned %v", errno)
}
if _, errno := memFS.Stat("dir/missing.txt"); errno != experimentalsys.ENOENT {
t.Fatalf("exclusive create changed dangling target: %v", errno)
}
}
func TestWasmMemoryFSEdgeSymlinkParentTraversal(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{
"file.txt": []byte("root-sentinel"),
"real/file.txt": []byte("target-data"),
})
memFSTestRequireErrno(t, 0, memFS.Mkdir("real/subdir", 0o700))
memFSTestRequireErrno(t, 0, memFS.Symlink("real/subdir", "alias"))
memFSTestRequireErrno(t, 0, memFS.Symlink("alias/../file.txt", "link"))
if got := string(memFSTestReadPath(t, memFS, "link")); got != "target-data" {
t.Fatalf("nested symlink resolved to the wrong file: %q", got)
}
file := memFSTestOpen(t, memFS, "link", experimentalsys.O_WRONLY|experimentalsys.O_TRUNC)
if n, errno := file.Write([]byte("updated")); errno != 0 || n != len("updated") {
_ = file.Close()
t.Fatalf("write through nested symlink: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, file.Close())
if got := string(memFSTestReadPath(t, memFS, "real/file.txt")); got != "updated" {
t.Fatalf("nested symlink did not update its target: %q", got)
}
if got := string(memFSTestReadPath(t, memFS, "file.txt")); got != "root-sentinel" {
t.Fatalf("nested symlink changed the root file: %q", got)
}
memFSTestRequireErrno(t, 0, memFS.Symlink("alias/../created.txt", "dangling"))
created := memFSTestOpen(t, memFS, "dangling", experimentalsys.O_CREAT|experimentalsys.O_RDWR)
memFSTestRequireErrno(t, 0, created.Close())
_, errno := memFS.Stat("real/created.txt")
memFSTestRequireErrno(t, 0, errno)
_, errno = memFS.Stat("created.txt")
memFSTestRequireErrno(t, experimentalsys.ENOENT, errno)
if target, errno := memFS.Readlink("dangling"); errno != 0 || target != "alias/../created.txt" {
t.Fatalf("create replaced the nested symlink: target=%q errno=%v", target, errno)
}
}
func TestWasmMemoryFSEdgeSymlinkParentTraversalRejectsInvalid(t *testing.T) {
for _, test := range []struct {
name string
target string
errno experimentalsys.Errno
}{
{name: "escape root", target: "root/../sentinel", errno: experimentalsys.EINVAL},
{name: "regular file parent", target: "file/../sentinel", errno: experimentalsys.ENOTDIR},
{name: "missing parent", target: "missing/../sentinel", errno: experimentalsys.ENOENT},
{name: "link loop", target: "loop/../sentinel", errno: experimentalsys.ELOOP},
} {
t.Run(test.name, func(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"sentinel": []byte("unchanged")})
memFSTestRequireErrno(t, 0, memFS.Symlink(".", "root"))
memFSTestRequireErrno(t, 0, memFS.Symlink("sentinel", "file"))
memFSTestRequireErrno(t, 0, memFS.Symlink("loop", "loop"))
memFSTestRequireErrno(t, 0, memFS.Symlink(test.target, "link"))
for _, flags := range []experimentalsys.Oflag{
experimentalsys.O_RDONLY,
experimentalsys.O_CREAT | experimentalsys.O_RDWR | experimentalsys.O_TRUNC,
} {
file, errno := memFS.OpenFile("link", flags, 0o600)
if file != nil {
_ = file.Close()
}
memFSTestRequireErrno(t, test.errno, errno)
}
if got := string(memFSTestReadPath(t, memFS, "sentinel")); got != "unchanged" {
t.Fatalf("invalid symlink changed the sentinel: %q", got)
}
})
}
}
func TestWasmMemoryFSEdgeSymlinkTargetBounds(t *testing.T) {
memFS := memFSTestNew(t, nil)
componentLimit := strings.Repeat("a", wasmMemoryFSMaxNameBytes)
memFSTestRequireErrno(t, 0, memFS.Symlink(componentLimit, "component-limit"))
if errno := memFS.Symlink(componentLimit+"a", "component-overflow"); errno != experimentalsys.ENAMETOOLONG {
t.Fatalf("overlong symlink component returned %v", errno)
}
pathLimit := wasmMemoryFSEdgeValidPath(wasmMemoryFSMaxPathBytes)
if len(pathLimit) != wasmMemoryFSMaxPathBytes {
t.Fatalf("path-limit helper produced %d bytes", len(pathLimit))
}
memFSTestRequireErrno(t, 0, memFS.Symlink(pathLimit, "path-limit"))
if target, errno := memFS.Readlink("path-limit"); errno != 0 || target != pathLimit {
t.Fatalf("path-limit symlink: target length=%d errno=%v", len(target), errno)
}
pathOverflow := wasmMemoryFSEdgeValidPath(wasmMemoryFSMaxPathBytes + 1)
if errno := memFS.Symlink(pathOverflow, "path-overflow"); errno != experimentalsys.ENAMETOOLONG {
t.Fatalf("overlong symlink target returned %v", errno)
}
for _, rejected := range []string{"component-overflow", "path-overflow"} {
if _, errno := memFS.Lstat(rejected); errno != experimentalsys.ENOENT {
t.Fatalf("rejected symlink %q left an entry: %v", rejected, errno)
}
}
}
//nolint:gocyclo // The test checks all passthrough escape and mutation boundaries together.
func TestWasmMemoryFSEdgeRawPassthroughIsReadOnlyAndIsolated(t *testing.T) {
raw, err := NewWasmMemoryFS(map[string][]byte{"seed.txt": []byte("memory")})
if err != nil {
t.Fatalf("NewWasmMemoryFS: %v", err)
}
hostPath := filepath.Join(memFSTestHostDir(t), "host.txt")
if err := os.WriteFile(hostPath, []byte("host-sentinel"), 0o600); err != nil {
t.Fatalf("write host fixture: %v", err)
}
hostGuestPath := memFSTestHostPath(t, hostPath)
hostFile, errno := raw.OpenFile(hostGuestPath, experimentalsys.O_RDONLY, 0)
memFSTestRequireErrno(t, 0, errno)
if got := string(memFSTestReadAll(t, hostFile)); got != "host-sentinel" {
t.Fatalf("raw passthrough contents: %q", got)
}
memFSTestRequireErrno(t, 0, hostFile.Close())
if stat, errno := raw.Stat(hostGuestPath); errno != 0 || stat.Size != int64(len("host-sentinel")) {
t.Fatalf("raw passthrough Stat: stat=%+v errno=%v", stat, errno)
}
if writable, errno := raw.OpenFile(hostGuestPath, experimentalsys.O_WRONLY|experimentalsys.O_TRUNC, 0o600); errno == 0 {
if n, writeErrno := writable.Write([]byte("changed")); writeErrno == 0 || n != 0 {
_ = writable.Close()
t.Fatalf("raw passthrough write: n=%d errno=%v", n, writeErrno)
}
_ = writable.Close()
}
createdPath := filepath.Join(filepath.Dir(hostPath), "created.txt")
createdGuestPath := memFSTestHostPath(t, createdPath)
if created, errno := raw.OpenFile(createdGuestPath, experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600); errno == 0 {
_, _ = created.Write([]byte("created"))
_ = created.Close()
}
if _, err := os.Stat(createdPath); !os.IsNotExist(err) {
t.Fatalf("raw passthrough created a host file: %v", err)
}
for _, traversal := range []string{
"memfs/../" + hostGuestPath,
"./memfs/../" + hostGuestPath,
"/memfs/../../" + hostGuestPath,
} {
file, errno := raw.OpenFile(traversal, experimentalsys.O_RDONLY, 0)
if file != nil {
_ = file.Close()
}
if errno != experimentalsys.EINVAL {
t.Fatalf("raw traversal %q returned %v", traversal, errno)
}
}
if contents, err := os.ReadFile(hostPath); err != nil || !bytes.Equal(contents, []byte("host-sentinel")) {
t.Fatalf("raw passthrough changed host fixture: contents=%q err=%v", contents, err)
}
if got := string(memFSTestReadPath(t, &memFSTestFS{fs: raw}, "seed.txt")); got != "memory" {
t.Fatalf("raw passthrough changed memfs: %q", got)
}
}
func TestWasmMemoryFSEdgeNamespaceParsingIsBounded(t *testing.T) {
const slashCount = 1 << 20
name := strings.Repeat("/", slashCount) + "memfs/file.txt"
subpath, memoryPath := splitWasmMemoryPath(name)
if !memoryPath || subpath != "file.txt" {
t.Fatalf("large namespace path: memory=%t subpath=%q", memoryPath, subpath)
}
name = strings.Repeat("./", slashCount/2) + "memfs/file.txt"
subpath, memoryPath = splitWasmMemoryPath(name)
if !memoryPath || subpath != "file.txt" {
t.Fatalf("large dotted namespace path: memory=%t subpath=%q", memoryPath, subpath)
}
}
func TestWasmMemoryFSEdgeConcurrentLazyPointerInitialization(t *testing.T) {
raw := &WasmMemoryFS{memFS: map[string][]byte{"seed.txt": []byte("seed")}}
const workers = 32
start := make(chan struct{})
errCh := make(chan error, workers)
var wait sync.WaitGroup
wait.Add(workers)
for range workers {
go func() {
defer wait.Done()
<-start
file, errno := raw.OpenFile("memfs/seed.txt", experimentalsys.O_RDONLY, 0)
if errno != 0 {
errCh <- fmt.Errorf("OpenFile: %w", errno)
return
}
contents := make([]byte, len("seed"))
n, errno := file.Read(contents)
if errno != 0 || n != len(contents) {
_ = file.Close()
errCh <- fmt.Errorf("Read: n=%d errno=%w", n, errno)
return
}
if errno := file.Close(); errno != 0 {
errCh <- fmt.Errorf("Close: %w", errno)
return
}
if !bytes.Equal(contents, []byte("seed")) {
errCh <- fmt.Errorf("contents: %q", contents)
}
}()
}
close(start)
wait.Wait()
close(errCh)
for err := range errCh {
t.Error(err)
}
}
//nolint:gocyclo // The concurrency test intentionally exercises both filesystem interfaces.
func TestWasmMemoryFSEdgeConcurrentMixedInterfaces(t *testing.T) {
testCases := map[string]func(*testing.T) *WasmMemoryFS{
"constructor": func(t *testing.T) *WasmMemoryFS {
raw, err := NewWasmMemoryFS(map[string][]byte{"seed.txt": []byte("seed")})
if err != nil {
t.Fatalf("NewWasmMemoryFS: %v", err)
}
return raw
},
"preinitialized legacy literal": func(t *testing.T) *WasmMemoryFS {
raw := &WasmMemoryFS{memFS: map[string][]byte{"seed.txt": []byte("seed")}}
if _, errno := raw.Stat("memfs/seed.txt"); errno != 0 {
t.Fatalf("initialize legacy literal: %v", errno)
}
return raw
},
}
for name, newFS := range testCases {
t.Run(name, func(t *testing.T) {
raw := newFS(t)
const workers = 32
errCh := make(chan error, workers)
var wait sync.WaitGroup
wait.Add(workers)
for worker := range workers {
go func() {
defer wait.Done()
if worker%2 == 0 {
file, err := raw.Open("memfs/seed.txt")
if err != nil {
errCh <- fmt.Errorf("legacy Open: %w", err)
return
}
contents, err := io.ReadAll(file)
closeErr := file.Close()
if err != nil {
errCh <- fmt.Errorf("legacy Read: %w", err)
return
}
if closeErr != nil {
errCh <- fmt.Errorf("legacy Close: %w", closeErr)
return
}
if !bytes.Equal(contents, []byte("seed")) {
errCh <- fmt.Errorf("legacy contents: %q", contents)
}
return
}
file, errno := raw.OpenFile("memfs/seed.txt", experimentalsys.O_RDONLY, 0)
if errno != 0 {
errCh <- fmt.Errorf("OpenFile: %w", errno)
return
}
contents := make([]byte, len("seed"))
n, errno := file.Read(contents)
if errno != 0 || n != len(contents) {
_ = file.Close()
errCh <- fmt.Errorf("Read: n=%d errno=%w", n, errno)
return
}
if errno := file.Close(); errno != 0 {
errCh <- fmt.Errorf("Close: %w", errno)
return
}
if !bytes.Equal(contents, []byte("seed")) {
errCh <- fmt.Errorf("contents: %q", contents)
}
}()
}
wait.Wait()
close(errCh)
for err := range errCh {
t.Error(err)
}
})
}
}
func wasmMemoryFSEdgeLimits(maxBytes, maxEntries, maxOpenFiles int64) WasmMemoryFSOption {
return func(config *wasmMemoryFSConfig) {
config.maxBytes = maxBytes
config.maxEntries = maxEntries
config.maxOpenFiles = maxOpenFiles
}
}
func wasmMemoryFSEdgeValidPath(length int) string {
if length <= 0 {
return ""
}
segments := make([]string, 0, length/(wasmMemoryFSMaxNameBytes+1)+1)
remaining := length
for remaining > wasmMemoryFSMaxNameBytes {
segmentLength := wasmMemoryFSMaxNameBytes
if remaining-segmentLength-1 == 0 {
segmentLength--
}
segments = append(segments, strings.Repeat("a", segmentLength))
remaining -= segmentLength + 1
}
segments = append(segments, strings.Repeat("b", remaining))
return strings.Join(segments, "/")
}
@@ -0,0 +1,801 @@
package extension
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"testing"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
wazerosys "github.com/tetratelabs/wazero/sys"
)
// memFSTestFS qualifies low-level wazero filesystem operations into the
// composite filesystem's memfs namespace. Open is intentionally not qualified:
// it also exercises the legacy io/fs passthrough API.
type memFSTestFS struct {
fs *WasmMemoryFS
}
func (m *memFSTestFS) Open(name string) (fs.File, error) {
return m.fs.Open(name)
}
func (m *memFSTestFS) OpenFile(name string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) {
return m.fs.OpenFile(memFSTestPath(name), flag, perm)
}
func (m *memFSTestFS) Lstat(name string) (wazerosys.Stat_t, experimentalsys.Errno) {
return m.fs.Lstat(memFSTestPath(name))
}
func (m *memFSTestFS) Stat(name string) (wazerosys.Stat_t, experimentalsys.Errno) {
return m.fs.Stat(memFSTestPath(name))
}
func (m *memFSTestFS) Mkdir(name string, perm fs.FileMode) experimentalsys.Errno {
return m.fs.Mkdir(memFSTestPath(name), perm)
}
func (m *memFSTestFS) Chmod(name string, perm fs.FileMode) experimentalsys.Errno {
return m.fs.Chmod(memFSTestPath(name), perm)
}
func (m *memFSTestFS) Rename(from, to string) experimentalsys.Errno {
return m.fs.Rename(memFSTestPath(from), memFSTestPath(to))
}
func (m *memFSTestFS) Rmdir(name string) experimentalsys.Errno {
return m.fs.Rmdir(memFSTestPath(name))
}
func (m *memFSTestFS) Unlink(name string) experimentalsys.Errno {
return m.fs.Unlink(memFSTestPath(name))
}
func (m *memFSTestFS) Link(oldName, newName string) experimentalsys.Errno {
return m.fs.Link(memFSTestPath(oldName), memFSTestPath(newName))
}
func (m *memFSTestFS) Symlink(oldName, linkName string) experimentalsys.Errno {
return m.fs.Symlink(oldName, memFSTestPath(linkName))
}
func (m *memFSTestFS) Readlink(name string) (string, experimentalsys.Errno) {
return m.fs.Readlink(memFSTestPath(name))
}
func (m *memFSTestFS) Utimens(name string, atim, mtim int64) experimentalsys.Errno {
return m.fs.Utimens(memFSTestPath(name), atim, mtim)
}
func memFSTestPath(name string) string {
name = strings.TrimPrefix(name, "/")
if name == "" || name == "." {
return "memfs"
}
if name == "memfs" || strings.HasPrefix(name, "memfs/") {
return name
}
return "memfs/" + name
}
func memFSTestNew(t *testing.T, files map[string][]byte) *memFSTestFS {
t.Helper()
memFS, err := NewWasmMemoryFS(files)
if err != nil {
t.Fatalf("NewWasmMemoryFS failed: %v", err)
}
return &memFSTestFS{fs: memFS}
}
func memFSTestOpen(t *testing.T, memFS *memFSTestFS, name string, flag experimentalsys.Oflag) experimentalsys.File {
t.Helper()
file, errno := memFS.OpenFile(name, flag, 0o600)
if errno != 0 {
t.Fatalf("OpenFile(%q, %#x) failed: %v", name, flag, errno)
}
return file
}
func memFSTestReadAll(t *testing.T, file experimentalsys.File) []byte {
t.Helper()
var result []byte
buf := make([]byte, 7)
for {
n, errno := file.Read(buf)
if errno != 0 {
t.Fatalf("Read failed: %v", errno)
}
result = append(result, buf[:n]...)
if n == 0 {
return result
}
}
}
func memFSTestReadPath(t *testing.T, memFS *memFSTestFS, name string) []byte {
t.Helper()
file := memFSTestOpen(t, memFS, name, experimentalsys.O_RDONLY)
defer func() { _ = file.Close() }()
return memFSTestReadAll(t, file)
}
func memFSTestRequireErrno(t *testing.T, want, got experimentalsys.Errno) {
t.Helper()
if got != want {
t.Fatalf("expected %v, got %v", want, got)
}
}
func TestWasmMemoryFSLocalFilesystemRootIsAbsolute(t *testing.T) {
root := wasmLocalFilesystemRoot()
if !filepath.IsAbs(root) {
t.Fatalf("local filesystem root %q is not absolute", root)
}
}
func TestWasmMemoryFSExtendedInputIsCopied(t *testing.T) {
contents := []byte("original")
files := map[string][]byte{"seed.txt": contents}
memFS := memFSTestNew(t, files)
copy(contents, "mutated!")
delete(files, "seed.txt")
files["added.txt"] = []byte("late")
if got := memFSTestReadPath(t, memFS, "seed.txt"); !bytes.Equal(got, []byte("original")) {
t.Fatalf("constructor retained caller-owned contents: %q", got)
}
if _, errno := memFS.Stat("added.txt"); errno != experimentalsys.ENOENT {
t.Fatalf("constructor retained caller-owned map: %v", errno)
}
file := memFSTestOpen(t, memFS, "seed.txt", experimentalsys.O_RDWR)
if n, errno := file.Pwrite([]byte("changed"), 0); errno != 0 || n != len("changed") {
t.Fatalf("Pwrite failed: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, file.Close())
if got := string(contents); got != "mutated!" {
t.Fatalf("filesystem write changed caller-owned bytes: %q", got)
}
}
func TestWasmMemoryFSExtendedOpenFlagsAndAccess(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{
"existing.txt": []byte("existing"),
})
if _, errno := memFS.OpenFile("missing.txt", experimentalsys.O_RDONLY, 0); errno != experimentalsys.ENOENT {
t.Fatalf("open missing file returned %v", errno)
}
readOnly := memFSTestOpen(t, memFS, "existing.txt", experimentalsys.O_RDONLY)
if n, errno := readOnly.Write([]byte("x")); errno != experimentalsys.EBADF || n != 0 {
t.Fatalf("write through read-only handle: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, readOnly.Close())
writeOnly := memFSTestOpen(t, memFS, "existing.txt", experimentalsys.O_WRONLY)
if n, errno := writeOnly.Read(make([]byte, 1)); errno != experimentalsys.EBADF || n != 0 {
t.Fatalf("read through write-only handle: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, writeOnly.Close())
created, errno := memFS.OpenFile(
"created.txt",
experimentalsys.O_CREAT|experimentalsys.O_EXCL|experimentalsys.O_RDWR,
0o640,
)
memFSTestRequireErrno(t, 0, errno)
if n, errno := created.Write([]byte("created")); errno != 0 || n != len("created") {
t.Fatalf("write created file: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, created.Close())
if _, errno := memFS.OpenFile(
"created.txt",
experimentalsys.O_CREAT|experimentalsys.O_EXCL|experimentalsys.O_RDWR,
0o600,
); errno != experimentalsys.EEXIST {
t.Fatalf("exclusive create returned %v", errno)
}
truncated := memFSTestOpen(t, memFS, "created.txt", experimentalsys.O_RDWR|experimentalsys.O_TRUNC)
st, errno := truncated.Stat()
memFSTestRequireErrno(t, 0, errno)
if st.Size != 0 {
t.Fatalf("O_TRUNC left size %d", st.Size)
}
memFSTestRequireErrno(t, 0, truncated.Close())
if _, errno := memFS.OpenFile("existing.txt", experimentalsys.O_RDONLY|experimentalsys.O_DIRECTORY, 0); errno != experimentalsys.ENOTDIR {
t.Fatalf("O_DIRECTORY on a file returned %v", errno)
}
if _, errno := memFS.OpenFile(".", experimentalsys.O_WRONLY, 0); errno != experimentalsys.EISDIR {
t.Fatalf("write-open root directory returned %v", errno)
}
if _, errno := memFS.OpenFile("existing.txt", experimentalsys.Oflag(3), 0); errno != experimentalsys.EINVAL {
t.Fatalf("invalid access mode returned %v", errno)
}
}
//nolint:gocyclo // The test validates the complete positioned-I/O state sequence.
func TestWasmMemoryFSExtendedPositionedIOSeekAndTruncate(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"position.txt": []byte("abcdef")})
file := memFSTestOpen(t, memFS, "position.txt", experimentalsys.O_RDWR)
defer func() { _ = file.Close() }()
if offset, errno := file.Seek(2, io.SeekStart); errno != 0 || offset != 2 {
t.Fatalf("SeekStart: offset=%d errno=%v", offset, errno)
}
buf := make([]byte, 2)
if n, errno := file.Pread(buf, 4); errno != 0 || n != 2 || string(buf) != "ef" {
t.Fatalf("Pread: n=%d errno=%v data=%q", n, errno, buf)
}
if offset, errno := file.Seek(0, io.SeekCurrent); errno != 0 || offset != 2 {
t.Fatalf("Pread changed offset: offset=%d errno=%v", offset, errno)
}
if n, errno := file.Pwrite([]byte("ZZ"), 0); errno != 0 || n != 2 {
t.Fatalf("Pwrite: n=%d errno=%v", n, errno)
}
if offset, errno := file.Seek(0, io.SeekCurrent); errno != 0 || offset != 2 {
t.Fatalf("Pwrite changed offset: offset=%d errno=%v", offset, errno)
}
if n, errno := file.Read(buf); errno != 0 || n != 2 || string(buf) != "cd" {
t.Fatalf("Read after positioned I/O: n=%d errno=%v data=%q", n, errno, buf)
}
if offset, errno := file.Seek(-1, io.SeekStart); errno != experimentalsys.EINVAL || offset != 0 {
t.Fatalf("negative SeekStart: offset=%d errno=%v", offset, errno)
}
if offset, errno := file.Seek(-1, io.SeekEnd); errno != 0 || offset != 5 {
t.Fatalf("SeekEnd: offset=%d errno=%v", offset, errno)
}
if n, errno := file.Write([]byte("!")); errno != 0 || n != 1 {
t.Fatalf("Write after SeekEnd: n=%d errno=%v", n, errno)
}
sparse, errno := memFS.OpenFile("sparse.bin", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600)
memFSTestRequireErrno(t, 0, errno)
if offset, errno := sparse.Seek(5, io.SeekStart); errno != 0 || offset != 5 {
t.Fatalf("sparse seek: offset=%d errno=%v", offset, errno)
}
if n, errno := sparse.Write([]byte{'x'}); errno != 0 || n != 1 {
t.Fatalf("sparse write: n=%d errno=%v", n, errno)
}
if got := memFSTestReadPath(t, memFS, "sparse.bin"); !bytes.Equal(got, []byte{0, 0, 0, 0, 0, 'x'}) {
t.Fatalf("sparse write did not zero-fill: %v", got)
}
memFSTestRequireErrno(t, 0, sparse.Truncate(8))
if got := memFSTestReadPath(t, memFS, "sparse.bin"); !bytes.Equal(got, []byte{0, 0, 0, 0, 0, 'x', 0, 0}) {
t.Fatalf("truncate growth did not zero-fill: %v", got)
}
memFSTestRequireErrno(t, 0, sparse.Truncate(3))
if got := memFSTestReadPath(t, memFS, "sparse.bin"); !bytes.Equal(got, []byte{0, 0, 0}) {
t.Fatalf("truncate shrink failed: %v", got)
}
if errno := sparse.Truncate(-1); errno != experimentalsys.EINVAL {
t.Fatalf("negative truncate returned %v", errno)
}
memFSTestRequireErrno(t, 0, sparse.Close())
memFSTestRequireErrno(t, 0, sparse.Close())
if _, errno := sparse.Read(make([]byte, 1)); errno != experimentalsys.EBADF {
t.Fatalf("read after close returned %v", errno)
}
}
func TestWasmMemoryFSExtendedAppend(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"append.txt": []byte("a")})
file := memFSTestOpen(t, memFS, "append.txt", experimentalsys.O_WRONLY|experimentalsys.O_APPEND)
if offset, errno := file.Seek(0, io.SeekStart); errno != 0 || offset != 0 {
t.Fatalf("seek append handle: offset=%d errno=%v", offset, errno)
}
if n, errno := file.Write([]byte("b")); errno != 0 || n != 1 {
t.Fatalf("append write: n=%d errno=%v", n, errno)
}
if got := string(memFSTestReadPath(t, memFS, "append.txt")); got != "ab" {
t.Fatalf("append ignored end of file: %q", got)
}
memFSTestRequireErrno(t, 0, file.SetAppend(false))
if file.IsAppend() {
t.Fatal("SetAppend(false) did not clear append mode")
}
if offset, errno := file.Seek(0, io.SeekStart); errno != 0 || offset != 0 {
t.Fatalf("seek non-append handle: offset=%d errno=%v", offset, errno)
}
if n, errno := file.Write([]byte("z")); errno != 0 || n != 1 {
t.Fatalf("non-append write: n=%d errno=%v", n, errno)
}
if got := string(memFSTestReadPath(t, memFS, "append.txt")); got != "zb" {
t.Fatalf("disabled append still appended: %q", got)
}
memFSTestRequireErrno(t, 0, file.Close())
}
//nolint:gocyclo // The test covers directory iteration and related metadata as one scenario.
func TestWasmMemoryFSExtendedDirectoriesAndMetadata(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{
"a/one.txt": []byte("one"),
"a/two.txt": []byte("two"),
"a/sub/three.txt": []byte("three"),
})
root, errno := memFS.Stat(".")
memFSTestRequireErrno(t, 0, errno)
if root.Mode&fs.ModeDir == 0 || root.Ino == 0 {
t.Fatalf("invalid root stat: %+v", root)
}
dirStat, errno := memFS.Stat("a")
memFSTestRequireErrno(t, 0, errno)
if dirStat.Mode&fs.ModeDir == 0 || dirStat.Ino == 0 {
t.Fatalf("invalid directory stat: %+v", dirStat)
}
memFSTestRequireErrno(t, 0, memFS.Mkdir("empty", 0o750))
if errno := memFS.Mkdir("empty", 0o750); errno != experimentalsys.EEXIST {
t.Fatalf("duplicate Mkdir returned %v", errno)
}
if errno := memFS.Mkdir("a/one.txt/child", 0o700); errno != experimentalsys.ENOTDIR {
t.Fatalf("Mkdir below file returned %v", errno)
}
dir := memFSTestOpen(t, memFS, "a", experimentalsys.O_RDONLY|experimentalsys.O_DIRECTORY)
defer func() { _ = dir.Close() }()
var names []string
for {
entries, errno := dir.Readdir(1)
memFSTestRequireErrno(t, 0, errno)
if len(entries) == 0 {
break
}
if len(entries) != 1 {
t.Fatalf("Readdir(1) returned %d entries", len(entries))
}
names = append(names, entries[0].Name)
}
sort.Strings(names)
if want := []string{"one.txt", "sub", "two.txt"}; !equalStrings(names, want) {
t.Fatalf("directory entries: got %v want %v", names, want)
}
if offset, errno := dir.Seek(0, io.SeekStart); errno != 0 || offset != 0 {
t.Fatalf("rewind directory: offset=%d errno=%v", offset, errno)
}
entries, errno := dir.Readdir(-1)
memFSTestRequireErrno(t, 0, errno)
if len(entries) != 3 {
t.Fatalf("Readdir(-1) after rewind returned %d entries", len(entries))
}
if _, errno := dir.Seek(1, io.SeekStart); errno != experimentalsys.EINVAL {
t.Fatalf("nonzero directory seek returned %v", errno)
}
file := memFSTestOpen(t, memFS, "a/one.txt", experimentalsys.O_RDWR)
before, errno := file.Stat()
memFSTestRequireErrno(t, 0, errno)
if got := memFSTestReadAll(t, file); string(got) != "one" {
t.Fatalf("read file: %q", got)
}
after, errno := file.Stat()
memFSTestRequireErrno(t, 0, errno)
if before.Size != after.Size || after.Size != 3 {
t.Fatalf("read changed reported size: before=%d after=%d", before.Size, after.Size)
}
memFSTestRequireErrno(t, 0, file.Sync())
memFSTestRequireErrno(t, 0, file.Datasync())
memFSTestRequireErrno(t, 0, memFS.Chmod("a/one.txt", 0o601))
st, errno := memFS.Stat("a/one.txt")
memFSTestRequireErrno(t, 0, errno)
if st.Mode.Perm() != 0o601 {
t.Fatalf("Chmod mode = %o", st.Mode.Perm())
}
memFSTestRequireErrno(t, 0, memFS.Utimens("a/one.txt", 111, 222))
st, errno = memFS.Stat("a/one.txt")
memFSTestRequireErrno(t, 0, errno)
if st.Atim != 111 || st.Mtim != 222 {
t.Fatalf("Utimens times: atim=%d mtim=%d", st.Atim, st.Mtim)
}
memFSTestRequireErrno(t, 0, memFS.Utimens("a/one.txt", experimentalsys.UTIME_OMIT, 333))
st, errno = memFS.Stat("a/one.txt")
memFSTestRequireErrno(t, 0, errno)
if st.Atim != 111 || st.Mtim != 333 {
t.Fatalf("UTIME_OMIT times: atim=%d mtim=%d", st.Atim, st.Mtim)
}
memFSTestRequireErrno(t, 0, file.Close())
}
func TestWasmMemoryFSExtendedRenameUnlinkAndRmdir(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{
"src/file.txt": []byte("source"),
"dst/old.txt": []byte("old"),
})
open := memFSTestOpen(t, memFS, "src/file.txt", experimentalsys.O_RDONLY)
memFSTestRequireErrno(t, 0, memFS.Rename("src/file.txt", "dst/moved.txt"))
if _, errno := memFS.Stat("src/file.txt"); errno != experimentalsys.ENOENT {
t.Fatalf("renamed source still exists: %v", errno)
}
if got := string(memFSTestReadPath(t, memFS, "dst/moved.txt")); got != "source" {
t.Fatalf("renamed contents: %q", got)
}
if got := string(memFSTestReadAll(t, open)); got != "source" {
t.Fatalf("open handle did not survive rename: %q", got)
}
memFSTestRequireErrno(t, 0, memFS.Rename("dst/moved.txt", "dst/old.txt"))
if got := string(memFSTestReadPath(t, memFS, "dst/old.txt")); got != "source" {
t.Fatalf("rename-overwrite contents: %q", got)
}
if errno := memFS.Rmdir("dst"); errno != experimentalsys.ENOTEMPTY {
t.Fatalf("Rmdir nonempty directory returned %v", errno)
}
if errno := memFS.Unlink("dst"); errno != experimentalsys.EISDIR {
t.Fatalf("Unlink directory returned %v", errno)
}
if errno := memFS.Rmdir("dst/old.txt"); errno != experimentalsys.ENOTDIR {
t.Fatalf("Rmdir file returned %v", errno)
}
unlinked := memFSTestOpen(t, memFS, "dst/old.txt", experimentalsys.O_RDONLY)
memFSTestRequireErrno(t, 0, memFS.Unlink("dst/old.txt"))
if _, errno := memFS.Stat("dst/old.txt"); errno != experimentalsys.ENOENT {
t.Fatalf("unlinked path still exists: %v", errno)
}
if got := string(memFSTestReadAll(t, unlinked)); got != "source" {
t.Fatalf("open handle did not survive unlink: %q", got)
}
memFSTestRequireErrno(t, 0, unlinked.Close())
memFSTestRequireErrno(t, 0, memFS.Rmdir("dst"))
memFSTestRequireErrno(t, 0, memFS.Mkdir("cycle", 0o700))
memFSTestRequireErrno(t, 0, memFS.Mkdir("cycle/child", 0o700))
if errno := memFS.Rename("cycle", "cycle/child/again"); errno != experimentalsys.EINVAL {
t.Fatalf("rename directory into descendant returned %v", errno)
}
if errno := memFS.Rmdir("."); errno != experimentalsys.EINVAL && errno != experimentalsys.EPERM {
t.Fatalf("remove root returned %v", errno)
}
memFSTestRequireErrno(t, 0, open.Close())
}
//nolint:gocyclo // Hard-link and symbolic-link compatibility are paired coverage.
func TestWasmMemoryFSExtendedLinks(t *testing.T) {
t.Run("hard link", func(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"target.txt": []byte("target")})
if errno := memFS.Link("target.txt", "hard.txt"); errno == experimentalsys.ENOSYS {
t.Skip("hard links are intentionally unsupported")
} else {
memFSTestRequireErrno(t, 0, errno)
}
target, errno := memFS.Stat("target.txt")
memFSTestRequireErrno(t, 0, errno)
hard, errno := memFS.Stat("hard.txt")
memFSTestRequireErrno(t, 0, errno)
if target.Ino == 0 || target.Ino != hard.Ino || target.Nlink < 2 || hard.Nlink < 2 {
t.Fatalf("hard-link metadata target=%+v hard=%+v", target, hard)
}
file := memFSTestOpen(t, memFS, "hard.txt", experimentalsys.O_RDWR)
if n, errno := file.Pwrite([]byte("X"), 0); errno != 0 || n != 1 {
t.Fatalf("write hard link: n=%d errno=%v", n, errno)
}
memFSTestRequireErrno(t, 0, file.Close())
if got := string(memFSTestReadPath(t, memFS, "target.txt")); got != "Xarget" {
t.Fatalf("hard link did not share contents: %q", got)
}
memFSTestRequireErrno(t, 0, memFS.Unlink("target.txt"))
if got := string(memFSTestReadPath(t, memFS, "hard.txt")); got != "Xarget" {
t.Fatalf("hard link did not survive unlink: %q", got)
}
})
t.Run("symbolic link", func(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"target.txt": []byte("target")})
if errno := memFS.Symlink("target.txt", "sym.txt"); errno == experimentalsys.ENOSYS {
t.Skip("symbolic links are intentionally unsupported")
} else {
memFSTestRequireErrno(t, 0, errno)
}
target, errno := memFS.Readlink("sym.txt")
memFSTestRequireErrno(t, 0, errno)
if target != "target.txt" {
t.Fatalf("Readlink = %q", target)
}
lst, errno := memFS.Lstat("sym.txt")
memFSTestRequireErrno(t, 0, errno)
if lst.Mode&fs.ModeSymlink == 0 {
t.Fatalf("Lstat mode = %v", lst.Mode)
}
st, errno := memFS.Stat("sym.txt")
memFSTestRequireErrno(t, 0, errno)
if st.Mode&fs.ModeSymlink != 0 || st.Size != int64(len("target")) {
t.Fatalf("Stat followed symlink incorrectly: %+v", st)
}
if got := string(memFSTestReadPath(t, memFS, "sym.txt")); got != "target" {
t.Fatalf("open symlink: %q", got)
}
memFSTestRequireErrno(t, 0, memFS.Unlink("sym.txt"))
if got := string(memFSTestReadPath(t, memFS, "target.txt")); got != "target" {
t.Fatalf("unlink symlink changed target: %q", got)
}
})
}
func TestWasmMemoryFSExtendedReadOnly(t *testing.T) {
rawMemFS, err := NewWasmMemoryFS(
map[string][]byte{"seed.txt": []byte("seed")},
WithWasmMemoryFSReadOnly(),
)
if err != nil {
t.Fatalf("NewWasmMemoryFS(read-only) failed: %v", err)
}
memFS := &memFSTestFS{fs: rawMemFS}
if got := string(memFSTestReadPath(t, memFS, "seed.txt")); got != "seed" {
t.Fatalf("read-only read: %q", got)
}
mutations := []struct {
name string
do func() experimentalsys.Errno
}{
{"open existing writable", func() experimentalsys.Errno {
_, errno := memFS.OpenFile("seed.txt", experimentalsys.O_RDWR, 0)
return errno
}},
{"create", func() experimentalsys.Errno {
_, errno := memFS.OpenFile("new.txt", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600)
return errno
}},
{"mkdir", func() experimentalsys.Errno { return memFS.Mkdir("dir", 0o700) }},
{"chmod", func() experimentalsys.Errno { return memFS.Chmod("seed.txt", 0o600) }},
{"rename", func() experimentalsys.Errno { return memFS.Rename("seed.txt", "renamed.txt") }},
{"rmdir", func() experimentalsys.Errno { return memFS.Rmdir(".") }},
{"unlink", func() experimentalsys.Errno { return memFS.Unlink("seed.txt") }},
{"link", func() experimentalsys.Errno { return memFS.Link("seed.txt", "hard.txt") }},
{"symlink", func() experimentalsys.Errno { return memFS.Symlink("seed.txt", "sym.txt") }},
{"utimens", func() experimentalsys.Errno { return memFS.Utimens("seed.txt", 1, 2) }},
}
for _, mutation := range mutations {
t.Run(mutation.name, func(t *testing.T) {
if errno := mutation.do(); errno != experimentalsys.EROFS {
t.Fatalf("read-only mutation returned %v", errno)
}
})
}
file := memFSTestOpen(t, memFS, "seed.txt", experimentalsys.O_RDONLY)
if _, errno := file.Write([]byte("x")); errno != experimentalsys.EBADF {
t.Fatalf("write through read-only handle returned %v", errno)
}
if _, errno := file.Pwrite([]byte("x"), 0); errno != experimentalsys.EBADF {
t.Fatalf("pwrite through read-only handle returned %v", errno)
}
if errno := file.Truncate(0); errno != experimentalsys.EBADF {
t.Fatalf("truncate through read-only handle returned %v", errno)
}
if errno := file.Utimens(1, 2); errno != experimentalsys.EBADF && errno != experimentalsys.EROFS {
t.Fatalf("utimens through read-only handle returned %v", errno)
}
memFSTestRequireErrno(t, 0, file.Close())
if got := string(memFSTestReadPath(t, memFS, "seed.txt")); got != "seed" {
t.Fatalf("read-only mutation changed contents: %q", got)
}
if _, errno := memFS.Stat("new.txt"); errno != experimentalsys.ENOENT {
t.Fatalf("read-only create changed namespace: %v", errno)
}
}
//nolint:gocyclo // The legacy API and traversal checks form one compatibility scenario.
func TestWasmMemoryFSExtendedLegacyOpenAndPassthroughSafety(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"nested/seed.txt": []byte("memory")})
for _, name := range []string{"memfs/nested/seed.txt", "/memfs/nested/seed.txt"} {
file, err := memFS.Open(name)
if err != nil {
t.Fatalf("legacy Open(%q): %v", name, err)
}
contents, err := io.ReadAll(file)
_ = file.Close()
if err != nil || string(contents) != "memory" {
t.Fatalf("legacy Open(%q): contents=%q err=%v", name, contents, err)
}
}
hostDir := memFSTestHostDir(t)
hostPath := filepath.Join(hostDir, "host.txt")
if err := os.WriteFile(hostPath, []byte("host-sentinel"), 0o600); err != nil {
t.Fatal(err)
}
guestPath := memFSTestHostPath(t, hostPath)
hostFile, err := memFS.Open(guestPath)
if err != nil {
t.Fatalf("passthrough Open(%q): %v", guestPath, err)
}
contents, err := io.ReadAll(hostFile)
if err != nil {
t.Fatalf("read passthrough: %v", err)
}
if string(contents) != "host-sentinel" {
t.Fatalf("passthrough contents: %q", contents)
}
if writer, ok := hostFile.(io.Writer); ok {
if _, err := writer.Write([]byte("changed")); err == nil {
t.Fatal("passthrough file unexpectedly allowed writes")
}
}
_ = hostFile.Close()
if contents, err := os.ReadFile(hostPath); err != nil || string(contents) != "host-sentinel" {
t.Fatalf("passthrough write changed host: contents=%q err=%v", contents, err)
}
for _, traversal := range []string{
"memfs/../" + guestPath,
"./memfs/../" + guestPath,
"/memfs/../../" + guestPath,
"memfs/nested/../../../" + guestPath,
} {
file, err := memFS.Open(traversal)
if err == nil {
_ = file.Close()
t.Fatalf("virtual traversal unexpectedly opened %q", traversal)
}
}
if _, errno := memFS.OpenFile("memfs/../escape.txt", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600); errno != experimentalsys.EINVAL && errno != experimentalsys.EPERM {
t.Fatalf("low-level traversal returned %v", errno)
}
if _, errno := memFS.fs.OpenFile("./memfs/../escape.txt", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600); errno != experimentalsys.EINVAL && errno != experimentalsys.EPERM {
t.Fatalf("dot-prefixed low-level traversal returned %v", errno)
}
if contents, err := os.ReadFile(hostPath); err != nil || string(contents) != "host-sentinel" {
t.Fatalf("traversal changed host: contents=%q err=%v", contents, err)
}
}
//nolint:gocyclo // The concurrency test validates append and positioned-write invariants together.
func TestWasmMemoryFSExtendedConcurrentAccess(t *testing.T) {
memFS := memFSTestNew(t, map[string][]byte{"append.log": nil, "blocks.bin": nil})
const (
writers = 16
writes = 32
)
record := []byte("record\n")
errCh := make(chan error, writers)
var wg sync.WaitGroup
for i := 0; i < writers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
file, errno := memFS.OpenFile("append.log", experimentalsys.O_WRONLY|experimentalsys.O_APPEND, 0)
if errno != 0 {
errCh <- errno
return
}
defer func() { _ = file.Close() }()
for j := 0; j < writes; j++ {
if n, errno := file.Write(record); errno != 0 || n != len(record) {
errCh <- fmt.Errorf("append n=%d errno=%v", n, errno)
return
}
}
}()
}
wg.Wait()
close(errCh)
for err := range errCh {
t.Fatal(err)
}
appended := memFSTestReadPath(t, memFS, "append.log")
if want := writers * writes; bytes.Count(appended, record) != want || len(appended) != want*len(record) {
t.Fatalf("concurrent append lost data: records=%d bytes=%d", bytes.Count(appended, record), len(appended))
}
const blockSize = 32
blocks := memFSTestOpen(t, memFS, "blocks.bin", experimentalsys.O_RDWR)
memFSTestRequireErrno(t, 0, blocks.Truncate(writers*blockSize))
memFSTestRequireErrno(t, 0, blocks.Close())
errCh = make(chan error, writers)
for i := 0; i < writers; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
file, errno := memFS.OpenFile("blocks.bin", experimentalsys.O_RDWR, 0)
if errno != 0 {
errCh <- errno
return
}
defer func() { _ = file.Close() }()
block := bytes.Repeat([]byte{byte(index + 1)}, blockSize)
if n, errno := file.Pwrite(block, int64(index*blockSize)); errno != 0 || n != len(block) {
errCh <- fmt.Errorf("pwrite n=%d errno=%v", n, errno)
}
}(i)
}
wg.Wait()
close(errCh)
for err := range errCh {
t.Fatal(err)
}
contents := memFSTestReadPath(t, memFS, "blocks.bin")
for i := 0; i < writers; i++ {
want := bytes.Repeat([]byte{byte(i + 1)}, blockSize)
got := contents[i*blockSize : (i+1)*blockSize]
if !bytes.Equal(got, want) {
t.Fatalf("block %d was corrupted: %v", i, got)
}
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func memFSTestHostDir(t *testing.T) string {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("get working directory: %v", err)
}
directory, err := os.MkdirTemp(cwd, ".sliver-memfs-test-*")
if err != nil {
t.Fatalf("create host fixture directory: %v", err)
}
t.Cleanup(func() {
if err := os.RemoveAll(directory); err != nil {
t.Errorf("remove host fixture directory: %v", err)
}
})
return directory
}
func memFSTestHostPath(t *testing.T, name string) string {
t.Helper()
relative, err := filepath.Rel(wasmLocalFilesystemRoot(), name)
if err != nil {
t.Fatalf("make host path %q relative to passthrough root: %v", name, err)
}
parentPrefix := ".." + string(filepath.Separator)
if relative == ".." || strings.HasPrefix(relative, parentPrefix) || filepath.IsAbs(relative) || filepath.VolumeName(relative) != "" {
t.Fatalf("host path %q escapes passthrough root", name)
}
return filepath.ToSlash(relative)
}
+228
View File
@@ -0,0 +1,228 @@
package extension
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"io"
"testing"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
func FuzzWasmMemoryFSPaths(f *testing.F) {
for _, seed := range []string{
"",
"file",
"nested/file",
".",
"..",
"../safe.txt",
"nested/../../safe.txt",
"/absolute",
"double//slash",
"trailing/",
"nul\x00byte",
"unicode-世界",
`windows\path`,
memFSFuzzStringOfLength('a', 255),
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, raw string) {
if len(raw) > 256 {
return
}
memFS := memFSTestNew(t, map[string][]byte{"safe.txt": []byte("sentinel")})
memFSTestRequireErrno(t, 0, memFS.Mkdir("scratch", 0o700))
candidate := "scratch/" + raw
if file, _ := memFS.OpenFile(candidate, experimentalsys.O_RDONLY, 0); file != nil {
_ = file.Close()
}
if file, _ := memFS.OpenFile(candidate, experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600); file != nil {
_, _ = file.Write([]byte("fuzz"))
_ = file.Close()
}
_, _ = memFS.Stat(candidate)
_, _ = memFS.Lstat(candidate)
_ = memFS.Chmod(candidate, 0o640)
_ = memFS.Utimens(candidate, 1, 2)
_ = memFS.Rename(candidate, "scratch/renamed")
_ = memFS.Unlink(candidate)
_ = memFS.Rmdir(candidate)
_ = memFS.Mkdir(candidate, 0o700)
_ = memFS.Link(candidate, "scratch/hard")
_ = memFS.Symlink(candidate, "scratch/sym")
_, _ = memFS.Readlink(candidate)
if file, err := memFS.Open("memfs/" + candidate); err == nil {
_, _ = io.ReadAll(file)
_ = file.Close()
}
if got := memFSTestReadPath(t, memFS, "safe.txt"); !bytes.Equal(got, []byte("sentinel")) {
t.Fatalf("path %q escaped its namespace and changed sentinel: %q", raw, got)
}
})
}
//nolint:gocyclo // The fuzz bytecode intentionally dispatches every filesystem operation.
func FuzzWasmMemoryFSOperations(f *testing.F) {
f.Add([]byte{})
f.Add([]byte{0, 3, 'a', 'b', 2, 4, 0, 0, 4, 8, 0, 0})
f.Add([]byte{1, 12, 'x', 'y', 5, 12, 4, 0, 3, 0xff, 0, 0})
f.Add(bytes.Repeat([]byte{0, 2, 'z', '!'}, 16))
f.Fuzz(func(t *testing.T, program []byte) {
if len(program) > 256 {
return
}
memFS := memFSTestNew(t, map[string][]byte{"file.bin": nil})
file := memFSTestOpen(t, memFS, "file.bin", experimentalsys.O_RDWR)
defer func() { _ = file.Close() }()
model := []byte(nil)
var offset int64
for pc := 0; pc+3 < len(program); pc += 4 {
opcode := program[pc] % 8
arg := program[pc+1]
payload := append([]byte(nil), program[pc+2:pc+4]...)
switch opcode {
case 0: // sequential write
payload = payload[:int(arg)%3]
n, errno := file.Write(payload)
memFSTestRequireErrno(t, 0, errno)
if n != len(payload) {
t.Fatalf("Write count=%d want=%d", n, len(payload))
}
model = memFSFuzzWriteAt(model, offset, payload)
offset += int64(len(payload))
case 1: // positioned write
writeOffset := int64(arg % 64)
n, errno := file.Pwrite(payload, writeOffset)
memFSTestRequireErrno(t, 0, errno)
if n != len(payload) {
t.Fatalf("Pwrite count=%d want=%d", n, len(payload))
}
model = memFSFuzzWriteAt(model, writeOffset, payload)
case 2: // absolute seek
want := int64(arg % 64)
got, errno := file.Seek(want, io.SeekStart)
memFSTestRequireErrno(t, 0, errno)
if got != want {
t.Fatalf("SeekStart=%d want=%d", got, want)
}
offset = want
case 3: // relative seek, including invalid negative results
delta := int64(int8(arg) % 16)
want := offset + delta
got, errno := file.Seek(delta, io.SeekCurrent)
if want < 0 {
memFSTestRequireErrno(t, experimentalsys.EINVAL, errno)
continue
}
memFSTestRequireErrno(t, 0, errno)
if got != want {
t.Fatalf("SeekCurrent=%d want=%d", got, want)
}
offset = want
case 4: // truncate
size := int(arg % 64)
memFSTestRequireErrno(t, 0, file.Truncate(int64(size)))
if size <= len(model) {
model = model[:size]
} else {
model = append(model, make([]byte, size-len(model))...)
}
case 5: // sequential read
readLen := int(arg % 8)
buf := make([]byte, readLen)
n, errno := file.Read(buf)
memFSTestRequireErrno(t, 0, errno)
want := memFSFuzzReadAt(model, offset, readLen)
if n != len(want) || !bytes.Equal(buf[:n], want) {
t.Fatalf("Read at %d: n=%d data=%v want=%v", offset, n, buf[:n], want)
}
offset += int64(n)
case 6: // positioned read
readOffset := int64(arg % 64)
buf := make([]byte, 2)
n, errno := file.Pread(buf, readOffset)
memFSTestRequireErrno(t, 0, errno)
want := memFSFuzzReadAt(model, readOffset, len(buf))
if n != len(want) || !bytes.Equal(buf[:n], want) {
t.Fatalf("Pread at %d: n=%d data=%v want=%v", readOffset, n, buf[:n], want)
}
case 7: // close and reopen resets the sequential offset
memFSTestRequireErrno(t, 0, file.Close())
file = memFSTestOpen(t, memFS, "file.bin", experimentalsys.O_RDWR)
offset = 0
}
st, errno := file.Stat()
memFSTestRequireErrno(t, 0, errno)
if st.Size != int64(len(model)) {
t.Fatalf("size=%d want=%d after opcode %d", st.Size, len(model), opcode)
}
}
if got := memFSTestReadPath(t, memFS, "file.bin"); !bytes.Equal(got, model) {
t.Fatalf("final contents=%v want=%v", got, model)
}
})
}
func memFSFuzzWriteAt(model []byte, offset int64, contents []byte) []byte {
if len(contents) == 0 {
return model
}
end := int(offset) + len(contents)
if end > len(model) {
model = append(model, make([]byte, end-len(model))...)
}
copy(model[int(offset):], contents)
return model
}
func memFSFuzzReadAt(model []byte, offset int64, length int) []byte {
if offset >= int64(len(model)) || length == 0 {
return nil
}
end := int(offset) + length
if end > len(model) {
end = len(model)
}
return model[int(offset):end]
}
func memFSFuzzStringOfLength(ch byte, count int) string {
return string(bytes.Repeat([]byte{ch}, count))
}
@@ -0,0 +1,156 @@
package extension
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"bytes"
"strings"
"testing"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
//nolint:gocyclo // The fuzz dispatcher verifies every mutating operation remains read-only.
func FuzzWasmMemoryFSReadOnlyInvariant(f *testing.F) {
f.Add([]byte{})
f.Add([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
f.Add(bytes.Repeat([]byte{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}, 8))
f.Add(bytes.Repeat([]byte{0xff}, 64))
f.Fuzz(func(t *testing.T, program []byte) {
if len(program) > 256 {
return
}
raw, err := NewWasmMemoryFS(
map[string][]byte{
"seed.txt": []byte("sentinel"),
"dir/nested.txt": []byte("nested"),
},
WithWasmMemoryFSReadOnly(),
)
if err != nil {
t.Fatalf("NewWasmMemoryFS(read-only): %v", err)
}
memFS := &memFSTestFS{fs: raw}
before, errno := memFS.Stat("seed.txt")
memFSTestRequireErrno(t, 0, errno)
for _, instruction := range program {
var mutationErrno experimentalsys.Errno
switch instruction % 10 {
case 0:
file, errno := memFS.OpenFile("seed.txt", experimentalsys.O_RDWR, 0)
if file != nil {
_ = file.Close()
}
mutationErrno = errno
case 1:
file, errno := memFS.OpenFile("new.txt", experimentalsys.O_CREAT|experimentalsys.O_RDWR, 0o600)
if file != nil {
_, _ = file.Write([]byte("changed"))
_ = file.Close()
}
mutationErrno = errno
case 2:
mutationErrno = memFS.Mkdir("new-dir", 0o700)
case 3:
mutationErrno = memFS.Chmod("seed.txt", 0o777)
case 4:
mutationErrno = memFS.Rename("seed.txt", "renamed.txt")
case 5:
mutationErrno = memFS.Rmdir("dir")
case 6:
mutationErrno = memFS.Unlink("seed.txt")
case 7:
mutationErrno = memFS.Link("seed.txt", "hard.txt")
case 8:
mutationErrno = memFS.Symlink("seed.txt", "sym.txt")
case 9:
mutationErrno = memFS.Utimens("seed.txt", int64(instruction), int64(instruction)+1)
}
if mutationErrno != experimentalsys.EROFS {
t.Fatalf("read-only instruction %d returned %v", instruction%10, mutationErrno)
}
}
after, errno := memFS.Stat("seed.txt")
memFSTestRequireErrno(t, 0, errno)
if after != before {
t.Fatalf("read-only operations changed metadata: before=%+v after=%+v", before, after)
}
if got := memFSTestReadPath(t, memFS, "seed.txt"); !bytes.Equal(got, []byte("sentinel")) {
t.Fatalf("read-only operations changed contents: %q", got)
}
for _, absent := range []string{"new.txt", "new-dir", "renamed.txt", "hard.txt", "sym.txt"} {
if _, errno := memFS.Lstat(absent); errno != experimentalsys.ENOENT {
t.Fatalf("read-only operation created %q: %v", absent, errno)
}
}
})
}
func FuzzWasmMemoryFSPathBoundaries(f *testing.F) {
for _, seed := range []string{
"",
".",
"..",
"../host",
"nested/../../host",
"nul\x00byte",
strings.Repeat("n", wasmMemoryFSMaxNameBytes),
strings.Repeat("n", wasmMemoryFSMaxNameBytes+1),
wasmMemoryFSEdgeValidPath(wasmMemoryFSMaxPathBytes - 1),
wasmMemoryFSEdgeValidPath(wasmMemoryFSMaxPathBytes),
wasmMemoryFSEdgeValidPath(wasmMemoryFSMaxPathBytes + 1),
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, rawPath string) {
if len(rawPath) > wasmMemoryFSMaxPathBytes*2 {
return
}
segments, errno := normalizeWasmMemoryPath(rawPath)
if errno == 0 {
if len(rawPath) > wasmMemoryFSMaxPathBytes {
t.Fatalf("accepted %d-byte path", len(rawPath))
}
for _, segment := range segments {
if segment == "" || segment == "." || segment == ".." {
t.Fatalf("accepted unsafe path segment %q in %q", segment, rawPath)
}
if len(segment) > wasmMemoryFSMaxNameBytes {
t.Fatalf("accepted %d-byte path segment", len(segment))
}
}
}
raw, err := NewWasmMemoryFS(map[string][]byte{"safe.txt": []byte("sentinel")})
if err != nil {
t.Fatalf("NewWasmMemoryFS: %v", err)
}
file, _ := raw.OpenFile("memfs/"+rawPath, experimentalsys.O_RDONLY, 0)
if file != nil {
_ = file.Close()
}
if got := memFSTestReadPath(t, &memFSTestFS{fs: raw}, "safe.txt"); !bytes.Equal(got, []byte("sentinel")) {
t.Fatalf("path %q escaped its namespace: %q", rawPath, got)
}
})
}
+1 -1
View File
@@ -64,7 +64,7 @@ func TestMemFSOpenRoot(t *testing.T) {
if err != nil {
t.Fatal(err)
}
wantEntries := map[string]bool{"memfs/test.1": false, "memfs/test.2": false, "memfs/a": true}
wantEntries := map[string]bool{"test.1": false, "test.2": false, "a": true}
if len(entries) != len(wantEntries) {
t.Fatalf("expected %d entries, got %d", len(wantEntries), len(entries))
}
@@ -0,0 +1,111 @@
//go:build (windows && (arm64 || amd64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (arm64 || amd64 || 386))
package extension
import (
"bytes"
"io"
"os"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWasmMemFSGuestIntegration(t *testing.T) {
wasmPath := buildWasmTestModule(
t,
"SLIVER_WASM_MEMFS_RW_WASM",
"./implant/sliver/extension/testdata/memfs-rw",
)
wasm, err := os.ReadFile(wasmPath)
mustNoError(t, err)
t.Run("read-write state persists across executions", func(t *testing.T) {
extension, err := NewWasmExtension(t.Name(), wasm, nil)
mustNoError(t, err)
stdout, stderr, exitCodes, executeErrors := executeWasmMemFSGuest(
t,
extension,
[]string{"write"},
[]string{"read"},
)
mustNoError(t, executeErrors[0], stderr)
mustNoError(t, executeErrors[1], stderr)
mustZero(t, exitCodes[0], stderr)
mustZero(t, exitCodes[1], stderr)
assert.Contains(t, stdout, "memfs-rw-write-ok")
assert.Contains(t, stdout, "memfs-rw-state-ok")
})
t.Run("nested symlinks resolve before parent traversal", func(t *testing.T) {
extension, err := NewWasmExtension(t.Name(), wasm, nil)
mustNoError(t, err)
stdout, stderr, exitCodes, executeErrors := executeWasmMemFSGuest(
t,
extension,
[]string{"symlinks"},
)
mustNoError(t, executeErrors[0], stderr)
mustZero(t, exitCodes[0], stderr)
assert.Contains(t, stdout, "memfs-symlinks-ok")
})
t.Run("read-only rejects guest mutations", func(t *testing.T) {
extension, err := NewWasmExtensionWithOptions(
t.Name(),
wasm,
map[string][]byte{"seed.txt": []byte("seed-data")},
WithReadOnlyMemFS(),
)
mustNoError(t, err)
stdout, stderr, exitCodes, executeErrors := executeWasmMemFSGuest(
t,
extension,
[]string{"readonly"},
)
mustNoError(t, executeErrors[0], stderr)
mustZero(t, exitCodes[0], stderr)
assert.Contains(t, stdout, "memfs-readonly-ok")
})
}
func executeWasmMemFSGuest(
t *testing.T,
extension *WasmExtension,
executions ...[]string,
) (stdout string, stderr string, exitCodes []uint32, executeErrors []error) {
t.Helper()
t.Cleanup(func() {
_ = extension.Close()
})
var stdoutBuffer bytes.Buffer
var stderrBuffer bytes.Buffer
var readers sync.WaitGroup
readers.Add(2)
go func() {
defer readers.Done()
_, _ = io.Copy(&stdoutBuffer, extension.Stdout.Reader)
}()
go func() {
defer readers.Done()
_, _ = io.Copy(&stderrBuffer, extension.Stderr.Reader)
}()
exitCodes = make([]uint32, 0, len(executions))
executeErrors = make([]error, 0, len(executions))
for _, args := range executions {
exitCode, err := extension.Execute(args)
exitCodes = append(exitCodes, exitCode)
executeErrors = append(executeErrors, err)
}
_ = extension.Stdout.Writer.Close()
_ = extension.Stderr.Writer.Close()
readers.Wait()
return stdoutBuffer.String(), stderrBuffer.String(), exitCodes, executeErrors
}
+20
View File
@@ -0,0 +1,20 @@
# Sliver WASI HTTP fetch example
This Go `wasip1` module fetches `https://example.com/` by default and accepts a
different URL as its first argument. Go's `wasip1` port has no operating-system
certificate store, so the module embeds curl's complete Mozilla public CA
extract using `//go:embed`.
`public-roots.pem` is the 119-certificate Mozilla snapshot dated 2026-07-16
from <https://curl.se/docs/caextract.html>. Its SHA-256 digest is
`3ff344e30b9b1ed2971044eabb438a08f2e2245ddb5f8ab1a3ad8b63ab4eaf91`.
Build it with the Sliver-specific compiler wrapper:
```console
$SLIVER_ROOT_DIR/go/bin/sliver-wasm-go build -o http-fetch.wasm .
```
The resulting module imports `sliver_wasi_net_v1`, so it must run in a Sliver
Wasm extension runtime (or another runtime implementing the same versioned
host ABI).
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"crypto/tls"
"crypto/x509"
_ "embed"
"fmt"
"io"
"net/http"
"os"
"time"
)
const (
defaultURL = "https://example.com/"
maxResponseLen = 1 << 20
)
// publicRootsPEM is a pinned Mozilla public CA extract, embedded because Go's
// wasip1 port has no operating-system certificate store.
//
//go:embed public-roots.pem
var publicRootsPEM []byte
func main() {
target := defaultURL
if len(os.Args) > 1 {
target = os.Args[1]
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(publicRootsPEM) {
fmt.Fprintln(os.Stderr, "could not parse embedded public TLS roots")
os.Exit(1)
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: roots,
}
defer transport.CloseIdleConnections()
client := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
response, err := client.Get(target)
if err != nil {
fmt.Fprintf(os.Stderr, "GET %s: %v\n", target, err)
os.Exit(1)
}
defer response.Body.Close()
body, err := io.ReadAll(io.LimitReader(response.Body, maxResponseLen))
if err != nil {
fmt.Fprintf(os.Stderr, "read %s: %v\n", target, err)
os.Exit(1)
}
fmt.Printf("GET %s: %s\n%s", target, response.Status, body)
if response.StatusCode < 200 || response.StatusCode >= 300 {
os.Exit(1)
}
}
File diff suppressed because it is too large. Load diff
+20
View File
@@ -0,0 +1,20 @@
# Writable memfs WASI fixture
This Go/WASI program is the integration fixture for the extension runtime's
`/memfs` implementation. It creates directories and files, performs sequential
and append I/O, renames entries, reopens persisted data, and removes files and
directories. Its `symlinks` mode verifies that nested symbolic links resolve
before parent-directory traversal during reads, truncation, and creation.
Its `readonly` mode verifies that seeded data remains readable
while create and truncate operations fail.
Build it outside the Sliver console with the bundled wrapper:
```console
$SLIVER_ROOT_DIR/go/bin/sliver-wasm-go build \
-o memfs-rw.wasm \
./implant/sliver/extension/testdata/memfs-rw
```
The resulting module uses standard Go `os` APIs and runs inside a Sliver Wasm
extension host.
+209
View File
@@ -0,0 +1,209 @@
package main
import (
"bytes"
"errors"
"fmt"
"io"
"os"
)
const (
memFSRoot = "/memfs/wasi-integration"
createdPath = memFSRoot + "/created.txt"
persistedPath = memFSRoot + "/persisted.txt"
removedPath = memFSRoot + "/removed.txt"
removedDir = memFSRoot + "/removed-dir"
seedPath = "/memfs/seed.txt"
blockedPath = "/memfs/blocked.txt"
persistedContents = "created-appended"
)
func main() {
if len(os.Args) != 2 {
fail("usage: memfs-rw <write|read|symlinks|readonly>")
}
var err error
switch os.Args[1] {
case "write":
err = writeAndMutate()
case "read":
err = readPersistedState()
case "symlinks":
err = verifyNestedSymlinks()
case "readonly":
err = verifyReadOnly()
default:
err = fmt.Errorf("unknown operation %q", os.Args[1])
}
if err != nil {
fail(err.Error())
}
}
func writeAndMutate() error {
if err := os.Mkdir(memFSRoot, 0o750); err != nil {
return fmt.Errorf("mkdir: %w", err)
}
file, err := os.OpenFile(createdPath, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o640)
if err != nil {
return fmt.Errorf("create: %w", err)
}
if _, err = file.WriteString("created"); err != nil {
_ = file.Close()
return fmt.Errorf("write: %w", err)
}
if _, err = file.Seek(0, io.SeekStart); err != nil {
_ = file.Close()
return fmt.Errorf("seek: %w", err)
}
contents := make([]byte, len("created"))
if _, err = io.ReadFull(file, contents); err != nil {
_ = file.Close()
return fmt.Errorf("read after seek: %w", err)
}
if !bytes.Equal(contents, []byte("created")) {
_ = file.Close()
return fmt.Errorf("read after seek = %q", contents)
}
if err = file.Close(); err != nil {
return fmt.Errorf("close created file: %w", err)
}
file, err = os.OpenFile(createdPath, os.O_WRONLY|os.O_APPEND, 0)
if err != nil {
return fmt.Errorf("open append: %w", err)
}
if _, err = file.WriteString("-appended"); err != nil {
_ = file.Close()
return fmt.Errorf("append: %w", err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("close appended file: %w", err)
}
if err = os.Rename(createdPath, persistedPath); err != nil {
return fmt.Errorf("rename: %w", err)
}
if err = expectFileContents(persistedPath, persistedContents); err != nil {
return err
}
if err = os.WriteFile(removedPath, []byte("remove-me"), 0o600); err != nil {
return fmt.Errorf("create removable file: %w", err)
}
if err = os.Remove(removedPath); err != nil {
return fmt.Errorf("remove file: %w", err)
}
if _, err = os.Stat(removedPath); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("stat removed file: got %v, want not-exist", err)
}
if err = os.Mkdir(removedDir, 0o700); err != nil {
return fmt.Errorf("create removable directory: %w", err)
}
if err = os.Remove(removedDir); err != nil {
return fmt.Errorf("remove directory: %w", err)
}
if _, err = os.Stat(removedDir); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("stat removed directory: got %v, want not-exist", err)
}
fmt.Println("memfs-rw-write-ok")
return nil
}
func readPersistedState() error {
if err := expectFileContents(persistedPath, persistedContents); err != nil {
return fmt.Errorf("read state from prior execution: %w", err)
}
fmt.Println("memfs-rw-state-ok")
return nil
}
func verifyNestedSymlinks() error {
const root = "/memfs/symlinks"
if err := os.MkdirAll(root+"/real/subdir", 0o700); err != nil {
return fmt.Errorf("create symlink directories: %w", err)
}
for name, contents := range map[string]string{
root + "/file.txt": "root-sentinel",
root + "/real/file.txt": "target-data",
} {
if err := os.WriteFile(name, []byte(contents), 0o600); err != nil {
return fmt.Errorf("seed symlink file: %w", err)
}
}
for name, target := range map[string]string{
root + "/alias": "real/subdir",
root + "/link": "alias/../file.txt",
root + "/dangling": "alias/../created.txt",
} {
if err := os.Symlink(target, name); err != nil {
return fmt.Errorf("create symlink: %w", err)
}
}
if err := expectFileContents(root+"/link", "target-data"); err != nil {
return err
}
if err := os.WriteFile(root+"/link", []byte("updated"), 0o600); err != nil {
return fmt.Errorf("truncate through nested symlink: %w", err)
}
if err := expectFileContents(root+"/real/file.txt", "updated"); err != nil {
return err
}
if err := expectFileContents(root+"/file.txt", "root-sentinel"); err != nil {
return err
}
if err := os.WriteFile(root+"/dangling", []byte("created"), 0o600); err != nil {
return fmt.Errorf("create through nested symlink: %w", err)
}
if err := expectFileContents(root+"/real/created.txt", "created"); err != nil {
return err
}
if _, err := os.Stat(root + "/created.txt"); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("nested symlink created a root file: %v", err)
}
fmt.Println("memfs-symlinks-ok")
return nil
}
func verifyReadOnly() error {
if err := expectFileContents(seedPath, "seed-data"); err != nil {
return fmt.Errorf("read seed: %w", err)
}
if err := os.WriteFile(blockedPath, []byte("blocked"), 0o600); err == nil {
return errors.New("creating a file on read-only memfs unexpectedly succeeded")
}
if _, err := os.Stat(blockedPath); !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("stat blocked file: got %v, want not-exist", err)
}
if file, err := os.OpenFile(seedPath, os.O_WRONLY|os.O_TRUNC, 0); err == nil {
_ = file.Close()
return errors.New("truncating a file on read-only memfs unexpectedly succeeded")
}
if err := expectFileContents(seedPath, "seed-data"); err != nil {
return fmt.Errorf("seed changed after rejected write: %w", err)
}
fmt.Println("memfs-readonly-ok")
return nil
}
func expectFileContents(name, expected string) error {
contents, err := os.ReadFile(name)
if err != nil {
return fmt.Errorf("read %s: %w", name, err)
}
if string(contents) != expected {
return fmt.Errorf("read %s = %q, want %q", name, contents, expected)
}
return nil
}
func fail(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(1)
}
+206
View File
@@ -0,0 +1,206 @@
package main
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"os"
"time"
)
const networkTimeout = 10 * time.Second
func main() {
if err := tcpRoundTrip(); err != nil {
fmt.Fprintf(os.Stderr, "TCP round trip: %v\n", err)
os.Exit(1)
}
if err := udpRoundTrip(); err != nil {
fmt.Fprintf(os.Stderr, "UDP round trip: %v\n", err)
os.Exit(1)
}
if err := udpReadRoundTrip(); err != nil {
fmt.Fprintf(os.Stderr, "UDP listener read: %v\n", err)
os.Exit(1)
}
if err := closeRace(); err != nil {
fmt.Fprintf(os.Stderr, "close race: %v\n", err)
os.Exit(1)
}
if addresses, err := net.LookupHost("localhost"); err != nil || len(addresses) == 0 {
fmt.Fprintf(os.Stderr, "lookup localhost: addresses=%v error=%v\n", addresses, err)
os.Exit(1)
}
if _, err := net.LookupHost("invalid host name"); err == nil {
fmt.Fprintln(os.Stderr, "invalid hostname lookup unexpectedly succeeded")
os.Exit(1)
} else {
var dnsError *net.DNSError
if !errors.As(err, &dnsError) || !dnsError.IsNotFound {
fmt.Fprintf(os.Stderr, "invalid hostname lookup error = %#v\n", err)
os.Exit(1)
}
}
fmt.Println("network-smoke-ok")
}
func tcpRoundTrip() error {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return err
}
defer listener.Close()
serverResult := make(chan error, 1)
go func() {
connection, acceptErr := listener.Accept()
if acceptErr != nil {
serverResult <- acceptErr
return
}
defer connection.Close()
if deadlineErr := connection.SetDeadline(time.Now().Add(networkTimeout)); deadlineErr != nil {
serverResult <- deadlineErr
return
}
request := make([]byte, 4)
if _, readErr := io.ReadFull(connection, request); readErr != nil {
serverResult <- readErr
return
}
if !bytes.Equal(request, []byte("ping")) {
serverResult <- fmt.Errorf("request = %q", request)
return
}
_, writeErr := connection.Write([]byte("pong"))
serverResult <- writeErr
}()
connection, err := net.DialTimeout("tcp4", listener.Addr().String(), networkTimeout)
if err != nil {
return err
}
defer connection.Close()
if err := connection.SetDeadline(time.Now().Add(networkTimeout)); err != nil {
return err
}
if _, err := connection.Write([]byte("ping")); err != nil {
return err
}
response := make([]byte, 4)
if _, err := io.ReadFull(connection, response); err != nil {
return err
}
if !bytes.Equal(response, []byte("pong")) {
return fmt.Errorf("response = %q", response)
}
return <-serverResult
}
func closeRace() error {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return err
}
acceptStarted := make(chan struct{})
acceptResult := make(chan error, 1)
go func() {
close(acceptStarted)
_, acceptErr := listener.Accept()
acceptResult <- acceptErr
}()
<-acceptStarted
time.Sleep(10 * time.Millisecond)
if err := listener.Close(); err != nil {
return err
}
select {
case err := <-acceptResult:
if !errors.Is(err, net.ErrClosed) {
return fmt.Errorf("Accept error = %v, want net.ErrClosed", err)
}
return nil
case <-time.After(networkTimeout):
return errors.New("Accept remained blocked after Close")
}
}
func udpRoundTrip() error {
server, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {
return err
}
defer server.Close()
client, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {
return err
}
defer client.Close()
deadline := time.Now().Add(networkTimeout)
if err := server.SetDeadline(deadline); err != nil {
return err
}
if err := client.SetDeadline(deadline); err != nil {
return err
}
if _, err := client.WriteTo([]byte("ping"), server.LocalAddr()); err != nil {
return err
}
request := make([]byte, 4)
n, clientAddress, err := server.ReadFrom(request)
if err != nil {
return err
}
if !bytes.Equal(request[:n], []byte("ping")) {
return fmt.Errorf("request = %q", request[:n])
}
if _, err := server.WriteTo([]byte("pong"), clientAddress); err != nil {
return err
}
response := make([]byte, 4)
n, _, err = client.ReadFrom(response)
if err != nil {
return err
}
if !bytes.Equal(response[:n], []byte("pong")) {
return fmt.Errorf("response = %q", response[:n])
}
return nil
}
func udpReadRoundTrip() error {
listener, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
return err
}
defer listener.Close()
client, err := net.DialUDP("udp4", nil, listener.LocalAddr().(*net.UDPAddr))
if err != nil {
return err
}
defer client.Close()
deadline := time.Now().Add(networkTimeout)
if err := listener.SetReadDeadline(deadline); err != nil {
return err
}
if err := client.SetWriteDeadline(deadline); err != nil {
return err
}
for _, payload := range [][]byte{[]byte("ping"), {}} {
if _, err := client.Write(payload); err != nil {
return err
}
request := make([]byte, 32)
n, err := listener.Read(request)
if err != nil {
return err
}
if !bytes.Equal(request[:n], payload) {
return fmt.Errorf("request = %q, want %q", request[:n], payload)
}
}
return nil
}
+110
View File
@@ -0,0 +1,110 @@
package main
import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"os"
"slices"
"sync/atomic"
"time"
"golang.org/x/net/dns/dnsmessage"
)
const hostname = "sliver-resolver-dial.invalid."
func main() {
for _, preferGo := range []bool{false, true} {
if err := checkResolver(preferGo); err != nil {
fmt.Fprintf(os.Stderr, "PreferGo=%t: %v\n", preferGo, err)
os.Exit(1)
}
}
fmt.Println("resolver-dial-ok")
}
func checkResolver(preferGo bool) error {
var calls atomic.Int32
resolver := &net.Resolver{
PreferGo: preferGo,
Dial: func(ctx context.Context, _, _ string) (net.Conn, error) {
calls.Add(1)
client, server := net.Pipe()
deadline, _ := ctx.Deadline()
_ = server.SetDeadline(deadline)
go func() {
defer server.Close()
_ = respond(server)
}()
return client, nil
},
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
addresses, err := resolver.LookupHost(ctx, hostname)
if err != nil {
return fmt.Errorf("LookupHost: %w", err)
}
if !slices.Contains(addresses, "192.0.2.10") || !slices.Contains(addresses, "2001:db8::10") {
return fmt.Errorf("LookupHost addresses = %v", addresses)
}
for _, query := range []struct{ network, want string }{
{"ip4", "192.0.2.10"},
{"ip6", "2001:db8::10"},
} {
addresses, err := resolver.LookupIP(ctx, query.network, hostname)
if err != nil {
return fmt.Errorf("LookupIP %s: %w", query.network, err)
}
if len(addresses) != 1 || addresses[0].String() != query.want {
return fmt.Errorf("LookupIP %s addresses = %v, want %s", query.network, addresses, query.want)
}
}
if calls.Load() < 4 {
return fmt.Errorf("custom Dial called %d times, want at least 4", calls.Load())
}
return nil
}
// net.Pipe is a stream connection, so the resolver uses DNS-over-TCP framing.
func respond(conn net.Conn) error {
var size [2]byte
if _, err := io.ReadFull(conn, size[:]); err != nil {
return err
}
query := make([]byte, binary.BigEndian.Uint16(size[:]))
if _, err := io.ReadFull(conn, query); err != nil {
return err
}
var message dnsmessage.Message
if err := message.Unpack(query); err != nil {
return err
}
message.Header.Response = true
message.Header.RecursionAvailable = true
for _, question := range message.Questions {
answer := dnsmessage.Resource{Header: dnsmessage.ResourceHeader{
Name: question.Name, Type: question.Type, Class: dnsmessage.ClassINET, TTL: 60,
}}
switch question.Type {
case dnsmessage.TypeA:
answer.Body = &dnsmessage.AResource{A: [4]byte{192, 0, 2, 10}}
case dnsmessage.TypeAAAA:
answer.Body = &dnsmessage.AAAAResource{AAAA: [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10}}
default:
continue
}
message.Answers = append(message.Answers, answer)
}
response, err := message.Pack()
if err != nil {
return err
}
binary.BigEndian.PutUint16(size[:], uint16(len(response)))
_, err = conn.Write(append(size[:], response...))
return err
}
+85 -17
View File
@@ -1,4 +1,4 @@
//go:build (windows && (amd64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (amd64 || 386))
//go:build (windows && (arm64 || amd64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (arm64 || amd64 || 386))
package extension
@@ -22,20 +22,26 @@ package extension
import (
"context"
"crypto/rand"
"errors"
"fmt"
"io"
"runtime"
"sync"
"github.com/bishopfox/sliver/implant/sliver/wasmnet"
// {{if .Config.Debug}}
"log"
// {{end}}
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
experimentalsysfs "github.com/tetratelabs/wazero/experimental/sysfs"
wasi "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/sys"
)
const wasmMemoryLimitPages = 4096 // 256 MiB
type wasmPipe struct {
Reader *io.PipeReader
Writer *io.PipeWriter
@@ -45,12 +51,17 @@ type wasmPipe struct {
type WasmExtension struct {
Name string
ctx context.Context
stop context.CancelFunc
lock sync.Mutex
mod wazero.CompiledModule
config wazero.ModuleConfig
runtime wazero.Runtime
closer api.Closer
network *wasmnet.Host
memFS *WasmMemoryFS
closeOnce sync.Once
closeErr error
Stdin *wasmPipe
Stdout *wasmPipe
@@ -59,7 +70,11 @@ type WasmExtension struct {
// IsExecuting - Check if the Wasm module runtime is currently executing
func (w *WasmExtension) IsExecuting() bool {
return w.lock.TryLock()
if !w.lock.TryLock() {
return true
}
w.lock.Unlock()
return false
}
// Execute - Execute the Wasm module with arguments, blocks during execution, returns errors
@@ -73,7 +88,11 @@ func (w *WasmExtension) Execute(args []string) (uint32, error) {
args = append([]string{"wasi"}, args...)
conf := w.config.WithArgs(args...)
if _, err := w.runtime.InstantiateModule(w.ctx, w.mod, conf); err != nil {
module, err := w.runtime.InstantiateModule(w.ctx, w.mod, conf)
if module != nil {
defer func() { _ = module.Close(w.ctx) }()
}
if err != nil {
// Note: Most compilers do not exit the module after running "_start",
// unless there was an error. This allows you to call exported functions.
if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
@@ -94,40 +113,89 @@ func (w *WasmExtension) Execute(args []string) (uint32, error) {
// Close - Close the Wasm module
func (w *WasmExtension) Close() error {
w.Stdin.Reader.Close()
w.Stdout.Reader.Close()
w.Stderr.Reader.Close()
return w.closer.Close(w.ctx)
w.closeOnce.Do(func() {
if w.stop != nil {
w.stop()
}
_ = w.Stdin.Writer.Close()
_ = w.Stdout.Writer.Close()
_ = w.Stderr.Writer.Close()
_ = w.Stdin.Reader.Close()
_ = w.Stdout.Reader.Close()
_ = w.Stderr.Reader.Close()
if w.network != nil {
w.closeErr = errors.Join(w.closeErr, w.network.Close())
}
if w.mod != nil {
w.closeErr = errors.Join(w.closeErr, w.mod.Close(context.Background()))
}
if w.runtime != nil {
w.closeErr = errors.Join(w.closeErr, w.runtime.Close(context.Background()))
}
})
return w.closeErr
}
// NewWasmExtension - Create a new Wasm extension
func NewWasmExtension(name string, wasm []byte, memFS map[string][]byte) (*WasmExtension, error) {
return NewWasmExtensionWithOptions(name, wasm, memFS)
}
// NewWasmExtensionWithOptions creates a Wasm extension with optional runtime
// features while preserving the historical NewWasmExtension API.
func NewWasmExtensionWithOptions(name string, wasm []byte, memFS map[string][]byte, options ...WasmExtensionOption) (*WasmExtension, error) {
extensionConfig := applyWasmExtensionOptions(options)
memoryFS, err := makeWasmMemFS(memFS, extensionConfig.memoryFSOptions...)
if err != nil {
return nil, err
}
fsConfig, ok := wazero.NewFSConfig().(experimentalsysfs.FSConfig)
if !ok {
return nil, errors.New("wazero writable filesystem mounting is unavailable")
}
stdinReader, stdinWriter := io.Pipe()
stdoutReader, stdoutWriter := io.Pipe()
stderrReader, stderrWriter := io.Pipe()
ctx, stop := context.WithCancel(context.Background())
wasmExt := &WasmExtension{
Name: name,
ctx: context.Background(),
lock: sync.Mutex{},
Name: name,
ctx: ctx,
stop: stop,
lock: sync.Mutex{},
memFS: memoryFS,
Stdin: &wasmPipe{Reader: stdinReader, Writer: stdinWriter},
Stdout: &wasmPipe{Reader: stdoutReader, Writer: stdoutWriter},
Stderr: &wasmPipe{Reader: stderrReader, Writer: stderrWriter},
}
wasmExt.runtime = wazero.NewRuntime(wasmExt.ctx)
runtimeConfig := wazero.NewRuntimeConfig().
WithCloseOnContextDone(true).
WithMemoryLimitPages(wasmMemoryLimitPages)
wasmExt.runtime = wazero.NewRuntimeWithConfig(wasmExt.ctx, runtimeConfig)
wasmExt.config = wazero.NewModuleConfig().
WithStdin(wasmExt.Stdin.Reader).
WithStdout(wasmExt.Stdout.Writer).
WithStderr(wasmExt.Stderr.Writer).
WithFS(makeWasmMemFS(memFS))
WithSysWalltime().
WithSysNanotime().
WithSysNanosleep().
WithOsyield(runtime.Gosched).
WithRandSource(rand.Reader).
WithFSConfig(fsConfig.WithSysFSMount(memoryFS, "/"))
var err error
wasmExt.closer, err = wasi.Instantiate(wasmExt.ctx, wasmExt.runtime)
if err != nil {
if _, err := wasi.Instantiate(wasmExt.ctx, wasmExt.runtime); err != nil {
_ = wasmExt.Close()
return nil, err
}
wasmExt.network = wasmnet.New(wasmExt.ctx)
if _, err := wasmExt.network.Instantiate(wasmExt.ctx, wasmExt.runtime); err != nil {
_ = wasmExt.Close()
return nil, err
}
wasmExt.mod, err = wasmExt.runtime.CompileModule(wasmExt.ctx, wasm)
if err != nil {
_ = wasmExt.Close()
return nil, err
}
return wasmExt, nil
+6 -1
View File
@@ -1,4 +1,4 @@
//go:build !((windows && (amd64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (amd64 || 386)))
//go:build !((windows && (arm64 || amd64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (arm64 || amd64 || 386)))
package extension
@@ -56,5 +56,10 @@ func (w *WasmExtension) Close() error {
// NewWasmExtension - For platforms that don't support Wasm extensions
func NewWasmExtension(name string, wasm []byte, memFS map[string][]byte) (*WasmExtension, error) {
return NewWasmExtensionWithOptions(name, wasm, memFS)
}
// NewWasmExtensionWithOptions - For platforms that don't support Wasm extensions
func NewWasmExtensionWithOptions(name string, wasm []byte, memFS map[string][]byte, options ...WasmExtensionOption) (*WasmExtension, error) {
return nil, errors.New("Wasm extensions are not supported on this platform")
}
@@ -0,0 +1,233 @@
//go:build (windows && (arm64 || amd64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (arm64 || amd64 || 386))
package extension
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWasmHTTPFetchExample(t *testing.T) {
wasmPath := buildWasmTestModule(
t,
"SLIVER_WASM_HTTP_FETCH_WASM",
"./implant/sliver/extension/testdata/http-fetch",
)
wasm, err := os.ReadFile(wasmPath)
mustNoError(t, err)
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(response, "local-network-ok")
}))
t.Cleanup(server.Close)
stdout, stderr, exitCode, err := runWasmTestModule(t, wasm, []string{server.URL})
mustNoError(t, err, string(stderr))
mustZero(t, exitCode, string(stderr))
assert.Contains(t, string(stdout), "200 OK")
assert.Contains(t, string(stdout), "local-network-ok")
if os.Getenv("SLIVER_WASM_LIVE_HTTPS") != "1" {
return
}
stdout, stderr, exitCode, err = runWasmTestModule(t, wasm, nil)
mustNoError(t, err, string(stderr))
mustZero(t, exitCode, string(stderr))
assert.Contains(t, string(stdout), "GET https://example.com/: 200 OK")
assert.Contains(t, strings.ToLower(string(stdout)), "example domain")
}
func TestWasmNetworkGuestRoundTrip(t *testing.T) {
wasmPath := buildWasmTestModule(
t,
"SLIVER_WASM_NETWORK_SMOKE_WASM",
"./implant/sliver/extension/testdata/network-smoke",
)
wasm, err := os.ReadFile(wasmPath)
mustNoError(t, err)
stdout, stderr, exitCode, err := runWasmTestModule(t, wasm, nil)
mustNoError(t, err, string(stderr))
mustZero(t, exitCode, string(stderr))
assert.Contains(t, string(stdout), "network-smoke-ok")
}
func TestWasmResolverDial(t *testing.T) {
wasmPath := buildWasmTestModule(
t,
"SLIVER_WASM_RESOLVER_DIAL_WASM",
"./implant/sliver/extension/testdata/resolver-dial",
)
wasm, err := os.ReadFile(wasmPath)
mustNoError(t, err)
stdout, stderr, exitCode, err := runWasmTestModule(t, wasm, nil)
mustNoError(t, err, string(stderr))
mustZero(t, exitCode, string(stderr))
assert.Contains(t, string(stdout), "resolver-dial-ok")
}
func buildWasmTestModule(t *testing.T, environmentName, packagePath string) string {
t.Helper()
if path := os.Getenv(environmentName); path != "" {
return path
}
rootAppDir := os.Getenv("SLIVER_ROOT_DIR")
if rootAppDir == "" {
t.Skipf("set %s or run through ./go-tests.sh with unpacked compiler assets", environmentName)
}
goName := "go"
wrapperName := "sliver-wasm-go"
if runtime.GOOS == "windows" {
goName += ".exe"
wrapperName += ".exe"
}
bundledGo := filepath.Join(rootAppDir, "go", "bin", goName)
if _, err := os.Stat(bundledGo); err != nil {
t.Skipf("bundled Go toolchain is not available at %s: %v", bundledGo, err)
}
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("could not resolve repository root")
}
repositoryRoot := filepath.Clean(filepath.Join(filepath.Dir(currentFile), "..", "..", ".."))
wrapperPath := filepath.Join(rootAppDir, "go", "bin", wrapperName)
if _, err := os.Stat(wrapperPath); os.IsNotExist(err) {
wrapperPattern := ".sliver-wasm-go-test-*"
if runtime.GOOS == "windows" {
wrapperPattern += ".exe"
}
wrapperFile, createErr := os.CreateTemp(filepath.Dir(wrapperPath), wrapperPattern)
if createErr != nil {
t.Fatalf("create temporary wrapper path: %v", createErr)
}
wrapperPath = wrapperFile.Name()
if closeErr := wrapperFile.Close(); closeErr != nil {
t.Fatalf("close temporary wrapper path: %v", closeErr)
}
t.Cleanup(func() {
_ = os.Remove(wrapperPath)
})
command := exec.Command(
bundledGo,
"build",
"-trimpath",
"-buildvcs=false",
"-mod=vendor",
"-o",
wrapperPath,
"./util/cmd/sliver-wasm-go",
)
command.Dir = repositoryRoot
command.Env = wasmTestEnvironment(
"GOOS="+runtime.GOOS,
"GOARCH="+runtime.GOARCH,
"CGO_ENABLED=0",
"GOTOOLCHAIN=local",
"GOWORK=off",
"GOFLAGS=",
)
output, buildErr := command.CombinedOutput()
if buildErr != nil {
t.Fatalf("build sliver-wasm-go: %v\n%s", buildErr, output)
}
} else if err != nil {
t.Fatalf("inspect sliver-wasm-go: %v", err)
}
outputPath := filepath.Join(t.TempDir(), filepath.Base(packagePath)+".wasm")
command := exec.Command(wrapperPath, "build", "-trimpath", "-o", outputPath, packagePath)
command.Dir = repositoryRoot
command.Env = wasmTestEnvironment("GOWORK=off", "GOFLAGS=-mod=vendor")
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("build %s: %v\n%s", packagePath, err, output)
}
return outputPath
}
func wasmTestEnvironment(overrides ...string) []string {
overrideKeys := make([]string, 0, len(overrides))
for _, override := range overrides {
key, _, _ := strings.Cut(override, "=")
overrideKeys = append(overrideKeys, key)
}
environment := make([]string, 0, len(os.Environ())+len(overrides))
for _, entry := range os.Environ() {
key, _, _ := strings.Cut(entry, "=")
replaced := false
for _, overrideKey := range overrideKeys {
if strings.EqualFold(key, overrideKey) {
replaced = true
break
}
}
if !replaced {
environment = append(environment, entry)
}
}
return append(environment, overrides...)
}
func runWasmTestModule(t *testing.T, wasm []byte, args []string) ([]byte, []byte, uint32, error) {
t.Helper()
extension, err := NewWasmExtension(t.Name(), wasm, nil)
mustNoError(t, err)
defer func() { _ = extension.Close() }()
var stdout bytes.Buffer
var stderr bytes.Buffer
var readers sync.WaitGroup
readers.Add(2)
go func() {
defer readers.Done()
_, _ = io.Copy(&stdout, extension.Stdout.Reader)
}()
go func() {
defer readers.Done()
_, _ = io.Copy(&stderr, extension.Stderr.Reader)
}()
exitCode, executeErr := extension.Execute(args)
_ = extension.Stdout.Writer.Close()
_ = extension.Stderr.Writer.Close()
readers.Wait()
return stdout.Bytes(), stderr.Bytes(), exitCode, executeErr
}
func mustNoError(t *testing.T, err error, context ...string) {
t.Helper()
if len(context) > 0 {
if !assert.NoError(t, err, context[0]) {
t.FailNow()
}
return
}
if !assert.NoError(t, err) {
t.FailNow()
}
}
func mustZero(t *testing.T, value uint32, context ...string) {
t.Helper()
if len(context) > 0 {
if !assert.Zero(t, value, context[0]) {
t.FailNow()
}
return
}
if !assert.Zero(t, value) {
t.FailNow()
}
}
+44
View File
@@ -0,0 +1,44 @@
package extension
/*
Sliver Implant Framework
Copyright (C) 2023 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// WasmExtensionOption configures a Wasm extension runtime without changing
// the backwards-compatible NewWasmExtension signature.
type WasmExtensionOption func(*wasmExtensionConfig)
type wasmExtensionConfig struct {
memoryFSOptions []WasmMemoryFSOption
}
// WithReadOnlyMemFS configures the extension's /memfs namespace as read-only.
func WithReadOnlyMemFS() WasmExtensionOption {
return func(config *wasmExtensionConfig) {
config.memoryFSOptions = append(config.memoryFSOptions, WithWasmMemoryFSReadOnly())
}
}
func applyWasmExtensionOptions(options []WasmExtensionOption) wasmExtensionConfig {
config := wasmExtensionConfig{}
for _, option := range options {
if option != nil {
option(&config)
}
}
return config
}
+4 -9
View File
@@ -39,10 +39,7 @@ func listWasmExtensionsHandler(data []byte, resp RPCResponse) {
log.Printf("List Wasm extensions ...")
// {{end}}
names := []string{}
for name := range tunnel_handlers.WasmExtensionCache {
names = append(names, name)
}
names := tunnel_handlers.ListWasmExtensions()
wasmExt, _ := proto.Marshal(&pb.ListWasmExtensions{Names: names, Response: &commonpb.Response{}})
resp(wasmExt, nil)
}
@@ -64,11 +61,11 @@ func registerWasmExtensionHandler(data []byte, resp RPCResponse) {
// Cache the Wasm extension in the map until we receive a
// MsgExecWasmExtensionReq message
tunnel_handlers.WasmExtensionCache[wasmExtReq.Name] = wasmExtReq
tunnel_handlers.SetWasmExtension(wasmExtReq)
// {{if .Config.Debug}}
log.Printf("*** Wasm extensions cache ***")
for name := range tunnel_handlers.WasmExtensionCache {
for _, name := range tunnel_handlers.ListWasmExtensions() {
log.Printf(" - %s", name)
}
// {{end}}
@@ -90,9 +87,7 @@ func deregisterWasmExtensionHandler(data []byte, resp RPCResponse) {
// Remove the Wasm extension from the map
errMsg := ""
if _, ok := tunnel_handlers.WasmExtensionCache[wasmExtReq.Name]; ok {
delete(tunnel_handlers.WasmExtensionCache, wasmExtReq.Name)
} else {
if !tunnel_handlers.DeleteWasmExtension(wasmExtReq.Name) {
errMsg = "Wasm extension not registered"
}
wasmExt, _ := proto.Marshal(&pb.RegisterWasmExtension{
@@ -18,7 +18,47 @@ import (
"google.golang.org/protobuf/proto"
)
var WasmExtensionCache = map[string]*pb.RegisterWasmExtensionReq{}
var (
wasmExtensionCacheMu sync.RWMutex
wasmExtensionCache = map[string]*pb.RegisterWasmExtensionReq{}
)
// GetWasmExtension returns a registered Wasm extension.
func GetWasmExtension(name string) (*pb.RegisterWasmExtensionReq, bool) {
wasmExtensionCacheMu.RLock()
defer wasmExtensionCacheMu.RUnlock()
extension, ok := wasmExtensionCache[name]
return extension, ok
}
// SetWasmExtension registers or replaces a Wasm extension.
func SetWasmExtension(extension *pb.RegisterWasmExtensionReq) {
wasmExtensionCacheMu.Lock()
defer wasmExtensionCacheMu.Unlock()
wasmExtensionCache[extension.Name] = extension
}
// ListWasmExtensions returns the registered Wasm extension names.
func ListWasmExtensions() []string {
wasmExtensionCacheMu.RLock()
defer wasmExtensionCacheMu.RUnlock()
names := make([]string, 0, len(wasmExtensionCache))
for name := range wasmExtensionCache {
names = append(names, name)
}
return names
}
// DeleteWasmExtension removes a registered Wasm extension.
func DeleteWasmExtension(name string) bool {
wasmExtensionCacheMu.Lock()
defer wasmExtensionCacheMu.Unlock()
if _, ok := wasmExtensionCache[name]; !ok {
return false
}
delete(wasmExtensionCache, name)
return true
}
// ExecWasmExtensionHandler - Execute a Wasm extension
func ExecWasmExtensionHandler(envelope *pb.Envelope, connection *transports.Connection) {
@@ -31,7 +71,7 @@ func ExecWasmExtensionHandler(envelope *pb.Envelope, connection *transports.Conn
return
}
extReq, ok := WasmExtensionCache[req.Name]
extReq, ok := GetWasmExtension(req.Name)
if !ok {
// {{if .Config.Debug}}
log.Printf("Wasm extension '%s' not found", req.Name)
@@ -94,39 +134,27 @@ func runNonInteractive(req *pb.ExecWasmExtensionReq, wasm *extension.WasmExtensi
log.Printf("Executing non-interactive wasm extension")
// {{end}}
defer func() {
_ = wasm.Close()
}()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
var readers sync.WaitGroup
readers.Add(2)
go func() {
for {
buf := make([]byte, 1024)
n, err := wasm.Stdout.Reader.Read(buf)
stdout.Write(buf[:n])
if err == io.EOF {
return
}
if err != nil {
// {{if .Config.Debug}}
log.Printf("error reading stdout: %v", err)
// {{end}}
return
}
defer readers.Done()
if _, err := io.Copy(stdout, wasm.Stdout.Reader); err != nil {
// {{if .Config.Debug}}
log.Printf("error reading stdout: %v", err)
// {{end}}
}
}()
stderr := &bytes.Buffer{}
go func() {
for {
buf := make([]byte, 1024)
n, err := wasm.Stderr.Reader.Read(buf)
stderr.Write(buf[:n])
if err == io.EOF {
return
}
if err != nil {
// {{if .Config.Debug}}
log.Printf("error reading stderr: %v", err)
// {{end}}
return
}
defer readers.Done()
if _, err := io.Copy(stderr, wasm.Stderr.Reader); err != nil {
// {{if .Config.Debug}}
log.Printf("error reading stderr: %v", err)
// {{end}}
}
}()
@@ -138,6 +166,9 @@ func runNonInteractive(req *pb.ExecWasmExtensionReq, wasm *extension.WasmExtensi
// {{end}}
errMsg = err.Error()
}
_ = wasm.Stdout.Writer.Close()
_ = wasm.Stderr.Writer.Close()
readers.Wait()
return proto.Marshal(&pb.ExecWasmExtension{
Stdout: stdout.Bytes(),
@@ -196,7 +227,7 @@ func runInteractive(req *pb.ExecWasmExtensionReq, conn *transports.Connection, w
// {{end}}
wasm.Stdout.Writer.Write([]byte(fmt.Sprintf("\r\n*** exit code %d ***\r\n", exitCode)))
wasm.Stdout.Writer.Write([]byte("Wait 10 seconds and press <enter> to continue ...\r\n"))
wasm.Close()
_ = wasm.Close()
return exitCode, err
}()
+22
View File
@@ -0,0 +1,22 @@
# implant/sliver/wasmnet
This package is the single implementation of Sliver's versioned
`sliver_wasi_net_v1` host module. Implant extensions, implant traffic
encoders, and server traffic encoders each create a private `Host` for their
wazero runtime.
The ABI exposes asynchronous TCP, UDP, and DNS operations plus polling,
cancellation, deadlines, shutdown, address lookup, and handle cleanup. Keeping
the host here makes generated implants and the server compile the same source,
while keeping socket and operation handles isolated between Wasm runtimes.
The module exports `dial_start`, `listen`, `accept_start`, `read_start`,
`write_start`, `recv_from_start`, `send_to_start`, `lookup_start`, `op_poll`,
`op_cancel`, `shutdown`, `close`, `get_addr`, and `set_deadline`. Guest code
normally reaches these through the `sliver-wasm-go` standard-library overlay
instead of importing them directly.
Unsupported Sliver targets keep a no-op stub so non-networked traffic encoders
remain buildable. A module that imports `sliver_wasi_net_v1` fails
instantiation on those targets instead of receiving partial networking
behavior.
+74
View File
@@ -0,0 +1,74 @@
//go:build (darwin && (arm64 || amd64)) || (linux && (arm64 || amd64 || 386))
package wasmnet
import (
"errors"
"syscall"
)
//nolint:gocyclo // The host-to-WASI errno mapping is intentionally exhaustive.
func wasmNetworkSystemErrno(err error) (uint32, bool) {
switch {
case errors.Is(err, syscall.EACCES):
return wasiErrnoAccess, true
case errors.Is(err, syscall.EADDRINUSE):
return wasiErrnoAddrInUse, true
case errors.Is(err, syscall.EADDRNOTAVAIL):
return wasiErrnoAddrNotAvailable, true
case errors.Is(err, syscall.EAFNOSUPPORT):
return wasiErrnoAddressFamily, true
case errors.Is(err, syscall.EAGAIN):
return wasiErrnoAgain, true
case errors.Is(err, syscall.EBADF):
return wasiErrnoBadFile, true
case errors.Is(err, syscall.ECONNABORTED):
return wasiErrnoConnAborted, true
case errors.Is(err, syscall.ECONNREFUSED):
return wasiErrnoConnRefused, true
case errors.Is(err, syscall.ECONNRESET):
return wasiErrnoConnReset, true
case errors.Is(err, syscall.EHOSTUNREACH):
return wasiErrnoHostUnreachable, true
case errors.Is(err, syscall.EINTR):
return wasiErrnoInterrupted, true
case errors.Is(err, syscall.EINVAL):
return wasiErrnoInvalid, true
case errors.Is(err, syscall.EIO):
return wasiErrnoIO, true
case errors.Is(err, syscall.EISCONN):
return wasiErrnoIsConnected, true
case errors.Is(err, syscall.EMSGSIZE):
return wasiErrnoMessageSize, true
case errors.Is(err, syscall.ENETDOWN):
return wasiErrnoNetworkDown, true
case errors.Is(err, syscall.ENETRESET):
return wasiErrnoNetworkReset, true
case errors.Is(err, syscall.ENETUNREACH):
return wasiErrnoNetworkUnreachable, true
case errors.Is(err, syscall.ENOBUFS):
return wasiErrnoNoBuffer, true
case errors.Is(err, syscall.ENOENT):
return wasiErrnoNoEntry, true
case errors.Is(err, syscall.ENOPROTOOPT):
return wasiErrnoNoProtocolOption, true
case errors.Is(err, syscall.ENOSYS):
return wasiErrnoNotImplemented, true
case errors.Is(err, syscall.ENOTCONN):
return wasiErrnoNotConnected, true
case errors.Is(err, syscall.ENOTSOCK):
return wasiErrnoNotSocket, true
case errors.Is(err, syscall.ENOTSUP):
return wasiErrnoNotSupported, true
case errors.Is(err, syscall.EPIPE):
return wasiErrnoPipe, true
case errors.Is(err, syscall.EPROTONOSUPPORT):
return wasiErrnoProtocolNotSupported, true
case errors.Is(err, syscall.ERANGE):
return wasiErrnoRange, true
case errors.Is(err, syscall.ETIMEDOUT):
return wasiErrnoTimedOut, true
default:
return 0, false
}
}
+76
View File
@@ -0,0 +1,76 @@
//go:build windows && (amd64 || arm64 || 386)
package wasmnet
import (
"errors"
"syscall"
"golang.org/x/sys/windows"
)
//nolint:gocyclo // The host-to-WASI errno mapping is intentionally exhaustive.
func wasmNetworkSystemErrno(err error) (uint32, bool) {
switch {
case errors.Is(err, syscall.EACCES), errors.Is(err, windows.WSAEACCES):
return wasiErrnoAccess, true
case errors.Is(err, syscall.EADDRINUSE), errors.Is(err, windows.WSAEADDRINUSE):
return wasiErrnoAddrInUse, true
case errors.Is(err, syscall.EADDRNOTAVAIL), errors.Is(err, windows.WSAEADDRNOTAVAIL):
return wasiErrnoAddrNotAvailable, true
case errors.Is(err, syscall.EAFNOSUPPORT), errors.Is(err, windows.WSAEAFNOSUPPORT):
return wasiErrnoAddressFamily, true
case errors.Is(err, syscall.EAGAIN), errors.Is(err, windows.WSAEWOULDBLOCK):
return wasiErrnoAgain, true
case errors.Is(err, syscall.EBADF), errors.Is(err, windows.WSAEBADF):
return wasiErrnoBadFile, true
case errors.Is(err, syscall.ECONNABORTED), errors.Is(err, windows.WSAECONNABORTED):
return wasiErrnoConnAborted, true
case errors.Is(err, syscall.ECONNREFUSED), errors.Is(err, windows.WSAECONNREFUSED):
return wasiErrnoConnRefused, true
case errors.Is(err, syscall.ECONNRESET), errors.Is(err, windows.WSAECONNRESET):
return wasiErrnoConnReset, true
case errors.Is(err, syscall.EHOSTUNREACH), errors.Is(err, windows.WSAEHOSTUNREACH):
return wasiErrnoHostUnreachable, true
case errors.Is(err, syscall.EINTR), errors.Is(err, windows.WSAEINTR):
return wasiErrnoInterrupted, true
case errors.Is(err, syscall.EINVAL), errors.Is(err, windows.WSAEINVAL):
return wasiErrnoInvalid, true
case errors.Is(err, syscall.EIO):
return wasiErrnoIO, true
case errors.Is(err, syscall.EISCONN), errors.Is(err, windows.WSAEISCONN):
return wasiErrnoIsConnected, true
case errors.Is(err, syscall.EMSGSIZE), errors.Is(err, windows.WSAEMSGSIZE):
return wasiErrnoMessageSize, true
case errors.Is(err, syscall.ENETDOWN), errors.Is(err, windows.WSAENETDOWN):
return wasiErrnoNetworkDown, true
case errors.Is(err, syscall.ENETRESET), errors.Is(err, windows.WSAENETRESET):
return wasiErrnoNetworkReset, true
case errors.Is(err, syscall.ENETUNREACH), errors.Is(err, windows.WSAENETUNREACH):
return wasiErrnoNetworkUnreachable, true
case errors.Is(err, syscall.ENOBUFS), errors.Is(err, windows.WSAENOBUFS):
return wasiErrnoNoBuffer, true
case errors.Is(err, syscall.ENOENT):
return wasiErrnoNoEntry, true
case errors.Is(err, syscall.ENOPROTOOPT), errors.Is(err, windows.WSAENOPROTOOPT):
return wasiErrnoNoProtocolOption, true
case errors.Is(err, syscall.ENOSYS):
return wasiErrnoNotImplemented, true
case errors.Is(err, syscall.ENOTCONN), errors.Is(err, windows.WSAENOTCONN):
return wasiErrnoNotConnected, true
case errors.Is(err, syscall.ENOTSOCK), errors.Is(err, windows.WSAENOTSOCK):
return wasiErrnoNotSocket, true
case errors.Is(err, syscall.ENOTSUP), errors.Is(err, windows.WSAEOPNOTSUPP):
return wasiErrnoNotSupported, true
case errors.Is(err, syscall.EPIPE), errors.Is(err, windows.WSAESHUTDOWN):
return wasiErrnoPipe, true
case errors.Is(err, syscall.EPROTONOSUPPORT), errors.Is(err, windows.WSAEPROTONOSUPPORT):
return wasiErrnoProtocolNotSupported, true
case errors.Is(err, syscall.ERANGE):
return wasiErrnoRange, true
case errors.Is(err, syscall.ETIMEDOUT), errors.Is(err, windows.WSAETIMEDOUT):
return wasiErrnoTimedOut, true
default:
return 0, false
}
}
File diff suppressed because it is too large. Load diff
+492
View File
@@ -0,0 +1,492 @@
//go:build (windows && (amd64 || arm64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (arm64 || amd64 || 386))
package wasmnet
import (
"bytes"
"context"
"net"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tetratelabs/wazero"
)
func TestWasmNetworkHostModule(t *testing.T) {
ctx := context.Background()
runtime := wazero.NewRuntime(ctx)
t.Cleanup(func() {
mustNoError(t, runtime.Close(ctx))
})
network := newWasmNetwork(ctx)
t.Cleanup(func() {
mustNoError(t, network.Close())
})
module, err := network.Instantiate(ctx, runtime)
mustNoError(t, err)
assert.Equal(t, ModuleName, module.Name())
}
func TestWasmNetworkTCP(t *testing.T) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
mustNoError(t, err)
t.Cleanup(func() {
_ = listener.Close()
})
go func() {
conn, acceptErr := listener.Accept()
if acceptErr != nil {
return
}
defer func() {
_ = conn.Close()
}()
buffer := make([]byte, 32)
n, readErr := conn.Read(buffer)
if readErr == nil {
_, _ = conn.Write(bytes.ToUpper(buffer[:n]))
}
}()
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
deadline := time.Now().Add(5 * time.Second).UnixNano()
dialResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.dial(ctx, "tcp4", "", listener.Addr().String(), deadline)
})
mustZero(t, dialResult.errno)
assert.NotZero(t, dialResult.handle)
writeResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.write(ctx, dialResult.handle, []byte("hello"), deadline)
})
mustZero(t, writeResult.errno)
assert.Equal(t, uint32(5), writeResult.n)
readResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.read(ctx, dialResult.handle, 32, deadline)
})
mustZero(t, readResult.errno)
assert.Equal(t, []byte("HELLO"), readResult.data)
}
func TestWasmNetworkConnectedUDP(t *testing.T) {
server, err := net.ListenPacket("udp4", "127.0.0.1:0")
mustNoError(t, err)
t.Cleanup(func() {
_ = server.Close()
})
go func() {
buffer := make([]byte, 32)
n, addr, readErr := server.ReadFrom(buffer)
if readErr == nil {
_, _ = server.WriteTo(bytes.ToUpper(buffer[:n]), addr)
}
}()
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
deadline := time.Now().Add(5 * time.Second).UnixNano()
dialResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.dial(ctx, "udp4", "", server.LocalAddr().String(), deadline)
})
mustZero(t, dialResult.errno)
writeResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.write(ctx, dialResult.handle, []byte("datagram"), deadline)
})
mustZero(t, writeResult.errno)
assert.Equal(t, uint32(8), writeResult.n)
readResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.read(ctx, dialResult.handle, 32, deadline)
})
mustZero(t, readResult.errno)
assert.Equal(t, []byte("DATAGRAM"), readResult.data)
}
func TestWasmNetworkUnconnectedUDP(t *testing.T) {
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
handle, errno := network.listen("udp4", "127.0.0.1:0")
mustZero(t, errno)
socket, errno := network.socket(handle)
mustZero(t, errno)
client, err := net.ListenPacket("udp4", "127.0.0.1:0")
mustNoError(t, err)
t.Cleanup(func() {
_ = client.Close()
})
_, err = client.WriteTo([]byte("ping"), socket.localAddr())
mustNoError(t, err)
deadline := time.Now().Add(5 * time.Second).UnixNano()
recvResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.recvFrom(ctx, handle, 32, deadline)
})
mustZero(t, recvResult.errno)
assert.Equal(t, []byte("ping"), recvResult.data)
assert.Equal(t, client.LocalAddr().String(), recvResult.addr)
sendResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.sendTo(ctx, handle, []byte("pong"), recvResult.addr, deadline)
})
mustZero(t, sendResult.errno)
assert.Equal(t, uint32(4), sendResult.n)
mustNoError(t, client.SetReadDeadline(time.Now().Add(5*time.Second)))
buffer := make([]byte, 32)
n, _, err := client.ReadFrom(buffer)
mustNoError(t, err)
assert.Equal(t, []byte("pong"), buffer[:n])
// UDPConn.Read also works on an unconnected listener, discarding the source
// address while retaining datagram boundaries, including empty datagrams.
mustZero(t, network.hostSetDeadline(context.Background(), handle, 1, deadline))
for _, payload := range [][]byte{[]byte("read"), {}} {
_, err = client.WriteTo(payload, socket.localAddr())
mustNoError(t, err)
readResult := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.read(ctx, handle, 32, deadline)
})
mustZero(t, readResult.errno)
assert.Equal(t, payload, readResult.data)
assert.Equal(t, uint32(len(payload)), readResult.n)
}
}
func TestWasmNetworkUnconnectedUDPReadDeadlineAndCancel(t *testing.T) {
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
handle, errno := network.listen("udp4", "127.0.0.1:0")
mustZero(t, errno)
mustZero(t, network.hostSetDeadline(context.Background(), handle, 1, time.Now().Add(-time.Second).UnixNano()))
result := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.read(ctx, handle, 32, 0)
})
assert.Equal(t, uint32(wasiErrnoTimedOut), result.errno)
mustZero(t, network.hostSetDeadline(context.Background(), handle, 1, 0))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
readResult := make(chan wasmNetworkResult, 1)
go func() {
readResult <- network.read(ctx, handle, 32, 0)
}()
cancel()
select {
case result := <-readResult:
assert.Equal(t, uint32(wasiErrnoCanceled), result.errno)
case <-time.After(5 * time.Second):
t.Fatal("UDP read remained blocked after cancellation")
}
}
func TestWasmNetworkLookupAndCleanup(t *testing.T) {
network := newWasmNetwork(context.Background())
deadline := time.Now().Add(5 * time.Second).UnixNano()
result := runWasmNetworkOperation(t, network, func(ctx context.Context) wasmNetworkResult {
return network.lookup(ctx, "ip", "localhost", deadline)
})
mustZero(t, result.errno)
assert.NotEmpty(t, result.addr)
handle, errno := network.listen("tcp4", "127.0.0.1:0")
mustZero(t, errno)
assert.NotZero(t, handle)
mustNoError(t, network.Close())
_, errno = network.socket(handle)
assert.Equal(t, uint32(wasiErrnoBadFile), errno)
_, errno = network.listen("tcp4", "127.0.0.1:0")
assert.Equal(t, uint32(wasiErrnoCanceled), errno)
}
func TestWasmNetworkOperationBudget(t *testing.T) {
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
operationCount := wasmNetworkMaxPending / wasmNetworkMaxBuffer
operationIDs := make([]uint32, 0, operationCount)
releaseWorkers := make(chan struct{})
var workers sync.WaitGroup
for range operationCount {
workers.Add(1)
opID, errno := network.startOperation(wasmNetworkMaxBuffer, func(ctx context.Context) wasmNetworkResult {
defer workers.Done()
<-ctx.Done()
<-releaseWorkers
return wasmNetworkResult{errno: wasiErrnoCanceled}
})
mustZero(t, errno)
operationIDs = append(operationIDs, opID)
}
_, errno := network.startOperation(1, func(context.Context) wasmNetworkResult {
return wasmNetworkResult{}
})
assert.Equal(t, uint32(wasiErrnoNoBuffer), errno)
for _, opID := range operationIDs {
mustZero(t, network.cancelOperation(opID))
}
_, errno = network.startOperation(1, func(context.Context) wasmNetworkResult {
return wasmNetworkResult{}
})
assert.Equal(t, uint32(wasiErrnoNoBuffer), errno)
network.mu.Lock()
pending := network.pending
operationLength := len(network.operations)
network.mu.Unlock()
assert.Equal(t, uint64(wasmNetworkMaxPending), pending)
assert.Zero(t, operationLength)
close(releaseWorkers)
workers.Wait()
deadline := time.Now().Add(5 * time.Second)
for {
network.mu.Lock()
pending = network.pending
network.mu.Unlock()
if pending == 0 || time.Now().After(deadline) {
break
}
time.Sleep(time.Millisecond)
}
assert.Zero(t, pending)
opID, errno := network.startOperation(1, func(ctx context.Context) wasmNetworkResult {
<-ctx.Done()
return wasmNetworkResult{errno: wasiErrnoCanceled}
})
mustZero(t, errno)
mustZero(t, network.cancelOperation(opID))
}
func TestWasmNetworkIDWrapSkipsLiveEntries(t *testing.T) {
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
firstHandle, errno := network.addHandle(&wasmNetworkSocket{})
mustZero(t, errno)
assert.Equal(t, uint32(1), firstHandle)
network.mu.Lock()
network.nextHandle = ^uint32(0)
network.mu.Unlock()
lastHandle, errno := network.addHandle(&wasmNetworkSocket{})
mustZero(t, errno)
assert.Equal(t, ^uint32(0), lastHandle)
wrappedHandle, errno := network.addHandle(&wasmNetworkSocket{})
mustZero(t, errno)
assert.Equal(t, uint32(2), wrappedHandle)
var workers sync.WaitGroup
startBlockingOperation := func() uint32 {
workers.Add(1)
opID, startErrno := network.startOperation(0, func(ctx context.Context) wasmNetworkResult {
defer workers.Done()
<-ctx.Done()
return wasmNetworkResult{errno: wasiErrnoCanceled}
})
mustZero(t, startErrno)
return opID
}
firstOperation := startBlockingOperation()
assert.Equal(t, uint32(1), firstOperation)
network.mu.Lock()
network.nextOp = ^uint32(0)
network.mu.Unlock()
lastOperation := startBlockingOperation()
assert.Equal(t, ^uint32(0), lastOperation)
wrappedOperation := startBlockingOperation()
assert.Equal(t, uint32(2), wrappedOperation)
for _, opID := range []uint32{firstOperation, lastOperation, wrappedOperation} {
mustZero(t, network.cancelOperation(opID))
}
workers.Wait()
}
func TestWasmNetworkOperationLimitTracksCanceledWorkers(t *testing.T) {
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
releaseWorkers := make(chan struct{})
operationIDs := make([]uint32, 0, wasmNetworkMaxOperations)
var workers sync.WaitGroup
for range wasmNetworkMaxOperations {
workers.Add(1)
opID, errno := network.startOperation(0, func(ctx context.Context) wasmNetworkResult {
defer workers.Done()
<-ctx.Done()
<-releaseWorkers
return wasmNetworkResult{errno: wasiErrnoCanceled}
})
mustZero(t, errno)
operationIDs = append(operationIDs, opID)
}
for _, opID := range operationIDs {
mustZero(t, network.cancelOperation(opID))
}
_, errno := network.startOperation(0, func(context.Context) wasmNetworkResult {
return wasmNetworkResult{}
})
assert.Equal(t, uint32(wasiErrnoNoBuffer), errno)
close(releaseWorkers)
workers.Wait()
deadline := time.Now().Add(5 * time.Second)
for {
network.mu.Lock()
activeWorkers := network.workers
network.mu.Unlock()
if activeWorkers == 0 {
break
}
if time.Now().After(deadline) {
t.Fatalf("active worker count did not drain: %d", activeWorkers)
}
time.Sleep(time.Millisecond)
}
opID, errno := network.startOperation(0, func(ctx context.Context) wasmNetworkResult {
<-ctx.Done()
return wasmNetworkResult{errno: wasiErrnoCanceled}
})
mustZero(t, errno)
mustZero(t, network.cancelOperation(opID))
}
func TestWasmNetworkCancelCompletedHandle(t *testing.T) {
network := newWasmNetwork(context.Background())
t.Cleanup(func() {
mustNoError(t, network.Close())
})
handle, errno := network.listen("tcp4", "127.0.0.1:0")
mustZero(t, errno)
opID, errno := network.startOperation(0, func(context.Context) wasmNetworkResult {
return wasmNetworkResult{handle: handle}
})
mustZero(t, errno)
network.mu.Lock()
operation := network.operations[opID]
network.mu.Unlock()
select {
case <-operation.done:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for completed handle operation")
}
mustZero(t, network.cancelOperation(opID))
_, errno = network.socket(handle)
assert.Equal(t, uint32(wasiErrnoBadFile), errno)
}
func TestWasmNetworkInterruptStopWaits(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
entered := make(chan struct{})
release := make(chan struct{})
stop := wasmNetworkInterrupt(ctx, func() {
close(entered)
<-release
})
cancel()
select {
case <-entered:
case <-time.After(5 * time.Second):
t.Fatal("interrupt callback did not start")
}
returned := make(chan struct{})
go func() {
stop()
close(returned)
}()
select {
case <-returned:
t.Fatal("interrupt stop returned before the callback exited")
case <-time.After(25 * time.Millisecond):
}
close(release)
select {
case <-returned:
case <-time.After(5 * time.Second):
t.Fatal("interrupt stop did not return after the callback exited")
}
}
func TestWasmNetworkNXDomainErrno(t *testing.T) {
errno := wasmNetworkErrno(&net.DNSError{
Err: "no such host",
Name: "does-not-exist.invalid",
IsNotFound: true,
})
assert.Equal(t, uint32(wasiErrnoNoEntry), errno)
}
func runWasmNetworkOperation(
t *testing.T,
network *Host,
fn func(context.Context) wasmNetworkResult,
) wasmNetworkResult {
t.Helper()
opID, errno := network.startOperation(0, fn)
mustZero(t, errno)
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
result, done, pollErrno := network.pollOperation(opID)
if pollErrno == wasiErrnoAgain {
time.Sleep(time.Millisecond)
continue
}
mustZero(t, pollErrno)
assert.True(t, done)
network.finishOperation(opID)
return result
}
t.Fatal("timed out waiting for Wasm network operation")
return wasmNetworkResult{}
}
func mustNoError(t *testing.T, err error, context ...string) {
t.Helper()
if len(context) > 0 {
if !assert.NoError(t, err, context[0]) {
t.FailNow()
}
return
}
if !assert.NoError(t, err) {
t.FailNow()
}
}
func mustZero(t *testing.T, value uint32, context ...string) {
t.Helper()
if len(context) > 0 {
if !assert.Zero(t, value, context[0]) {
t.FailNow()
}
return
}
if !assert.Zero(t, value) {
t.FailNow()
}
}
@@ -0,0 +1,28 @@
//go:build !((windows && (amd64 || arm64 || 386)) || (darwin && (arm64 || amd64)) || (linux && (arm64 || amd64 || 386)))
package wasmnet
import (
"context"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
// Host is a no-op network host on platforms where Sliver does not expose the
// networking ABI. Modules that do not import ModuleName continue to run;
// networking-aware modules fail instantiation because the host module is not
// registered.
type Host struct{}
func New(context.Context) *Host {
return &Host{}
}
func (*Host) Instantiate(context.Context, wazero.Runtime) (api.Module, error) {
return nil, nil
}
func (*Host) Close() error {
return nil
}
+7
View File
@@ -0,0 +1,7 @@
// Package wasmnet implements the Sliver networking host module for WASI
// guests.
package wasmnet
// ModuleName is the versioned host module imported by Sliver networking-aware
// WASI guests.
const ModuleName = "sliver_wasi_net_v1"
+1
View File
@@ -36,6 +36,7 @@ package-lock.json
.vagrant
zig-cache/
.zig-cache/
zig-out/
.DS_Store
+107 -103
View File
@@ -1,7 +1,7 @@
gofumpt := mvdan.cc/gofumpt@v0.5.0
gofumpt := mvdan.cc/gofumpt@v0.6.0
gosimports := github.com/rinchsan/gosimports/cmd/[email protected]
golangci_lint := github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
golangci_lint := github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.5
asmfmt := github.com/klauspost/asmfmt/cmd/[email protected]
# sync this with netlify.toml!
hugo := github.com/gohugoio/[email protected]
@@ -20,22 +20,6 @@ main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,
go_test_options ?= -timeout 300s
ensureCompilerFastest := -ldflags '-X github.com/tetratelabs/wazero/internal/integration_test/vs.ensureCompilerFastest=true'
.PHONY: bench
bench:
@go build ./internal/integration_test/bench/...
@# Don't use -test.benchmem as it isn't accurate when comparing against CGO libs
@for d in vs/time vs/wasmedge vs/wasmtime ; do \
cd ./internal/integration_test/$$d ; \
go test -bench=. . -tags='wasmedge' $(ensureCompilerFastest) ; \
cd - ;\
done
bench_testdata_dir := internal/integration_test/bench/testdata
.PHONY: build.bench
build.bench:
@tinygo build -o $(bench_testdata_dir)/case.wasm -scheduler=none --no-debug -target=wasi $(bench_testdata_dir)/case.go
.PHONY: test.examples
test.examples:
@go test $(go_test_options) ./examples/... ./imports/assemblyscript/example/... ./imports/emscripten/... ./imports/wasi_snapshot_preview1/example/...
@@ -52,14 +36,24 @@ build.examples.as:
build.examples.zig: examples/allocation/zig/testdata/greet.wasm imports/wasi_snapshot_preview1/example/testdata/zig/cat.wasm imports/wasi_snapshot_preview1/testdata/zig/wasi.wasm
@cd internal/testing/dwarftestdata/testdata/zig; zig build; mv zig-out/*/main.wasm ./ # Need DWARF custom sections.
tinygo_sources := examples/basic/testdata/add.go examples/allocation/tinygo/testdata/greet.go examples/cli/testdata/cli.go imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.go imports/wasi_snapshot_preview1/testdata/tinygo/wasi.go cmd/wazero/testdata/cat/cat.go
.PHONY: build.examples.tinygo
build.examples.tinygo: $(tinygo_sources)
tinygo_reactor_sources_reactor := examples/basic/testdata/add.go examples/allocation/tinygo/testdata/greet.go
.PHONY: build.examples.tinygo_reactor
build.examples.tinygo_reactor: $(tinygo_sources_reactor)
@for f in $^; do \
tinygo build -o $$(echo $$f | sed -e 's/\.go/\.wasm/') -scheduler=none --no-debug --target=wasi $$f; \
tinygo build -o $$(echo $$f | sed -e 's/\.go/\.wasm/') -scheduler=none --no-debug --target=wasip1 -buildmode=c-shared $$f; \
done
tinygo_sources_clis := examples/cli/testdata/cli.go imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.go imports/wasi_snapshot_preview1/testdata/tinygo/wasi.go cmd/wazero/testdata/cat/cat.go
.PHONY: build.examples.tinygo_clis
build.examples.tinygo_clis: $(tinygo_sources_clis)
@for f in $^; do \
tinygo build -o $$(echo $$f | sed -e 's/\.go/\.wasm/') -scheduler=none --no-debug --target=wasip1 $$f; \
done
@mv cmd/wazero/testdata/cat/cat.wasm cmd/wazero/testdata/cat/cat-tinygo.wasm
.PHONY: build.examples.tinygo
build.examples.tinygo: build.examples.tinygo_reactor build.examples.tinygo_clis
# We use zig to build C as it is easy to install and embeds a copy of zig-cc.
# Note: Don't use "-Oz" as that breaks our wasi sock example.
c_sources := imports/wasi_snapshot_preview1/example/testdata/zig-cc/cat.c imports/wasi_snapshot_preview1/testdata/zig-cc/wasi.c internal/testing/dwarftestdata/testdata/zig-cc/main.c
@@ -96,41 +90,67 @@ build.examples.emscripten: $(emscripten_sources)
done
%/greet.wasm : cargo_target := wasm32-unknown-unknown
%/cat.wasm : cargo_target := wasm32-wasi
%/wasi.wasm : cargo_target := wasm32-wasi
%/cat.wasm : cargo_target := wasm32-wasip1
%/wasi.wasm : cargo_target := wasm32-wasip1
.PHONY: build.examples.rust
build.examples.rust: examples/allocation/rust/testdata/greet.wasm imports/wasi_snapshot_preview1/example/testdata/cargo-wasi/cat.wasm imports/wasi_snapshot_preview1/testdata/cargo-wasi/wasi.wasm internal/testing/dwarftestdata/testdata/rust/main.wasm.xz
# Normally, we build release because it is smaller. Testing dwarf requires the debug build.
internal/testing/dwarftestdata/testdata/rust/main.wasm.xz:
cd $(@D) && cargo wasi build
mv $(@D)/target/wasm32-wasi/debug/main.wasm $(@D)
cd $(@D) && cargo build --target wasm32-wasip1
mv $(@D)/target/wasm32-wasip1/debug/main.wasm $(@D)
cd $(@D) && xz -k -f ./main.wasm # Rust's DWARF section is huge, so compress it.
# Builds rust using cargo normally, or cargo-wasi.
# Builds rust using cargo normally
%.wasm: %.rs
@(cd $(@D); cargo $(if $(findstring wasi,$(cargo_target)),wasi build,build --target $(cargo_target)) --release)
@(cd $(@D); cargo build --target $(cargo_target) --release)
@mv $(@D)/target/$(cargo_target)/release/$(@F) $(@D)
spectest_base_dir := internal/integration_test/spectest
spectest_v1_dir := $(spectest_base_dir)/v1
spectest_v1_testdata_dir := $(spectest_v1_dir)/testdata
spec_version_v1 := wg-1.0
spectest_v2_dir := $(spectest_base_dir)/v2
spectest_v2_testdata_dir := $(spectest_v2_dir)/testdata
# Latest draft state as of March 12, 2024.
spec_version_v2 := 1c5e5d178bd75c79b7a12881c529098beaee2a05
spec_version_v2 := wg-2.0
spectest_threads_dir := $(spectest_base_dir)/threads
spectest_threads_testdata_dir := $(spectest_threads_dir)/testdata
# From https://github.com/WebAssembly/threads/tree/upstream-rebuild which has not been merged to main yet.
# It will likely be renamed to main in the future - https://github.com/WebAssembly/threads/issues/216.
spec_version_threads := 3635ca51a17e57e106988846c5b0e0cc48ac04fc
spec_version_threads := ff17701446d8e2086142423ef77ae947a025e26f
spectest_tail_call_dir := $(spectest_base_dir)/tail-call
spectest_tail_call_testdata_dir := $(spectest_tail_call_dir)/testdata
spec_version_tail_call := 88e97b0f742f4c3ee01fea683da130f344dd7b02
spectest_extended_const_dir := $(spectest_base_dir)/extended-const
spectest_extended_const_testdata_dir := $(spectest_extended_const_dir)/testdata
spectest_exception_handling_dir := $(spectest_base_dir)/exception-handling
spectest_exception_handling_testdata_dir := $(spectest_exception_handling_dir)/testdata
spec_version_exception_handling := 13734f8fb871a5dab939070f893adbd90bffe28c
spectest_typed_function_references_dir := $(spectest_base_dir)/typed-function-references
spectest_typed_function_references_testdata_dir := $(spectest_typed_function_references_dir)/testdata
spec_version_typed_function_references := 74d2ec81d15efd3c0f2fba46a023f376101d8e46
typed_function_references_wast_files := \
br_on_non_null.wast br_on_null.wast br_table.wast call_ref.wast elem.wast \
func.wast linking.wast local_init.wast ref.wast ref_as_non_null.wast \
ref_func.wast ref_is_null.wast ref_null.wast return_call_indirect.wast \
return_call_ref.wast return_call.wast select.wast table-sub.wast table.wast \
type-equivalence.wast unreached-invalid.wast unreached-valid.wast
.PHONY: build.spectest
build.spectest:
@$(MAKE) build.spectest.v1
@$(MAKE) build.spectest.v2
@$(MAKE) build.spectest.threads
@$(MAKE) build.spectest.tail_call
@$(MAKE) build.spectest.extended_const
@$(MAKE) build.spectest.exception_handling
@$(MAKE) build.spectest.typed_function_references
.PHONY: build.spectest.v1
build.spectest.v1: # Note: wabt by default uses >1.0 features, so wast2json flags might drift as they include more. See WebAssembly/wabt#1878
@@ -161,6 +181,7 @@ build.spectest.v1: # Note: wabt by default uses >1.0 features, so wast2json flag
.PHONY: build.spectest.v2
build.spectest.v2: # Note: SIMD cases are placed in the "simd" subdirectory.
@rm -rf $(spectest_v2_testdata_dir)
@mkdir -p $(spectest_v2_testdata_dir)
@cd $(spectest_v2_testdata_dir) \
&& curl -sSL 'https://api.github.com/repos/WebAssembly/spec/contents/test/core?ref=$(spec_version_v2)' | jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O
@@ -174,16 +195,56 @@ build.spectest.v2: # Note: SIMD cases are placed in the "simd" subdirectory.
# https://github.com/WebAssembly/wabt/issues/2348#issuecomment-1878003959
.PHONY: build.spectest.threads
build.spectest.threads:
@rm -rf $(spectest_threads_testdata_dir)
@mkdir -p $(spectest_threads_testdata_dir)
@cd $(spectest_threads_testdata_dir) \
&& curl -sSL 'https://api.github.com/repos/WebAssembly/threads/contents/test/core?ref=$(spec_version_threads)' | jq -r '.[]| .download_url' | grep -E "atomic.wast" | xargs -Iurl curl -sJL url -O
&& curl -sSL 'https://api.github.com/repos/WebAssembly/threads/contents/test/core/threads?ref=$(spec_version_threads)' | jq -r '.[]| .download_url' | grep -E "/atomic.wast" | xargs -Iurl curl -sJL url -O
@cd $(spectest_threads_testdata_dir) && for f in `find . -name '*.wast'`; do \
wast2json --enable-threads --debug-names $$f; \
done
.PHONY: build.spectest.extended_const
build.spectest.extended_const:
@cd $(spectest_extended_const_testdata_dir) && for f in `find . -name '*.wast'`; do \
wast2json --enable-extended-const --debug-names $$f; \
done
.PHONY: build.spectest.tail_call
build.spectest.tail_call:
@rm -rf $(spectest_tail_call_testdata_dir)
@mkdir -p $(spectest_tail_call_testdata_dir)
@cd $(spectest_tail_call_testdata_dir) \
&& curl -sSL 'https://api.github.com/repos/WebAssembly/testsuite/contents/proposals/tail-call?ref=$(spec_version_tail_call)' | jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O
@cd $(spectest_tail_call_testdata_dir) && for f in `find . -name '*.wast'`; do \
wast2json --enable-tail-call --debug-names $$f; \
done
.PHONY: build.spectest.exception_handling
build.spectest.exception_handling:
@rm -rf $(spectest_exception_handling_testdata_dir)
@mkdir -p $(spectest_exception_handling_testdata_dir)
@cd $(spectest_exception_handling_testdata_dir) \
&& curl -sSL 'https://api.github.com/repos/WebAssembly/spec/contents/test/core/exceptions?ref=$(spec_version_exception_handling)' \
| jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O
@cd $(spectest_exception_handling_testdata_dir) && for f in `find . -name '*.wast'`; do \
wasm-tools json-from-wast --wasm-dir . -o $$(basename $$f .wast).json $$f || true; \
done
.PHONY: build.spectest.typed_function_references
build.spectest.typed_function_references:
@rm -rf $(spectest_typed_function_references_testdata_dir)
@mkdir -p $(spectest_typed_function_references_testdata_dir)
@cd $(spectest_typed_function_references_testdata_dir) \
&& for f in $(typed_function_references_wast_files); do \
curl -sJL "https://raw.githubusercontent.com/WebAssembly/function-references/$(spec_version_typed_function_references)/test/core/$$f" -O; \
done
@cd $(spectest_typed_function_references_testdata_dir) && for f in `find . -name '*.wast'`; do \
wasm-tools json-from-wast --wasm-dir . -o $$(basename $$f .wast).json $$f || true; \
done
.PHONY: test
test:
@go test $(go_test_options) $$(go list ./... | grep -vE '$(spectest_v1_dir)|$(spectest_v2_dir)')
@go test $(go_test_options) ./...
@cd internal/version/testdata && go test $(go_test_options) ./...
@cd internal/integration_test/fuzz/wazerolib && CGO_ENABLED=0 WASM_BINARY_PATH=testdata/test.wasm go test ./...
@@ -194,17 +255,6 @@ coverage: ## Generate test coverage
@go test -coverprofile=coverage.txt -covermode=atomic --coverpkg=$(coverpkg) $(main_packages)
@go tool cover -func coverage.txt
.PHONY: spectest
spectest:
@$(MAKE) spectest.v1
@$(MAKE) spectest.v2
spectest.v1:
@go test $(go_test_options) $$(go list ./... | grep $(spectest_v1_dir))
spectest.v2:
@go test $(go_test_options) $$(go list ./... | grep $(spectest_v2_dir))
golangci_lint_path := $(shell go env GOPATH)/bin/golangci-lint
$(golangci_lint_path):
@@ -214,7 +264,7 @@ golangci_lint_goarch ?= $(shell go env GOARCH)
.PHONY: lint
lint: $(golangci_lint_path)
@GOARCH=$(golangci_lint_goarch) CGO_ENABLED=0 $(golangci_lint_path) run --timeout 5m
@GOARCH=$(golangci_lint_goarch) CGO_ENABLED=0 $(golangci_lint_path) run --timeout 5m -E testableexamples
.PHONY: format
format:
@@ -237,13 +287,10 @@ check:
@GOARCH=wasm GOOS=wasip1 go build ./...
# Ensure we build on aix. See #1723
@GOARCH=ppc64 GOOS=aix go build ./...
# Ensure we build on windows:
@GOARCH=amd64 GOOS=windows go build ./...
# Ensure we build on an arbitrary operating system:
@GOARCH=amd64 GOOS=dragonfly go build ./...
# Ensure we build on solaris/illumos:
@GOARCH=amd64 GOOS=illumos go build ./...
@GOARCH=amd64 GOOS=solaris go build ./...
# Ensure we build on linux s390x. See #2412
@GOARCH=s390x GOOS=linux go build ./...
# Ensure we build on linux ppc64le. See #2412
@GOARCH=ppc64le GOOS=linux go build ./...
# Ensure we build on linux arm for Dapr:
# gh release view -R dapr/dapr --json assets --jq 'first(.assets[] | select(.name = "daprd_linux_arm.tar.gz") | {url, downloadCount})'
@GOARCH=arm GOOS=linux go build ./...
@@ -291,22 +338,15 @@ libsodium:
#### CLI release related ####
VERSION ?= dev
# Default to a dummy version 0.0.1.1, which is always lower than a real release.
# Legal version values should look like 'x.x.x.x' where x is an integer from 0 to 65534.
# https://learn.microsoft.com/en-us/windows/win32/msi/productversion?redirectedfrom=MSDN
# https://stackoverflow.com/questions/9312221/msi-version-numbers
MSI_VERSION ?= 0.0.1.1
non_windows_platforms := darwin_amd64 darwin_arm64 linux_amd64 linux_arm64
non_windows_archives := $(non_windows_platforms:%=dist/wazero_$(VERSION)_%.tar.gz)
windows_platforms := windows_amd64 # TODO: add arm64 windows once we start testing on it.
windows_archives := $(windows_platforms:%=dist/wazero_$(VERSION)_%.zip) $(windows_platforms:%=dist/wazero_$(VERSION)_%.msi)
windows_archives := $(windows_platforms:%=dist/wazero_$(VERSION)_%.zip)
checksum_txt := dist/wazero_$(VERSION)_checksums.txt
# define macros for multi-platform builds. these parse the filename being built
go-arch = $(if $(findstring amd64,$1),amd64,arm64)
go-os = $(if $(findstring .exe,$1),windows,$(if $(findstring linux,$1),linux,darwin))
# msi-arch is a macro so we can detect it based on the file naming convention
msi-arch = $(if $(findstring amd64,$1),x64,arm64)
build/wazero_%/wazero:
$(call go-build,$@,$<)
@@ -331,51 +371,15 @@ define go-build
@echo build "ok"
endef
# this makes a marker file ending in .signed to avoid repeatedly calling codesign
%.signed: %
$(call codesign,$<)
@touch $@
# This requires osslsigncode package (apt or brew) or latest windows release from mtrojnar/osslsigncode
#
# Default is self-signed while production should be a Digicert signing key
#
# Ex.
# ```bash
# keytool -genkey -alias wazero -storetype PKCS12 -keyalg RSA -keysize 2048 -storepass wazero-bunch \
# -keystore wazero.p12 -dname "O=wazero,CN=wazero.io" -validity 3650
# ```
WINDOWS_CODESIGN_P12 ?= packaging/msi/wazero.p12
WINDOWS_CODESIGN_PASSWORD ?= wazero-bunch
define codesign
@printf "$(ansi_format_dark)" codesign "signing $1"
@osslsigncode sign -h sha256 -pkcs12 ${WINDOWS_CODESIGN_P12} -pass "${WINDOWS_CODESIGN_PASSWORD}" \
-n "wazero is the zero dependency WebAssembly runtime for Go developers" -i https://wazero.io -t http://timestamp.digicert.com \
$(if $(findstring msi,$(1)),-add-msi-dse) -in $1 -out $1-signed
@mv $1-signed $1
@printf "$(ansi_format_bright)" codesign "ok"
endef
# This task is only supported on Windows, where we use candle.exe (compile wxs to wixobj) and light.exe (link to msi)
dist/wazero_$(VERSION)_%.msi: build/wazero_%/wazero.exe.signed
ifeq ($(OS),Windows_NT)
@echo msi "building $@"
@mkdir -p $(@D)
@candle -nologo -arch $(call msi-arch,$@) -dVersion=$(MSI_VERSION) -dBin=$(<:.signed=) -o build/wazero.wixobj packaging/msi/wazero.wxs
@light -nologo -o $@ build/wazero.wixobj -spdb
$(call codesign,$@)
@echo msi "ok"
endif
dist/wazero_$(VERSION)_%.zip: build/wazero_%/wazero.exe.signed
dist/wazero_$(VERSION)_%.zip: build/wazero_%/wazero.exe
@echo zip "zipping $@"
@mkdir -p $(@D)
@zip -qj $@ $(<:.signed=)
@zip -qj $@ $<
@echo zip "ok"
# Darwin doesn't have sha256sum. See https://github.com/actions/virtual-environments/issues/90
sha256sum := $(if $(findstring darwin,$(shell go env GOOS)),shasum -a 256,sha256sum)
$(checksum_txt):
@cd $(@D); touch $(@F); $(sha256sum) * >> $(@F)
$(checksum_txt): $(non_windows_archives) $(windows_archives)
@cd $(@D); touch $(@F); $(sha256sum) * > $(@F)
dist: $(non_windows_archives) $(if $(findstring Windows_NT,$(OS)),$(windows_archives),) $(checksum_txt)
dist: $(non_windows_archives) $(windows_archives) $(checksum_txt)
+33 -69
View File
@@ -7,24 +7,19 @@ have heavy impact usually due to CGO. By avoiding CGO, wazero avoids
prerequisites such as shared libraries or libc, and lets users keep features
like cross compilation.
Avoiding go.mod dependencies reduces interference on Go version support, and
size of a statically compiled binary. However, doing so brings some
Avoiding most `go.mod` dependencies reduces interference on Go version support,
and size of a statically compiled binary. However, doing so brings some
responsibility into the project.
Go's native platform support is good: We don't need platform-specific code to
Go's native platform support is good: we don't need platform-specific code to
get monotonic time, nor do we need much work to implement certain features
needed by our compiler such as `mmap`. That said, Go does not support all
common operating systems to the same degree. For example, Go 1.18 includes
`Mprotect` on Linux and Darwin, but not FreeBSD.
common operating systems to the same degree.
The general tradeoff the project takes from a zero dependency policy is more
The general tradeoff the project takes from a strict dependency policy is more
explicit support of platforms (in the compiler runtime), as well a larger and
more technically difficult codebase.
At some point, we may allow extensions to supply their own platform-specific
hooks. Until then, one end user impact/tradeoff is some glitches trying
untested platforms (with the Compiler runtime).
### Why do we use CGO to implement system calls on darwin?
wazero is dependency and CGO free by design. In some cases, we have code that
@@ -45,25 +40,20 @@ This plays to our advantage for system calls that aren't yet exposed in the Go
standard library, notably `futimens` for nanosecond-precision timestamp
manipulation.
### Why not x/sys
### Why x/sys
Going beyond Go's SDK limitations can be accomplished with their [x/sys library](https://pkg.go.dev/golang.org/x/sys/unix).
For example, this includes `zsyscall_freebsd_amd64.go` missing from the Go SDK.
The [x/sys library](https://pkg.go.dev/golang.org/x/sys/unix) is currently
our only `go.mod` dependency.
However, like all dependencies, x/sys is a source of conflict. For example,
x/sys had to be in order to upgrade to Go 1.18.
That module is maintained by the Go authors, and covers OSes that the syscall
package neglects.
If we depended on x/sys, we could get more precise functionality needed for
features such as clocks or more platform support for the compiler runtime.
After [heavy consideration](https://github.com/wazero/wazero/issues/2434) we
decided to add it as a dependency.
That said, formally supporting an operating system may still require testing as
even use of x/sys can require platform-specifics. For example, [mmap-go](https://github.com/edsrzf/mmap-go)
uses x/sys, but also mentions limitations, some not surmountable with x/sys
alone.
Regardless, we may at some point introduce a separate go.mod for users to use
x/sys as a platform plugin without forcing all users to maintain that
dependency.
Using was shown to improve the experience of using wazero on older,
or less common, OSes without increasing the maintenance work, or creating
deployment issues for users of wazero.
## Project structure
@@ -471,17 +461,6 @@ case, a user can use multiple runtimes until "multi-store" is better understood.
If later, we have demand for multiple stores, that can be accomplished by overload. e.g. `Runtime.InstantiateInStore` or
`Runtime.Store(name) Store`.
## wazeroir
wazero's intermediate representation (IR) is called `wazeroir`. Lowering into an IR provides us a faster interpreter
and a closer to assembly representation for used by our compiler.
### Intermediate Representation (IR) design
`wazeroir`'s initial design borrowed heavily from the defunct `microwasm` format (a.k.a. LightbeamIR). Notably,
`wazeroir` doesn't have block operations: this simplifies the implementation.
Note: `microwasm` was never specified formally, and only exists in a historical codebase of wasmtime:
https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/microwasm.rs
## Exit
### Why do we only return a `sys.ExitError` on a non-zero exit code?
@@ -518,7 +497,7 @@ inserted after exit: https://github.com/emscripten-core/emscripten/issues/12322
## WASI
Unfortunately, (WASI Snapshot Preview 1)[https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md] is not formally defined enough, and has APIs with ambiguous semantics.
Unfortunately, [WASI Snapshot Preview 1](https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md) is not formally defined enough, and has APIs with ambiguous semantics.
This section describes how Wazero interprets and implements the semantics of several WASI APIs that may be interpreted differently by different wasm runtimes.
Those APIs may affect the portability of a WASI application.
@@ -568,8 +547,7 @@ In short, wazero defined system configuration in `ModuleConfig`, not a WASI type
one spec to another with minimal impact. This has other helpful benefits, as centralized resources are simpler to close
coherently (ex via `Module.Close`).
In reflection, this worked well as more ABI became usable in wazero. For example, `GOOS=js GOARCH=wasm` code uses the
same `ModuleConfig` (and `FSConfig`) WASI uses, and in compatible ways.
In reflection, this worked well as more ABI became usable in wazero.
### Background on `ModuleConfig` design
@@ -753,8 +731,7 @@ independent of the root file system. This intended to help separate concerns
like mutability of files, but it didn't work and was removed.
Compilers that target wasm act differently with regard to the working
directory. For example, while `GOOS=js` uses host functions to track the
working directory, WASI host functions do not. wasi-libc, used by TinyGo,
directory. For example, wasi-libc, used by TinyGo,
tracks working directory changes in compiled wasm instead: initially "/" until
code calls `chdir`. Zig assumes the first pre-opened file descriptor is the
working directory.
@@ -769,7 +746,7 @@ value (possibly `PWD`). Those unable to control the compiled code should only
use absolute paths in configuration.
See
* https://github.com/golang/go/blob/go1.20/src/syscall/fs_js.go#L324
* https://github.com/golang/go/blob/go1.24.0/src/syscall/fs_js.go#L341
* https://github.com/WebAssembly/wasi-libc/pull/214#issue-673090117
* https://github.com/ziglang/zig/blob/53a9ee699a35a3d245ab6d1dac1f0687a4dcb42c/src/main.zig#L32
@@ -829,10 +806,7 @@ The main reason is that `os.DirFS` is a virtual filesystem abstraction while
WASI is an abstraction over syscalls. For example, the signature of `fs.Open`
does not permit use of flags. This creates conflict on what default behaviors
to take when Go implemented `os.DirFS`. On the other hand, `path_open` can pass
flags, and in fact tests require them to be honored in specific ways. This
extends beyond WASI as even `GOOS=js GOARCH=wasm` compiled code requires
certain flags passed to `os.OpenFile` which are impossible to pass due to the
signature of `fs.FS`.
flags, and in fact tests require them to be honored in specific ways.
This conflict requires us to choose what to be more compatible with, and which
type of user to surprise the least. We assume there will be more developers
@@ -840,10 +814,6 @@ compiling code to wasm than developers of custom filesystem plugins, and those
compiling code to wasm will be better served if we are compatible with WASI.
Hence on conflict, we prefer WASI behavior vs the behavior of `os.DirFS`.
Meanwhile, it is possible that Go will one day compile to `GOOS=wasi` in
addition to `GOOS=js`. When there is shared stake in WASI, we expect gaps like
these to be easier to close.
See https://github.com/WebAssembly/wasi-testsuite
See https://github.com/golang/go/issues/58141
@@ -1084,7 +1054,7 @@ reason, wazero adds overhead to synthesize dot entries despite it being
unnecessary for most users.
See https://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html
See https://github.com/golang/go/blob/go1.20/src/os/dir_unix.go#L108-L110
See https://github.com/golang/go/blob/go1.24.0/src/os/dir_unix.go#L122-L124
See https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L1026-L1041
### Why don't we pre-populate an inode for the dot-dot ("..") entry?
@@ -1170,7 +1140,7 @@ See
* https://linux.die.net/man/3/getdents
* https://www.unix.com/man-page/osx/2/getdirentries/
* https://man.openbsd.org/OpenBSD-5.4/getdirentries.2
* https://github.com/golang/go/blob/go1.20/src/syscall/dirent.go#L60-L102
* https://github.com/golang/go/blob/go1.24.0/src/syscall/dirent.go#L57-L101
* https://go-review.googlesource.com/c/go/+/507915
## sys.Walltime and Nanotime
@@ -1205,10 +1175,7 @@ See https://gruss.cc/files/fantastictimers.pdf for an example attacks.
### Why does fake time increase on reading?
Both the fake nanotime and walltime increase by 1ms on reading. Particularly in
the case of nanotime, this prevents spinning. For example, when Go compiles
`time.Sleep` using `GOOS=js GOARCH=wasm`, nanotime is used in a loop. If that
never increases, the gouroutine is mistaken for being busy. This would be worse
if a compiler implement sleep using nanotime, yet doesn't check for spinning!
the case of nanotime, this prevents spinning.
### Why not `time.Clock`?
@@ -1224,7 +1191,7 @@ See https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md
WebAssembly time imports do not have the same concern. In fact even Go's
imports for clocks split walltime from nanotime readings.
See https://github.com/golang/go/blob/go1.20/misc/wasm/wasm_exec.js#L243-L255
See https://github.com/golang/go/blob/go1.24.0/lib/wasm/wasm_exec.js#L258-L268
Finally, Go's clock is not an interface. WebAssembly users who want determinism
or security need to be able to substitute an alternative clock implementation
@@ -1238,13 +1205,13 @@ value. For now, we return fixed values of 1us for realtime and 1ns for monotonic
often lower precision than monotonic clocks. In the future, this could be improved by having OS+arch specific assembly
to make syscalls.
For example, Go implements time.Now for linux-amd64 with this [assembly](https://github.com/golang/go/blob/go1.20/src/runtime/time_linux_amd64.s).
For example, Go implements time.Now for linux-amd64 with this [assembly](https://github.com/golang/go/blob/go1.24.0/src/runtime/time_linux_amd64.s).
Because retrieving resolution is not generally called often, unlike getting time, it could be appropriate to only
implement the fallback logic that does not use VDSO (executing syscalls in user mode). The syscall for clock_getres
is 229 and should be usable. https://pkg.go.dev/syscall#pkg-constants.
If implementing similar for Windows, [mingw](https://github.com/mirror/mingw-w64/blob/6a0e9165008f731bccadfc41a59719cf7c8efc02/mingw-w64-libraries/winpthreads/src/clock.c#L77
) is often a good source to find the Windows API calls that correspond
If implementing similar for Windows, [mingw](https://github.com/mirror/mingw-w64/blob/v12.0.0/mingw-w64-libraries/winpthreads/src/clock.c#L54)
is often a good source to find the Windows API calls that correspond
to a POSIX method.
Writing assembly would allow making syscalls without CGO, but comes with the cost that it will require implementations
@@ -1266,8 +1233,7 @@ pub fn main() !void {
```
Besides Zig, this is also the case with TinyGo (`-target=wasi`) and Rust
(`--target wasm32-wasi`). This isn't the case with Go (`GOOS=js GOARCH=wasm`),
though. In the latter case, wasm loops on `sys.Nanotime`.
(`--target wasm32-wasi`).
We decided to expose `sys.Nanosleep` to allow overriding the implementation
used in the common case, even if it isn't used by Go, because this gives an
@@ -1514,7 +1480,7 @@ This is due to the same reason for the limitation on the number of functions abo
While the the spec does not clarify a limitation of function stack values, wazero limits this to 2^27 = 134,217,728.
The reason is that we internally represent all the values as 64-bit integers regardless of its types (including f32, f64), and 2^27 values means
1 GiB = (2^30). 1 GiB is the reasonable for most applications [as we see a Goroutine has 250 MB as a limit on the stack for 32-bit arch](https://github.com/golang/go/blob/go1.20/src/runtime/proc.go#L152-L159), considering that WebAssembly is (currently) 32-bit environment.
1 GiB = (2^30). 1 GiB is the reasonable for most applications [as we see a Goroutine has 250 MB as a limit on the stack for 32-bit arch](https://github.com/golang/go/blob/go1.24.0/src/runtime/proc.go#L154-L161), considering that WebAssembly is (currently) 32-bit environment.
All the functions are statically analyzed at module instantiation phase, and if a function can potentially reach this limit, an error is returned.
@@ -1547,8 +1513,8 @@ since it tries to interrupt the execution of Goroutine at any point of function,
Fortunately, our runtime-generated machine codes do not need to take the async preemption into account.
All the assembly codes are entered via the trampoline implemented as Go Assembler Function (e.g. [arch_amd64.s](./arch_amd64.s)),
and as of Go 1.20, these assembler functions are considered as _unsafe_ for async preemption:
- https://github.com/golang/go/blob/go1.20rc1/src/runtime/preempt.go#L406-L407
- https://github.com/golang/go/blob/9f0234214473dfb785a5ad84a8fc62a6a395cbc3/src/runtime/traceback.go#L227
- https://github.com/golang/go/blob/go1.24.0/src/runtime/preempt.go#L407-L408
- https://github.com/golang/go/blob/go1.24.0/src/runtime/traceback.go#L350
From the Go runtime point of view, the execution of runtime-generated machine codes is considered as a part of
that trampoline function. Therefore, runtime-generated machine code is also correctly considered unsafe for async preemption.
@@ -1558,15 +1524,13 @@ that trampoline function. Therefore, runtime-generated machine code is also corr
Since [wazero v1.0.0-pre.9](https://github.com/tetratelabs/wazero/releases/tag/v1.0.0-pre.9), the runtime
supports integration with Go contexts to interrupt execution after a timeout, or in response to explicit cancellation.
This support is internally implemented as a special opcode `builtinFunctionCheckExitCode` that triggers the execution of
a Go function (`ModuleInstance.FailIfClosed`) that atomically checks a sentinel value at strategic points in the code
(e.g. [within loops][checkexitcode_loop]).
a Go function (`ModuleInstance.FailIfClosed`) that atomically checks a sentinel value at strategic points in the code.
[It _is indeed_ possible to check the sentinel value directly, without leaving the native world][native_check], thus sparing some cycles;
however, because native code never preempts (see section above), this may lead to a state where the other goroutines
never get the chance to run, and thus never get the chance to set the sentinel value; effectively preventing
cancellation from taking place.
[checkexitcode_loop]: https://github.com/tetratelabs/wazero/blob/86444c67a37dbf9e693ae5b365901f64968d9025/internal/wazeroir/compiler.go#L467-L476
[native_check]: https://github.com/tetratelabs/wazero/issues/1409
## Golang patterns
@@ -1608,6 +1572,6 @@ In lieu of formal documentation, we infer this pattern works from other sources
* `sync.WaitGroup` by definition must support calling `Add` from other goroutines. Internally, it uses atomics.
* rsc in golang/go#5045 writes "atomics guarantee sequential consistency among the atomic variables".
See https://github.com/golang/go/blob/go1.20/src/sync/waitgroup.go#L64
See https://github.com/golang/go/blob/go1.24.0/src/sync/waitgroup.go#L76
See https://github.com/golang/go/issues/5045#issuecomment-252730563
See https://www.youtube.com/watch?v=VmrEG-3bWyM
+35 -32
View File
@@ -1,6 +1,6 @@
# wazero: the zero dependency WebAssembly runtime for Go developers
[![WebAssembly Core Specification Test](https://github.com/tetratelabs/wazero/actions/workflows/spectest.yaml/badge.svg)](https://github.com/tetratelabs/wazero/actions/workflows/spectest.yaml) [![Go Reference](https://pkg.go.dev/badge/github.com/tetratelabs/wazero.svg)](https://pkg.go.dev/github.com/tetratelabs/wazero) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Go Reference](https://pkg.go.dev/badge/github.com/tetratelabs/wazero.svg)](https://pkg.go.dev/github.com/tetratelabs/wazero) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
WebAssembly is a way to safely run code compiled in other languages. Runtimes
execute WebAssembly Modules (Wasm), which are most often binaries with a `.wasm`
@@ -43,7 +43,7 @@ magnitude (10x) or more. This is done without host-specific dependencies.
### Conformance
Both runtimes pass WebAssembly Core [1.0][7] and [2.0][14] specification tests
Both runtimes pass WebAssembly Core [1.0][3] and [2.0][4] specification tests
on supported platforms:
| Runtime | Usage | amd64 | arm64 | others |
@@ -58,7 +58,7 @@ wazero into their Go applications.
### wazero
wazero's [1.0 release][15] happened in March 2023, and is [in use][16] by many
wazero's [1.0 release][8] happened in March 2023, and is [in use][9] by many
projects and production sites.
We offer an API stability promise with semantic versioning. In other words, we
@@ -72,23 +72,17 @@ You can get the latest version of wazero like this.
go get github.com/tetratelabs/wazero@latest
```
Please give us a [star][17] if you end up using wazero!
Please give us a [star][10] if you end up using wazero!
### Go
wazero has no dependencies except Go, so the only source of conflict in your
project's use of wazero is the Go version.
wazero has no dependencies except Go and [`x/sys`][12], so the only source of
conflict in your project's use of wazero is the Go version.
wazero follows the same version policy as Go's [Release Policy][10]: two
wazero follows the same version policy as Go's [Release Policy][5]: two
versions. wazero will ensure these versions work and bugs are valid if there's
an issue with a current Go version.
Additionally, wazero intentionally delays usage of language or standard library
features one additional version. For example, when Go 1.29 is released, wazero
can use language features or standard libraries added in 1.27. This is a
convenience for embedders who have a slower version policy than Go. However,
only supported Go versions may be used to raise support issues.
### Platform
wazero has two runtime modes: Interpreter and Compiler. The only supported operating
@@ -96,37 +90,46 @@ systems are ones we test, but that doesn't necessarily mean other operating
system versions won't work.
We currently test Linux (Ubuntu and scratch), MacOS and Windows as packaged by
[GitHub Actions][11], as well compilation of 32-bit Linux and 64-bit FreeBSD.
[GitHub Actions][6], as well as nested VMs running on Linux for FreeBSD, NetBSD,
OpenBSD, DragonFly BSD, illumos and Solaris.
We also test cross compilation for many `GOOS` and `GOARCH` combinations.
* Interpreter
* Linux is tested on amd64 (native) as well arm64 and riscv64 via emulation.
* MacOS and Windows are only tested on amd64.
* Linux is tested on amd64, arm64 and riscv64.
* Windows, FreeBSD, NetBSD, OpenBSD, DragonFly BSD, illumos and Solaris are
tested only on amd64.
* macOS is tested only on arm64.
* Compiler
* Linux is tested on amd64 (native) as well arm64 via emulation.
* MacOS and Windows are only tested on amd64.
* Linux is tested on amd64 and arm64.
* Windows, FreeBSD, NetBSD, DragonFly BSD, illumos and Solaris are
tested only on amd64.
* macOS is tested only on arm64.
wazero has no dependencies and doesn't require CGO. This means it can also be
embedded in an application that doesn't use an operating system. This is a main
differentiator between wazero and alternatives.
We verify zero dependencies by running tests in Docker's [scratch image][12].
We verify zero dependencies by running tests in Docker's [scratch image][7].
This approach ensures compatibility with any parent image.
### macOS code-signing entitlements
If you're developing for macOS and need to code-sign your application,
please read issue [#2393][11].
-----
wazero is a registered trademark of Tetrate.io, Inc. in the United States and/or other countries
[1]: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/
[2]: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/
[4]: https://github.com/WebAssembly/meetings/blob/main/process/subgroups.md
[5]: https://github.com/WebAssembly/WASI
[6]: https://pkg.go.dev/golang.org/x/sys/unix
[7]: https://github.com/WebAssembly/spec/tree/wg-1.0/test/core
[9]: https://github.com/tetratelabs/wazero/issues/506
[10]: https://go.dev/doc/devel/release
[11]: https://github.com/actions/virtual-environments
[12]: https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch
[13]: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
[14]: https://github.com/WebAssembly/spec/tree/d39195773112a22b245ffbe864bab6d1182ccb06/test/core
[15]: https://tetrate.io/blog/introducing-wazero-from-tetrate/
[16]: https://wazero.io/community/users/
[17]: https://github.com/tetratelabs/wazero/stargazers
[3]: https://github.com/WebAssembly/spec/tree/wg-1.0/test/core
[4]: https://github.com/WebAssembly/spec/tree/d39195773112a22b245ffbe864bab6d1182ccb06/test/core
[5]: https://go.dev/doc/devel/release
[6]: https://github.com/actions/virtual-environments
[7]: https://docs.docker.com/develop/develop-images/baseimages/#create-a-simple-parent-image-using-scratch
[8]: https://tetrate.io/blog/introducing-wazero-from-tetrate/
[9]: https://wazero.io/community/users/
[10]: https://github.com/wazero/wazero/stargazers
[11]: https://github.com/wazero/wazero/issues/2393
[12]: https://pkg.go.dev/golang.org/x/sys
+14
View File
@@ -209,6 +209,20 @@ func featureName(f CoreFeatures) string {
case CoreFeatureSIMD:
// match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md
return "simd"
// The cases below cover features defined in the experimental package
// (experimental.CoreFeaturesThreads, CoreFeaturesTailCall,
// experimental.CoreFeaturesExtendedConst, experimental.CoreFeaturesExceptionHandling).
// They cannot be imported here (circular dependency), so we match by value.
case CoreFeatureSIMD << 1: // experimental.CoreFeaturesThreads
return "threads"
case CoreFeatureSIMD << 2: // experimental.CoreFeaturesTailCall
return "tail-call"
case CoreFeatureSIMD << 3: // experimental.CoreFeaturesExtendedConst
return "extended-const"
case CoreFeatureSIMD << 4: // experimental.CoreFeaturesExceptionHandling
return "exception-handling"
case CoreFeatureSIMD << 5: // experimental.CoreFeaturesTypedFunctionReferences
return "typed-function-references"
}
return ""
}
+7 -3
View File
@@ -151,9 +151,13 @@ type Module interface {
// ExportedFunction returns a function exported from this module or nil if it wasn't.
//
// Note: The default wazero.ModuleConfig attempts to invoke `_start`, which
// in rare cases can close the module. When in doubt, check IsClosed prior
// to invoking a function export after instantiation.
// # Notes
// - The default wazero.ModuleConfig attempts to invoke `_start`, which
// in rare cases can close the module. When in doubt, check IsClosed prior
// to invoking a function export after instantiation.
// - The semantics of host functions assumes the existence of an "importing module" because, for example, the host function needs access to
// the memory of the importing module. Therefore, direct use of ExportedFunction is forbidden for host modules.
// Practically speaking, it is usually meaningless to directly call a host function from Go code as it is already somewhere in Go code.
ExportedFunction(name string) Function
// ExportedFunctionDefinitions returns all the exported function
+18 -3
View File
@@ -179,6 +179,9 @@ type HostFunctionBuilder interface {
// are deferred until Compile.
// - Functions are indexed in order of calls to NewFunctionBuilder as
// insertion ordering is needed by ABI such as Emscripten (invoke_*).
// - The semantics of host functions assumes the existence of an "importing module" because, for example, the host function needs access to
// the memory of the importing module. Therefore, direct use of ExportedFunction is forbidden for host modules.
// Practically speaking, it is usually meaningless to directly call a host function from Go code as it is already somewhere in Go code.
type HostModuleBuilder interface {
// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
@@ -240,13 +243,13 @@ type hostFunctionBuilder struct {
// WithGoFunction implements HostFunctionBuilder.WithGoFunction
func (h *hostFunctionBuilder) WithGoFunction(fn api.GoFunction, params, results []api.ValueType) HostFunctionBuilder {
h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
h.fn = &wasm.HostFunc{ParamTypes: wasm.FromApiValueType(params), ResultTypes: wasm.FromApiValueType(results), Code: wasm.Code{GoFunc: fn}}
return h
}
// WithGoModuleFunction implements HostFunctionBuilder.WithGoModuleFunction
func (h *hostFunctionBuilder) WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder {
h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
h.fn = &wasm.HostFunc{ParamTypes: wasm.FromApiValueType(params), ResultTypes: wasm.FromApiValueType(results), Code: wasm.Code{GoFunc: fn}}
return h
}
@@ -341,12 +344,24 @@ func (b *hostModuleBuilder) Compile(ctx context.Context) (CompiledModule, error)
return c, nil
}
// hostModuleInstance is a wrapper around api.Module that prevents calling ExportedFunction.
type hostModuleInstance struct{ api.Module }
// ExportedFunction implements api.Module ExportedFunction.
func (h hostModuleInstance) ExportedFunction(name string) api.Function {
panic("calling ExportedFunction is forbidden on host modules. See the note on ExportedFunction interface")
}
// Instantiate implements HostModuleBuilder.Instantiate
func (b *hostModuleBuilder) Instantiate(ctx context.Context) (api.Module, error) {
if compiled, err := b.Compile(ctx); err != nil {
return nil, err
} else {
compiled.(*compiledModule).closeWithModule = true
return b.r.InstantiateModule(ctx, compiled, NewModuleConfig())
m, err := b.r.InstantiateModule(ctx, compiled, NewModuleConfig())
if err != nil {
return nil, err
}
return hostModuleInstance{m}, nil
}
}
+7
View File
@@ -24,6 +24,13 @@ import (
// All implementations are in wazero.
// - Instances of this can be reused across multiple runtimes, if configured
// via RuntimeConfig.
// - The cache check happens before the compilation, so if multiple Goroutines are
// trying to compile the same module simultaneously, it is possible that they
// all compile the module. The design here is that the lock isn't held for the action "Compile"
// but only for checking and saving the compiled result. Therefore, we strongly recommend that the embedder
// does the centralized compilation in a single Goroutines (or multiple Goroutines per Wasm binary) to generate cache rather than
// trying to Compile in parallel for a single module. In other words, we always recommend to produce CompiledModule
// share it across multiple Goroutines to avoid trying to compile the same module simultaneously.
type CompilationCache interface{ api.Closer }
// NewCompilationCache returns a new CompilationCache to be passed to RuntimeConfig.
+34 -11
View File
@@ -12,8 +12,6 @@ import (
"github.com/tetratelabs/wazero/api"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/engine/interpreter"
"github.com/tetratelabs/wazero/internal/engine/wazevo"
"github.com/tetratelabs/wazero/internal/filecache"
"github.com/tetratelabs/wazero/internal/internalapi"
"github.com/tetratelabs/wazero/internal/platform"
@@ -148,7 +146,7 @@ type RuntimeConfig interface {
// customSections := c.CustomSections()
WithCustomSections(bool) RuntimeConfig
// WithCloseOnContextDone ensures the executions of functions to be closed under one of the following circumstances:
// WithCloseOnContextDone ensures the executions of functions to be terminated under one of the following circumstances:
//
// - context.Context passed to the Call method of api.Function is canceled during execution. (i.e. ctx by context.WithCancel)
// - context.Context passed to the Call method of api.Function reaches timeout during execution. (i.e. ctx by context.WithTimeout or context.WithDeadline)
@@ -159,6 +157,8 @@ type RuntimeConfig interface {
// entire underlying OS thread which runs the api.Function call. See "Why it's safe to execute runtime-generated
// machine codes against async Goroutine preemption" section in RATIONALE.md for detail.
//
// Upon the termination of the function executions, api.Module is closed.
//
// Note that this comes with a bit of extra cost when enabled. The reason is that internally this forces
// interpreter and compiler runtimes to insert the periodical checks on the conditions above. For that reason,
// this is disabled by default.
@@ -173,7 +173,9 @@ type RuntimeConfig interface {
// NewRuntimeConfig returns a RuntimeConfig using the compiler if it is supported in this environment,
// or the interpreter otherwise.
func NewRuntimeConfig() RuntimeConfig {
return newRuntimeConfig()
ret := engineLessConfig.clone()
ret.engineKind = engineKindAuto
return ret
}
type newEngine func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine
@@ -201,7 +203,8 @@ var engineLessConfig = &runtimeConfig{
type engineKind int
const (
engineKindCompiler engineKind = iota
engineKindAuto engineKind = iota - 1
engineKindCompiler
engineKindInterpreter
engineKindCount
)
@@ -217,13 +220,21 @@ const (
// part. wazero automatically performs ahead-of-time compilation as needed when
// Runtime.CompileModule is invoked.
//
// Warning: This panics at runtime if the runtime.GOOS or runtime.GOARCH does not
// support Compiler. Use NewRuntimeConfig to safely detect and fallback to
// NewRuntimeConfigInterpreter if needed.
// # Warning
//
// - This panics at runtime if the runtime.GOOS or runtime.GOARCH does not
// support compiler. Use NewRuntimeConfig to safely detect and fallback to
// NewRuntimeConfigInterpreter if needed.
//
// - If you are using wazero in buildmode=c-archive or c-shared, make sure that you set up the alternate signal stack
// by using, e.g. `sigaltstack` combined with `SA_ONSTACK` flag on `sigaction` on Linux,
// before calling any api.Function. This is because the Go runtime does not set up the alternate signal stack
// for c-archive or c-shared modes, and wazero uses the different stack than the calling Goroutine.
// Hence, the signal handler might get invoked on the wazero's stack, which may cause a stack overflow.
// https://github.com/tetratelabs/wazero/blob/2092c0a879f30d49d7b37f333f4547574b8afe0d/internal/integration_test/fuzz/fuzz/tests/sigstack.rs#L19-L36
func NewRuntimeConfigCompiler() RuntimeConfig {
ret := engineLessConfig.clone()
ret.engineKind = engineKindCompiler
ret.newEngine = wazevo.NewEngine
return ret
}
@@ -231,7 +242,6 @@ func NewRuntimeConfigCompiler() RuntimeConfig {
func NewRuntimeConfigInterpreter() RuntimeConfig {
ret := engineLessConfig.clone()
ret.engineKind = engineKindInterpreter
ret.newEngine = interpreter.NewEngine
return ret
}
@@ -484,7 +494,20 @@ type ModuleConfig interface {
WithFSConfig(FSConfig) ModuleConfig
// WithName configures the module name. Defaults to what was decoded from
// the name section. Empty string ("") clears any name.
// the name section. Duplicate names are not allowed in a single Runtime.
//
// Calling this with the empty string "" makes the module anonymous.
// That is useful when you want to instantiate the same CompiledModule multiple times like below:
//
// for i := 0; i < N; i++ {
// // Instantiate a new Wasm module from the already compiled `compiledWasm` anonymously without a name.
// instance, err := r.InstantiateModule(ctx, compiledWasm, wazero.NewModuleConfig().WithName(""))
// // ....
// }
//
// See the `concurrent-instantiation` example for a complete usage.
//
// Non-empty named modules are available for other modules to import by name.
WithName(string) ModuleConfig
// WithStartFunctions configures the functions to call after the module is
-14
View File
@@ -1,14 +0,0 @@
// Note: The build constraints here are about the compiler, which is more
// narrow than the architectures supported by the assembler.
//
// Constraints here must match platform.CompilerSupported.
//
// Meanwhile, users who know their runtime.GOOS can operate with the compiler
// may choose to use NewRuntimeConfigCompiler explicitly.
//go:build (amd64 || arm64) && (darwin || linux || freebsd || windows)
package wazero
func newRuntimeConfig() RuntimeConfig {
return NewRuntimeConfigCompiler()
}
-8
View File
@@ -1,8 +0,0 @@
// This is the opposite constraint of config_supported.go
//go:build !(amd64 || arm64) || !(darwin || linux || freebsd || windows)
package wazero
func newRuntimeConfig() RuntimeConfig {
return NewRuntimeConfigInterpreter()
}
@@ -21,13 +21,6 @@ type Snapshotter interface {
Snapshot() Snapshot
}
// EnableSnapshotterKey is a context key to indicate that snapshotting should be enabled.
// The context.Context passed to a exported function invocation should have this key set
// to a non-nil value, and host functions will be able to retrieve it using SnapshotterKey.
//
// Deprecated: use WithSnapshotter to enable snapshots.
type EnableSnapshotterKey = expctxkeys.EnableSnapshotterKey
// WithSnapshotter enables snapshots.
// Passing the returned context to a exported function invocation enables snapshots,
// and allows host functions to retrieve the Snapshotter using GetSnapshotter.
@@ -35,12 +28,6 @@ func WithSnapshotter(ctx context.Context) context.Context {
return context.WithValue(ctx, expctxkeys.EnableSnapshotterKey{}, struct{}{})
}
// SnapshotterKey is a context key to access a Snapshotter from a host function.
// It is only present if EnableSnapshotter was set in the function invocation context.
//
// Deprecated: use GetSnapshotter to get the snapshotter.
type SnapshotterKey = expctxkeys.SnapshotterKey
// GetSnapshotter gets the Snapshotter from a host function.
// It is only present if WithSnapshotter was called with the function invocation context.
func GetSnapshotter(ctx context.Context) Snapshotter {
@@ -0,0 +1,19 @@
package experimental
import (
"context"
"github.com/tetratelabs/wazero/internal/expctxkeys"
)
// WithCompilationWorkers sets the desired number of compilation workers.
func WithCompilationWorkers(ctx context.Context, workers int) context.Context {
return context.WithValue(ctx, expctxkeys.CompilationWorkers{}, workers)
}
// GetCompilationWorkers returns the desired number of compilation workers.
// The minimum value returned is 1.
func GetCompilationWorkers(ctx context.Context) int {
workers, _ := ctx.Value(expctxkeys.CompilationWorkers{}).(int)
return max(workers, 1)
}
+24
View File
@@ -13,3 +13,27 @@ import "github.com/tetratelabs/wazero/api"
// binaries will use a theroetical maximum like 4GB, so if using such a binary on a system
// without mmap, consider editing the binary to reduce the max size setting of memory.
const CoreFeaturesThreads = api.CoreFeatureSIMD << 1
// CoreFeaturesTailCall enables tail call instructions ("tail-call").
const CoreFeaturesTailCall = api.CoreFeatureSIMD << 2
// CoreFeaturesExtendedConst enables extended constant expressions.
//
// # Notes
//
// - Enables i32.add/sub/mul and i64.add/sub/mul in constant expressions.
// - Enables references to any previous global index in constant expressions,
// instead of just imported globals.
//
// See https://github.com/WebAssembly/extended-const for further details.
const CoreFeaturesExtendedConst = api.CoreFeatureSIMD << 3
// CoreFeaturesExceptionHandling enables exception handling instructions.
//
// See https://github.com/WebAssembly/exception-handling for further details.
const CoreFeaturesExceptionHandling = api.CoreFeatureSIMD << 4
// CoreFeaturesTypedFunctionReferences enables typed function references.
//
// See https://github.com/WebAssembly/function-references for further details.
const CoreFeaturesTypedFunctionReferences = api.CoreFeatureSIMD << 5
@@ -0,0 +1,19 @@
package experimental
import (
"context"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/expctxkeys"
)
// ImportResolver is an experimental func type that, if set,
// will be used as the first step in resolving imports.
// See issue 2294.
// If the import name is not found, it should return nil.
type ImportResolver func(name string) api.Module
// WithImportResolver returns a new context with the given ImportResolver.
func WithImportResolver(ctx context.Context, resolver ImportResolver) context.Context {
return context.WithValue(ctx, expctxkeys.ImportResolverKey{}, resolver)
}
@@ -24,12 +24,6 @@ type StackIterator interface {
ProgramCounter() ProgramCounter
}
// FunctionListenerFactoryKey is a context.Context Value key.
// Its associated value should be a FunctionListenerFactory.
//
// Deprecated: use WithFunctionListenerFactory to enable snapshots.
type FunctionListenerFactoryKey = expctxkeys.FunctionListenerFactoryKey
// WithFunctionListenerFactory registers a FunctionListenerFactory
// with the context.
func WithFunctionListenerFactory(ctx context.Context, factory FunctionListenerFactory) context.Context {
+3 -1
View File
@@ -35,13 +35,15 @@ type LinearMemory interface {
// Notes:
// - To back a shared memory, Reallocate can't change the address of the
// backing []byte (only its length/capacity may change).
// - Reallocate may return nil if fails to grow the LinearMemory. This
// condition may or may not be handled gracefully by the Wasm module.
Reallocate(size uint64) []byte
// Free the backing memory buffer.
Free()
}
// WithMemoryAllocator registers the given MemoryAllocator into the given
// context.Context.
// context.Context. The context must be passed when initializing a module.
func WithMemoryAllocator(ctx context.Context, allocator MemoryAllocator) context.Context {
if allocator != nil {
return context.WithValue(ctx, expctxkeys.MemoryAllocatorKey{}, allocator)
+42 -3
View File
@@ -3,7 +3,7 @@ package sys
import "github.com/tetratelabs/wazero/sys"
// File is a writeable fs.File bridge backed by syscall functions needed for ABI
// including WASI and runtime.GOOS=js.
// including WASI.
//
// Implementations should embed UnimplementedFile for forward compatibility. Any
// unsupported method or parameter should return ENOSYS.
@@ -14,8 +14,7 @@ import "github.com/tetratelabs/wazero/sys"
// on success.
//
// Restricting to Errno matches current WebAssembly host functions,
// which are constrained to well-known error codes. For example, `GOOS=js` maps
// hard coded values and panics otherwise. More commonly, WASI maps syscall
// which are constrained to well-known error codes. For example, WASI maps syscall
// errors to u32 numeric values.
//
// # Notes
@@ -315,3 +314,43 @@ type File interface {
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html
Close() Errno
}
// PollableFile is a File that additionally supports polling for readiness
// and non-blocking mode. This is separated from File to avoid breaking
// downstream implementations that do not need polling.
//
// Implementations should embed UnimplementedFile for forward compatibility
// of the base File methods, and add Poll, IsNonblock, and SetNonblock.
//
// # Notes
//
// - This is the public equivalent of the internal fsapi.File interface.
// - See Pollable for a standalone poll interface usable with io.Reader
// or fs.File implementations that are not full File implementations.
type PollableFile interface {
File
Pollable
// IsNonblock returns true if the file was opened with O_NONBLOCK, or
// SetNonblock was successfully enabled on this file.
//
// # Notes
//
// - This might not match the underlying state of the file descriptor if
// the file was not opened via OpenFile.
IsNonblock() bool
// SetNonblock toggles the non-blocking mode (O_NONBLOCK) of this file.
//
// # Errors
//
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.SetNonblock and `fcntl` with O_NONBLOCK in
// POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html
SetNonblock(enable bool) Errno
}
+2 -3
View File
@@ -7,7 +7,7 @@ import (
)
// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
// including WASI and runtime.GOOS=js.
// including WASI.
//
// Implementations should embed UnimplementedFS for forward compatibility. Any
// unsupported method or parameter should return ENO
@@ -18,8 +18,7 @@ import (
// on success.
//
// Restricting to Errno matches current WebAssembly host functions,
// which are constrained to well-known error codes. For example, `GOOS=js` maps
// hard coded values and panics otherwise. More commonly, WASI maps syscall
// which are constrained to well-known error codes. For example, WASI maps syscall
// errors to u32 numeric values.
//
// # Notes
+48
View File
@@ -0,0 +1,48 @@
package sys
// Pflag are bit flags used for polling. Values, including zero, should not
// be interpreted numerically. Instead, use by constants prefixed with 'POLL'.
//
// # Notes
//
// - This is like `pollfd.events` flags for `poll` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/poll.h.html
type Pflag uint32
// Only define bitflags we support and are needed by `poll_oneoff` in wasip1
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#eventrwflags
const (
// POLLIN is a read event.
POLLIN Pflag = 1 << iota
// POLLOUT is a write event.
POLLOUT
)
// Pollable is implemented by custom readers that support polling for
// readiness. If a custom io.Reader passed to WithStdin implements this
// interface, poll_oneoff will use it for asynchronous I/O instead of
// returning "always ready".
//
// # Parameters
//
// The `flag` parameter determines which event to await, such as POLLIN,
// POLLOUT, or a combination like `POLLIN|POLLOUT`.
//
// The `timeoutMillis` parameter is how long to block for an event, or
// interrupted, in milliseconds. There are two special values:
// - zero returns immediately
// - any negative value blocks any amount of time
//
// # Results
//
// `ready` means there was data ready to read or written. False can mean no
// event was ready or `errno` is not zero.
//
// A zero `errno` is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - ENOTSUP: the implementation does not support the flag combination.
// - EINTR: the call was interrupted prior to an event.
type Pollable interface {
Poll(flag Pflag, timeoutMillis int32) (ready bool, errno Errno)
}
@@ -1,4 +1,4 @@
//go:build !plan9 && !aix
//go:build !(plan9 || aix)
package sys
@@ -1,57 +1,32 @@
package sys
import "syscall"
// These are errors not defined in the syscall package. They are prefixed with
// underscore to avoid exporting them.
//
// See https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
const (
// _ERROR_INVALID_HANDLE is a Windows error returned by syscall.Write
// instead of syscall.EBADF
_ERROR_INVALID_HANDLE = syscall.Errno(6)
// _ERROR_INVALID_NAME is a Windows error returned by open when a file
// path has a trailing slash
_ERROR_INVALID_NAME = syscall.Errno(0x7B)
// _ERROR_NEGATIVE_SEEK is a Windows error returned by os.Truncate
// instead of syscall.EINVAL
_ERROR_NEGATIVE_SEEK = syscall.Errno(0x83)
// _ERROR_DIRECTORY is a Windows error returned by syscall.Rmdir
// instead of syscall.ENOTDIR
_ERROR_DIRECTORY = syscall.Errno(0x10B)
// _ERROR_INVALID_SOCKET is a Windows error returned by winsock_select
// when a given handle is not a socket.
_ERROR_INVALID_SOCKET = syscall.Errno(0x2736)
)
import "golang.org/x/sys/windows"
func errorToErrno(err error) Errno {
switch err := err.(type) {
case Errno:
return err
case syscall.Errno:
case windows.Errno:
// Note: In windows, _ERROR_PATH_NOT_FOUND(0x3) maps to syscall.ENOTDIR
switch err {
case syscall.ERROR_ALREADY_EXISTS:
case windows.ERROR_ALREADY_EXISTS, windows.ERROR_FILE_EXISTS:
return EEXIST
case _ERROR_DIRECTORY:
case windows.ERROR_DIRECTORY:
// ERROR_DIRECTORY is returned by syscall.Rmdir.
return ENOTDIR
case syscall.ERROR_DIR_NOT_EMPTY:
case windows.ERROR_DIR_NOT_EMPTY:
return ENOTEMPTY
case syscall.ERROR_FILE_EXISTS:
return EEXIST
case _ERROR_INVALID_HANDLE, _ERROR_INVALID_SOCKET:
return EBADF
case syscall.ERROR_ACCESS_DENIED:
case windows.ERROR_INVALID_HANDLE, windows.WSAENOTSOCK, windows.ERROR_ACCESS_DENIED:
// WSAENOTSOCK is returned by winsock_select when a given handle is not a socket.
// POSIX read and write functions expect EBADF, not EACCES when not
// open for reading or writing.
return EBADF
case syscall.ERROR_PRIVILEGE_NOT_HELD:
case windows.ERROR_PRIVILEGE_NOT_HELD:
return EPERM
case _ERROR_NEGATIVE_SEEK, _ERROR_INVALID_NAME:
case windows.ERROR_NEGATIVE_SEEK, windows.ERROR_NOT_A_REPARSE_POINT, windows.ERROR_INVALID_NAME:
// ERROR_NEGATIVE_SEEK is returned by os.Truncate.
// ERROR_NOT_A_REPARSE_POINT is returned by os.Readlink.
// ERROR_INVALID_NAME is returned by open when a file path has a trailing slash.
return EINVAL
}
errno, _ := syscallToErrno(err)
@@ -0,0 +1,16 @@
package sysfs
import (
"github.com/tetratelabs/wazero"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
)
// FSConfig extends wazero.FSConfig, allowing access to the experimental
// sys.FS until it is moved to the "sys" package.
type FSConfig interface {
// WithSysFSMount assigns a sys.FS file system for any paths beginning at
// `guestPath`.
//
// This is an alternative to WithFSMount, allowing more features.
WithSysFSMount(fs experimentalsys.FS, guestPath string) wazero.FSConfig
}
@@ -0,0 +1,36 @@
// Package sysfs includes a low-level filesystem interface and utilities needed
// for WebAssembly host functions (ABI) such as WASI.
//
// The name sysfs was chosen because wazero's public API has a "sys" package,
// which was named after https://github.com/golang/sys.
//
// This tracked in https://github.com/tetratelabs/wazero/issues/1013
package sysfs
import (
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
)
// AdaptFS adapts the input to sys.FS. Use DirFS instead of adapting an
// os.DirFS as it handles interop issues such as windows support.
//
// Note: This performs no flag verification on OpenFile. sys.FS cannot read
// flags as there is no parameter to pass them through with. Moreover, sys.FS
// documentation does not require the file to be present. In summary, we can't
// enforce flag behavior.
type AdaptFS = sysfs.AdaptFS
// DirFS is like os.DirFS except it returns sys.FS, which has more features.
func DirFS(dir string) experimentalsys.FS {
return sysfs.DirFS(dir)
}
// ReadFS is used to mask an existing sys.FS for reads. Notably, this allows
// the CLI to do read-only mounts of directories the host user can write, but
// doesn't want the guest wasm to. For example, Python libraries shouldn't be
// written to at runtime by the python wasm file.
//
// Note: This implements read-only by returning sys.EROFS or sys.EBADF,
// depending on the operation that require write access.
type ReadFS = sysfs.ReadFS
-2
View File
@@ -30,8 +30,6 @@ import (
// current directory prior to requesting files.
//
// More notes on `guestPath`
// - Go compiled with runtime.GOOS=js do not pay attention to this value.
// It only works with root mounts ("").
// - Working directories are typically tracked in wasm, though possible some
// relative paths are requested. For example, TinyGo may attempt to resolve
// a path "../.." in unit tests.
@@ -41,7 +41,7 @@ import (
// See argsSizesGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
var argsGet = newHostFunc(wasip1.ArgsGetName, argsGetFn, []api.ValueType{i32, i32}, "argv", "argv_buf")
var argsGet = newHostFunc(wasip1.ArgsGetName, argsGetFn, []wasm.ValueType{i32, i32}, "argv", "argv_buf")
func argsGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -78,7 +78,7 @@ func argsGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
// See argsGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#args_sizes_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
var argsSizesGet = newHostFunc(wasip1.ArgsSizesGetName, argsSizesGetFn, []api.ValueType{i32, i32}, "result.argc", "result.argv_len")
var argsSizesGet = newHostFunc(wasip1.ArgsSizesGetName, argsSizesGetFn, []wasm.ValueType{i32, i32}, "result.argc", "result.argv_len")
func argsSizesGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -37,7 +37,7 @@ import (
// Note: This is similar to `clock_getres` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp
// See https://linux.die.net/man/3/clock_getres
var clockResGet = newHostFunc(wasip1.ClockResGetName, clockResGetFn, []api.ValueType{i32, i32}, "id", "result.resolution")
var clockResGet = newHostFunc(wasip1.ClockResGetName, clockResGetFn, []wasm.ValueType{i32, i32}, "id", "result.resolution")
func clockResGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -90,7 +90,7 @@ func clockResGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
// Note: This is similar to `clock_gettime` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
// See https://linux.die.net/man/3/clock_gettime
var clockTimeGet = newHostFunc(wasip1.ClockTimeGetName, clockTimeGetFn, []api.ValueType{i32, i64, i32}, "id", "precision", "result.timestamp")
var clockTimeGet = newHostFunc(wasip1.ClockTimeGetName, clockTimeGetFn, []wasm.ValueType{i32, i64, i32}, "id", "precision", "result.timestamp")
func clockTimeGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -41,7 +41,7 @@ import (
// See environSizesGet
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_get
// See https://en.wikipedia.org/wiki/Null-terminated_string
var environGet = newHostFunc(wasip1.EnvironGetName, environGetFn, []api.ValueType{i32, i32}, "environ", "environ_buf")
var environGet = newHostFunc(wasip1.EnvironGetName, environGetFn, []wasm.ValueType{i32, i32}, "environ", "environ_buf")
func environGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -81,7 +81,7 @@ func environGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
// See environGet
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#environ_sizes_get
// and https://en.wikipedia.org/wiki/Null-terminated_string
var environSizesGet = newHostFunc(wasip1.EnvironSizesGetName, environSizesGetFn, []api.ValueType{i32, i32}, "result.environc", "result.environv_len")
var environSizesGet = newHostFunc(wasip1.EnvironSizesGetName, environSizesGetFn, []wasm.ValueType{i32, i32}, "result.environc", "result.environv_len")
func environSizesGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -115,7 +115,7 @@ func fdAllocateFn(_ context.Context, mod api.Module, params []uint64) experiment
// Note: This is similar to `close` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_close
// and https://linux.die.net/man/3/close
var fdClose = newHostFunc(wasip1.FdCloseName, fdCloseFn, []api.ValueType{i32}, "fd")
var fdClose = newHostFunc(wasip1.FdCloseName, fdCloseFn, []wasm.ValueType{i32}, "fd")
func fdCloseFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -128,7 +128,7 @@ func fdCloseFn(_ context.Context, mod api.Module, params []uint64) experimentals
// the data of a file to disk.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_datasyncfd-fd---errno
var fdDatasync = newHostFunc(wasip1.FdDatasyncName, fdDatasyncFn, []api.ValueType{i32}, "fd")
var fdDatasync = newHostFunc(wasip1.FdDatasyncName, fdDatasyncFn, []wasm.ValueType{i32}, "fd")
func fdDatasyncFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -179,7 +179,7 @@ func fdDatasyncFn(_ context.Context, mod api.Module, params []uint64) experiment
// well as additional fields.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fdstat
// and https://linux.die.net/man/3/fsync
var fdFdstatGet = newHostFunc(wasip1.FdFdstatGetName, fdFdstatGetFn, []api.ValueType{i32, i32}, "fd", "result.stat")
var fdFdstatGet = newHostFunc(wasip1.FdFdstatGetName, fdFdstatGetFn, []wasm.ValueType{i32, i32}, "fd", "result.stat")
// fdFdstatGetFn cannot currently use proxyResultParams because fdstat is larger
// than api.ValueTypeI64 (i64 == 8 bytes, but fdstat is 24).
@@ -206,7 +206,7 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) experimen
fdflags |= wasip1.FD_APPEND
}
if f.File.IsNonblock() {
if isNonblock(f.File) {
fdflags |= wasip1.FD_NONBLOCK
}
@@ -311,9 +311,14 @@ func fdFdstatSetFlagsFn(_ context.Context, mod api.Module, params []uint64) expe
return experimentalsys.EBADF
} else {
nonblock := wasip1.FD_NONBLOCK&wasiFlag != 0
errno := f.File.SetNonblock(nonblock)
if errno != 0 {
return errno
if pf, ok := f.File.(experimentalsys.PollableFile); ok {
if errno := pf.SetNonblock(nonblock); errno != 0 {
return errno
}
} else if isDir, errno := f.File.IsDir(); errno == 0 && isDir {
return experimentalsys.EISDIR
} else if nonblock {
return experimentalsys.ENOSYS
}
if stat, err := f.File.Stat(); err == 0 && stat.Mode.IsRegular() {
// For normal files, proceed to apply an append flag.
@@ -381,7 +386,7 @@ var fdFdstatSetRights = stubFunction(
// Note: This is similar to `fstat` in POSIX.
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat
// and https://linux.die.net/man/3/fstat
var fdFilestatGet = newHostFunc(wasip1.FdFilestatGetName, fdFilestatGetFn, []api.ValueType{i32, i32}, "fd", "result.filestat")
var fdFilestatGet = newHostFunc(wasip1.FdFilestatGetName, fdFilestatGetFn, []wasm.ValueType{i32, i32}, "fd", "result.filestat")
// fdFilestatGetFn cannot currently use proxyResultParams because filestat is
// larger than api.ValueTypeI64 (i64 == 8 bytes, but filestat is 64).
@@ -559,7 +564,7 @@ func toTimes(walltime func() int64, atim, mtim int64, fstFlags uint16) (int64, i
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_preadfd-fd-iovs-iovec_array-offset-filesize---errno-size
var fdPread = newHostFunc(
wasip1.FdPreadName, fdPreadFn,
[]api.ValueType{i32, i32, i32, i64, i32},
[]wasm.ValueType{i32, i32, i32, i64, i32},
"fd", "iovs", "iovs_len", "offset", "result.nread",
)
@@ -598,7 +603,7 @@ func fdPreadFn(_ context.Context, mod api.Module, params []uint64) experimentals
//
// See fdPrestatDirName and
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat
var fdPrestatGet = newHostFunc(wasip1.FdPrestatGetName, fdPrestatGetFn, []api.ValueType{i32, i32}, "fd", "result.prestat")
var fdPrestatGet = newHostFunc(wasip1.FdPrestatGetName, fdPrestatGetFn, []wasm.ValueType{i32, i32}, "fd", "result.prestat")
func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -611,7 +616,7 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) experime
// Upper 32-bits are zero because...
// * Zero-value 8-bit tag, and 3-byte zero-value padding
prestat := uint64(len(name) << 32)
prestat := uint64(len(name)) << 32
if !mod.Memory().WriteUint64Le(resultPrestat, prestat) {
return experimentalsys.EFAULT
}
@@ -650,7 +655,7 @@ func fdPrestatGetFn(_ context.Context, mod api.Module, params []uint64) experime
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_prestat_dir_name
var fdPrestatDirName = newHostFunc(
wasip1.FdPrestatDirNameName, fdPrestatDirNameFn,
[]api.ValueType{i32, i32, i32},
[]wasm.ValueType{i32, i32, i32},
"fd", "result.path", "result.path_len",
)
@@ -682,7 +687,7 @@ func fdPrestatDirNameFn(_ context.Context, mod api.Module, params []uint64) expe
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_pwritefd-fd-iovs-ciovec_array-offset-filesize---errno-size
var fdPwrite = newHostFunc(
wasip1.FdPwriteName, fdPwriteFn,
[]api.ValueType{i32, i32, i32, i64, i32},
[]wasm.ValueType{i32, i32, i32, i64, i32},
"fd", "iovs", "iovs_len", "offset", "result.nwritten",
)
@@ -741,7 +746,7 @@ func fdPwriteFn(_ context.Context, mod api.Module, params []uint64) experimental
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_read
var fdRead = newHostFunc(
wasip1.FdReadName, fdReadFn,
[]api.ValueType{i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32},
"fd", "iovs", "iovs_len", "result.nread",
)
@@ -1108,7 +1113,7 @@ func fdRenumberFn(_ context.Context, mod api.Module, params []uint64) experiment
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_seek
var fdSeek = newHostFunc(
wasip1.FdSeekName, fdSeekFn,
[]api.ValueType{i32, i64, i32, i32},
[]wasm.ValueType{i32, i64, i32, i32},
"fd", "offset", "whence", "result.newoffset",
)
@@ -1135,7 +1140,7 @@ func fdSeekFn(_ context.Context, mod api.Module, params []uint64) experimentalsy
// and metadata of a file to disk.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_syncfd-fd---errno
var fdSync = newHostFunc(wasip1.FdSyncName, fdSyncFn, []api.ValueType{i32}, "fd")
var fdSync = newHostFunc(wasip1.FdSyncName, fdSyncFn, []wasm.ValueType{i32}, "fd")
func fdSyncFn(_ context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -1153,7 +1158,7 @@ func fdSyncFn(_ context.Context, mod api.Module, params []uint64) experimentalsy
// offset of a file descriptor.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_tellfd-fd---errno-filesize
var fdTell = newHostFunc(wasip1.FdTellName, fdTellFn, []api.ValueType{i32, i32}, "fd", "result.offset")
var fdTell = newHostFunc(wasip1.FdTellName, fdTellFn, []wasm.ValueType{i32, i32}, "fd", "result.offset")
func fdTellFn(ctx context.Context, mod api.Module, params []uint64) experimentalsys.Errno {
fd := params[0]
@@ -1225,7 +1230,7 @@ func fdTellFn(ctx context.Context, mod api.Module, params []uint64) experimental
// and https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fd_write
var fdWrite = newHostFunc(
wasip1.FdWriteName, fdWriteFn,
[]api.ValueType{i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32},
"fd", "iovs", "iovs_len", "result.nwritten",
)
@@ -1385,7 +1390,7 @@ func pathCreateDirectoryFn(_ context.Context, mod api.Module, params []uint64) e
// and https://linux.die.net/man/2/fstatat
var pathFilestatGet = newHostFunc(
wasip1.PathFilestatGetName, pathFilestatGetFn,
[]api.ValueType{i32, i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32, i32},
"fd", "flags", "path", "path_len", "result.filestat",
)
@@ -1566,7 +1571,7 @@ func pathLinkFn(_ context.Context, mod api.Module, params []uint64) experimental
// See https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#path_open
var pathOpen = newHostFunc(
wasip1.PathOpenName, pathOpenFn,
[]api.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32, i32, i64, i64, i32, i32},
"fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd",
)
@@ -1596,6 +1601,10 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) experimental
return errno
}
if pathLen == 0 {
return experimentalsys.EINVAL
}
fileOpenFlags := openFlags(dirflags, oflags, fdflags, rights)
isDir := fileOpenFlags&experimentalsys.O_DIRECTORY != 0
@@ -1704,7 +1713,6 @@ func openFlags(dirflags, oflags, fdflags uint16, rights uint32) (openFlags exper
}
if oflags&wasip1.O_DIRECTORY != 0 {
openFlags |= experimentalsys.O_DIRECTORY
return // Early return for directories as the rest of flags doesn't make sense for it.
} else if oflags&wasip1.O_EXCL != 0 {
openFlags |= experimentalsys.O_EXCL
}
@@ -1951,25 +1959,19 @@ func pathSymlinkFn(_ context.Context, mod api.Module, params []uint64) experimen
return experimentalsys.EFAULT
}
newPathBuf, ok := mem.Read(newPath, newPathLen)
if !ok {
return experimentalsys.EFAULT
_, newPathName, errno := atPath(fsc, mod.Memory(), fd, newPath, newPathLen)
if errno != 0 {
return errno
}
return dir.FS.Symlink(
// Do not join old path since it's only resolved when dereference the link created here.
// And the dereference result depends on the opening directory's file descriptor at that point.
bufToStr(oldPathBuf),
path.Join(dir.Name, bufToStr(newPathBuf)),
unsafe.String(&oldPathBuf[0], int(oldPathLen)),
newPathName,
)
}
// bufToStr converts the given byte slice as string unsafely.
func bufToStr(buf []byte) string {
// TODO: use unsafe.String after flooring Go 1.20.
return *(*string)(unsafe.Pointer(&buf))
}
// pathUnlinkFile is the WASI function named PathUnlinkFileName which unlinks a
// file.
//
@@ -6,7 +6,6 @@ import (
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
@@ -39,7 +38,7 @@ import (
// See https://linux.die.net/man/3/poll
var pollOneoff = newHostFunc(
wasip1.PollOneoffName, pollOneoffFn,
[]api.ValueType{i32, i32, i32, i32},
[]wasm.ValueType{i32, i32, i32, i32},
"in", "out", "nsubscriptions", "result.nevents",
)
@@ -68,9 +67,7 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
}
outBuf, ok := mem.Read(out, nsubscriptions*32)
// zero-out all buffer before writing
for i := range outBuf {
outBuf[i] = 0
}
clear(outBuf)
if !ok {
return sys.EFAULT
@@ -134,7 +131,7 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
evt.errno = wasip1.ErrnoBadf
writeEvent(outBuf[outOffset:], evt)
nevents++
} else if fd != internalsys.FdStdin && file.File.IsNonblock() {
} else if fd != internalsys.FdStdin && isNonblock(file.File) {
writeEvent(outBuf[outOffset:], evt)
nevents++
} else {
@@ -176,17 +173,19 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
if !ok {
return sys.EBADF
}
// Wait for the timeout to expire, or for some data to become available on Stdin.
if stdinReady, errno := stdin.File.Poll(fsapi.POLLIN, int32(timeout.Milliseconds())); errno != 0 {
return errno
} else if stdinReady {
// stdin has data ready to for reading, write back all the events
for i := range blockingStdinSubs {
evt := blockingStdinSubs[i]
evt.errno = 0
writeEvent(outBuf[nevents*32:], evt)
nevents++
// Wait for the timeout to expire, or for some data to become available on Stdin.
if p, ok := stdin.File.(sys.Pollable); ok {
if stdinReady, errno := p.Poll(sys.POLLIN, int32(timeout.Milliseconds())); errno != 0 {
return errno
} else if stdinReady {
// stdin has data ready to for reading, write back all the events
for i := range blockingStdinSubs {
evt := blockingStdinSubs[i]
evt.errno = 0
writeEvent(outBuf[nevents*32:], evt)
nevents++
}
}
}
@@ -228,6 +227,15 @@ func processClockEvent(inBuf []byte) (time.Duration, sys.Errno) {
}
}
// isNonblock returns true if the file implements PollableFile and is in
// non-blocking mode.
func isNonblock(f sys.File) bool {
if pf, ok := f.(sys.PollableFile); ok {
return pf.IsNonblock()
}
return false
}
// writeEvent writes the event corresponding to the processed subscription.
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-event-struct
func writeEvent(outBuf []byte, evt *event) {
@@ -21,7 +21,7 @@ import (
var procExit = &wasm.HostFunc{
ExportName: wasip1.ProcExitName,
Name: wasip1.ProcExitName,
ParamTypes: []api.ValueType{i32},
ParamTypes: []wasm.ValueType{i32},
ParamNames: []string{"rval"},
Code: wasm.Code{GoFunc: api.GoModuleFunc(procExitFn)},
}
@@ -41,4 +41,4 @@ func procExitFn(ctx context.Context, mod api.Module, params []uint64) {
// procRaise is stubbed and will never be supported, as it was removed.
//
// See https://github.com/WebAssembly/WASI/pull/136
var procRaise = stubFunction(wasip1.ProcRaiseName, []api.ValueType{i32}, "sig")
var procRaise = stubFunction(wasip1.ProcRaiseName, []wasm.ValueType{i32}, "sig")
@@ -34,7 +34,7 @@ import (
// buf --^
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
var randomGet = newHostFunc(wasip1.RandomGetName, randomGetFn, []api.ValueType{i32, i32}, "buf", "buf_len")
var randomGet = newHostFunc(wasip1.RandomGetName, randomGetFn, []wasm.ValueType{i32, i32}, "buf", "buf_len")
func randomGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
@@ -269,7 +269,7 @@ func writeOffsetsAndNullTerminatedValues(mem api.Memory, values [][]byte, offset
func newHostFunc(
name string,
goFunc wasiFunc,
paramTypes []api.ValueType,
paramTypes []wasm.ValueType,
paramNames ...string,
) *wasm.HostFunc {
return &wasm.HostFunc{
@@ -277,7 +277,7 @@ func newHostFunc(
Name: name,
ParamTypes: paramTypes,
ParamNames: paramNames,
ResultTypes: []api.ValueType{i32},
ResultTypes: []wasm.ValueType{i32},
ResultNames: []string{"errno"},
Code: wasm.Code{GoFunc: goFunc},
}
@@ -305,7 +305,7 @@ func stubFunction(name string, paramTypes []wasm.ValueType, paramNames ...string
Name: name,
ParamTypes: paramTypes,
ParamNames: paramNames,
ResultTypes: []api.ValueType{i32},
ResultTypes: []wasm.ValueType{i32},
ResultNames: []string{"errno"},
Code: wasm.Code{
GoFunc: api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = uint64(wasip1.ErrnoNosys) }),
+17 -32
View File
@@ -1,6 +1,9 @@
package descriptor
import "math/bits"
import (
"math/bits"
"slices"
)
// Table is a data structure mapping 32 bit descriptor to items.
//
@@ -37,23 +40,13 @@ func (t *Table[Key, Item]) Len() (n int) {
return n
}
// grow ensures that t has enough room for n items, potentially reallocating the
// internal buffers if their capacity was too small to hold this many items.
// grow grows the table by n * 64 items.
func (t *Table[Key, Item]) grow(n int) {
// Round up to a multiple of 64 since this is the smallest increment due to
// using 64 bits masks.
n = (n*64 + 63) / 64
total := len(t.masks) + n
t.masks = slices.Grow(t.masks, n)[:total]
if n > len(t.masks) {
masks := make([]uint64, n)
copy(masks, t.masks)
items := make([]Item, n*64)
copy(items, t.items)
t.masks = masks
t.items = items
}
total = len(t.items) + n*64
t.items = slices.Grow(t.items, n*64)[:total]
}
// Insert inserts the given item to the table, returning the key that it is
@@ -78,13 +71,9 @@ insert:
}
}
// No free slot found, grow the table and retry.
offset = len(t.masks)
n := 2 * len(t.masks)
if n == 0 {
n = 1
}
t.grow(n)
t.grow(1)
goto insert
}
@@ -109,10 +98,10 @@ func (t *Table[Key, Item]) InsertAt(item Item, key Key) bool {
if key < 0 {
return false
}
if diff := int(key) - t.Len(); diff > 0 {
index := uint(key) / 64
if diff := int(index) - len(t.masks) + 1; diff > 0 {
t.grow(diff)
}
index := uint(key) / 64
shift := uint(key) % 64
t.masks[index] |= 1 << shift
t.items[key] = item
@@ -124,7 +113,8 @@ func (t *Table[Key, Item]) Delete(key Key) {
if key < 0 { // invalid key
return
}
if index, shift := key/64, key%64; int(index) < len(t.masks) {
if index := uint(key) / 64; int(index) < len(t.masks) {
shift := uint(key) % 64
mask := t.masks[index]
if (mask & (1 << shift)) != 0 {
var zero Item
@@ -154,11 +144,6 @@ func (t *Table[Key, Item]) Range(f func(Key, Item) bool) {
// Reset clears the content of the table.
func (t *Table[Key, Item]) Reset() {
for i := range t.masks {
t.masks[i] = 0
}
var zero Item
for i := range t.items {
t.items[i] = zero
}
clear(t.masks)
clear(t.items)
}
@@ -1,19 +1,17 @@
package wazeroir
package interpreter
import (
"bytes"
)
const EntrypointLabel = ".entrypoint"
func Format(ops []UnionOperation) string {
func format(ops []unionOperation) string {
buf := bytes.NewBuffer(nil)
_, _ = buf.WriteString(EntrypointLabel + "\n")
_, _ = buf.WriteString(".entrypoint\n")
for i := range ops {
op := &ops[i]
str := op.String()
isLabel := op.Kind == OperationKindLabel
isLabel := op.Kind == operationKindLabel
if !isLabel {
const indent = "\t"
str = indent + str
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,4 +1,4 @@
package wazeroir
package interpreter
import (
"fmt"
@@ -9,249 +9,249 @@ import (
// signature represents how a Wasm opcode
// manipulates the value stacks in terms of value types.
type signature struct {
in, out []UnsignedType
in, out []unsignedType
}
var (
signature_None_None = &signature{}
signature_Unknown_None = &signature{
in: []UnsignedType{UnsignedTypeUnknown},
in: []unsignedType{unsignedTypeUnknown},
}
signature_None_I32 = &signature{
out: []UnsignedType{UnsignedTypeI32},
out: []unsignedType{unsignedTypeI32},
}
signature_None_I64 = &signature{
out: []UnsignedType{UnsignedTypeI64},
out: []unsignedType{unsignedTypeI64},
}
signature_None_V128 = &signature{
out: []UnsignedType{UnsignedTypeV128},
out: []unsignedType{unsignedTypeV128},
}
signature_None_F32 = &signature{
out: []UnsignedType{UnsignedTypeF32},
out: []unsignedType{unsignedTypeF32},
}
signature_None_F64 = &signature{
out: []UnsignedType{UnsignedTypeF64},
out: []unsignedType{unsignedTypeF64},
}
signature_I32_None = &signature{
in: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32},
}
signature_I64_None = &signature{
in: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeI64},
}
signature_F32_None = &signature{
in: []UnsignedType{UnsignedTypeF32},
in: []unsignedType{unsignedTypeF32},
}
signature_F64_None = &signature{
in: []UnsignedType{UnsignedTypeF64},
in: []unsignedType{unsignedTypeF64},
}
signature_V128_None = &signature{
in: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128},
}
signature_I32_I32 = &signature{
in: []UnsignedType{UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32},
out: []unsignedType{unsignedTypeI32},
}
signature_I32_I64 = &signature{
in: []UnsignedType{UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeI32},
out: []unsignedType{unsignedTypeI64},
}
signature_I64_I64 = &signature{
in: []UnsignedType{UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeI64},
out: []unsignedType{unsignedTypeI64},
}
signature_I32_F32 = &signature{
in: []UnsignedType{UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeF32},
in: []unsignedType{unsignedTypeI32},
out: []unsignedType{unsignedTypeF32},
}
signature_I32_F64 = &signature{
in: []UnsignedType{UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeF64},
in: []unsignedType{unsignedTypeI32},
out: []unsignedType{unsignedTypeF64},
}
signature_I64_I32 = &signature{
in: []UnsignedType{UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI64},
out: []unsignedType{unsignedTypeI32},
}
signature_I64_F32 = &signature{
in: []UnsignedType{UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeF32},
in: []unsignedType{unsignedTypeI64},
out: []unsignedType{unsignedTypeF32},
}
signature_I64_F64 = &signature{
in: []UnsignedType{UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeF64},
in: []unsignedType{unsignedTypeI64},
out: []unsignedType{unsignedTypeF64},
}
signature_F32_I32 = &signature{
in: []UnsignedType{UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeF32},
out: []unsignedType{unsignedTypeI32},
}
signature_F32_I64 = &signature{
in: []UnsignedType{UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeF32},
out: []unsignedType{unsignedTypeI64},
}
signature_F32_F64 = &signature{
in: []UnsignedType{UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeF64},
in: []unsignedType{unsignedTypeF32},
out: []unsignedType{unsignedTypeF64},
}
signature_F32_F32 = &signature{
in: []UnsignedType{UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeF32},
in: []unsignedType{unsignedTypeF32},
out: []unsignedType{unsignedTypeF32},
}
signature_F64_I32 = &signature{
in: []UnsignedType{UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeF64},
out: []unsignedType{unsignedTypeI32},
}
signature_F64_F32 = &signature{
in: []UnsignedType{UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeF32},
in: []unsignedType{unsignedTypeF64},
out: []unsignedType{unsignedTypeF32},
}
signature_F64_I64 = &signature{
in: []UnsignedType{UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeF64},
out: []unsignedType{unsignedTypeI64},
}
signature_F64_F64 = &signature{
in: []UnsignedType{UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeF64},
in: []unsignedType{unsignedTypeF64},
out: []unsignedType{unsignedTypeF64},
}
signature_I32I32_None = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32, unsignedTypeI32},
}
signature_I32I32_I32 = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32, unsignedTypeI32},
out: []unsignedType{unsignedTypeI32},
}
signature_I32I64_None = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI64},
in: []unsignedType{unsignedTypeI32, unsignedTypeI64},
}
signature_I32F32_None = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeF32},
in: []unsignedType{unsignedTypeI32, unsignedTypeF32},
}
signature_I32F64_None = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeF64},
in: []unsignedType{unsignedTypeI32, unsignedTypeF64},
}
signature_I64I32_I32 = &signature{
in: []UnsignedType{UnsignedTypeI64, UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI64, unsignedTypeI32},
out: []unsignedType{unsignedTypeI32},
}
signature_I64I64_I32 = &signature{
in: []UnsignedType{UnsignedTypeI64, UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI64, unsignedTypeI64},
out: []unsignedType{unsignedTypeI32},
}
signature_I64I64_I64 = &signature{
in: []UnsignedType{UnsignedTypeI64, UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeI64, unsignedTypeI64},
out: []unsignedType{unsignedTypeI64},
}
signature_F32F32_I32 = &signature{
in: []UnsignedType{UnsignedTypeF32, UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeF32, unsignedTypeF32},
out: []unsignedType{unsignedTypeI32},
}
signature_F32F32_F32 = &signature{
in: []UnsignedType{UnsignedTypeF32, UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeF32},
in: []unsignedType{unsignedTypeF32, unsignedTypeF32},
out: []unsignedType{unsignedTypeF32},
}
signature_F64F64_I32 = &signature{
in: []UnsignedType{UnsignedTypeF64, UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeF64, unsignedTypeF64},
out: []unsignedType{unsignedTypeI32},
}
signature_F64F64_F64 = &signature{
in: []UnsignedType{UnsignedTypeF64, UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeF64},
in: []unsignedType{unsignedTypeF64, unsignedTypeF64},
out: []unsignedType{unsignedTypeF64},
}
signature_I32I32I32_None = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI32, UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32, unsignedTypeI32, unsignedTypeI32},
}
signature_I32I64I32_None = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI64, UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32, unsignedTypeI64, unsignedTypeI32},
}
signature_UnknownUnknownI32_Unknown = &signature{
in: []UnsignedType{UnsignedTypeUnknown, UnsignedTypeUnknown, UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeUnknown},
in: []unsignedType{unsignedTypeUnknown, unsignedTypeUnknown, unsignedTypeI32},
out: []unsignedType{unsignedTypeUnknown},
}
signature_V128V128_V128 = &signature{
in: []UnsignedType{UnsignedTypeV128, UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128, unsignedTypeV128},
out: []unsignedType{unsignedTypeV128},
}
signature_V128V128V128_V32 = &signature{
in: []UnsignedType{UnsignedTypeV128, UnsignedTypeV128, UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128, unsignedTypeV128, unsignedTypeV128},
out: []unsignedType{unsignedTypeV128},
}
signature_I32_V128 = &signature{
in: []UnsignedType{UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeI32},
out: []unsignedType{unsignedTypeV128},
}
signature_I32V128_None = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeV128},
in: []unsignedType{unsignedTypeI32, unsignedTypeV128},
}
signature_I32V128_V128 = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeI32, unsignedTypeV128},
out: []unsignedType{unsignedTypeV128},
}
signature_V128I32_V128 = &signature{
in: []UnsignedType{UnsignedTypeV128, UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128, unsignedTypeI32},
out: []unsignedType{unsignedTypeV128},
}
signature_V128I64_V128 = &signature{
in: []UnsignedType{UnsignedTypeV128, UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128, unsignedTypeI64},
out: []unsignedType{unsignedTypeV128},
}
signature_V128F32_V128 = &signature{
in: []UnsignedType{UnsignedTypeV128, UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128, unsignedTypeF32},
out: []unsignedType{unsignedTypeV128},
}
signature_V128F64_V128 = &signature{
in: []UnsignedType{UnsignedTypeV128, UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128, unsignedTypeF64},
out: []unsignedType{unsignedTypeV128},
}
signature_V128_I32 = &signature{
in: []UnsignedType{UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeV128},
out: []unsignedType{unsignedTypeI32},
}
signature_V128_I64 = &signature{
in: []UnsignedType{UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeV128},
out: []unsignedType{unsignedTypeI64},
}
signature_V128_F32 = &signature{
in: []UnsignedType{UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeF32},
in: []unsignedType{unsignedTypeV128},
out: []unsignedType{unsignedTypeF32},
}
signature_V128_F64 = &signature{
in: []UnsignedType{UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeF64},
in: []unsignedType{unsignedTypeV128},
out: []unsignedType{unsignedTypeF64},
}
signature_V128_V128 = &signature{
in: []UnsignedType{UnsignedTypeV128},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeV128},
out: []unsignedType{unsignedTypeV128},
}
signature_I64_V128 = &signature{
in: []UnsignedType{UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeI64},
out: []unsignedType{unsignedTypeV128},
}
signature_F32_V128 = &signature{
in: []UnsignedType{UnsignedTypeF32},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeF32},
out: []unsignedType{unsignedTypeV128},
}
signature_F64_V128 = &signature{
in: []UnsignedType{UnsignedTypeF64},
out: []UnsignedType{UnsignedTypeV128},
in: []unsignedType{unsignedTypeF64},
out: []unsignedType{unsignedTypeV128},
}
signature_I32I64_I64 = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeI32, unsignedTypeI64},
out: []unsignedType{unsignedTypeI64},
}
signature_I32I32I64_I32 = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI32, UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32, unsignedTypeI32, unsignedTypeI64},
out: []unsignedType{unsignedTypeI32},
}
signature_I32I64I64_I32 = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI64, UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32, unsignedTypeI64, unsignedTypeI64},
out: []unsignedType{unsignedTypeI32},
}
signature_I32I32I32_I32 = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI32, UnsignedTypeI32},
out: []UnsignedType{UnsignedTypeI32},
in: []unsignedType{unsignedTypeI32, unsignedTypeI32, unsignedTypeI32},
out: []unsignedType{unsignedTypeI32},
}
signature_I32I64I64_I64 = &signature{
in: []UnsignedType{UnsignedTypeI32, UnsignedTypeI64, UnsignedTypeI64},
out: []UnsignedType{UnsignedTypeI64},
in: []unsignedType{unsignedTypeI32, unsignedTypeI64, unsignedTypeI64},
out: []unsignedType{unsignedTypeI64},
}
)
@@ -259,8 +259,8 @@ var (
// Note that some of opcodes' signature vary depending on
// the function instance (for example, local types).
// "index" parameter is not used by most of opcodes.
// The returned signature is used for stack validation when lowering Wasm's opcodes to wazeroir.
func (c *Compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature, error) {
// The returned signature is used for stack validation when lowering Wasm's opcodes to interpreterir.
func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature, error) {
switch op {
case wasm.OpcodeUnreachable, wasm.OpcodeNop, wasm.OpcodeBlock, wasm.OpcodeLoop:
return signature_None_None, nil
@@ -268,14 +268,28 @@ func (c *Compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
return signature_I32_None, nil
case wasm.OpcodeElse, wasm.OpcodeEnd, wasm.OpcodeBr:
return signature_None_None, nil
case wasm.OpcodeThrow, wasm.OpcodeThrowRef, wasm.OpcodeTryTable:
// Stack manipulation handled dynamically by the compiler.
return signature_None_None, nil
case wasm.OpcodeBrIf, wasm.OpcodeBrTable:
return signature_I32_None, nil
case wasm.OpcodeReturn:
return signature_None_None, nil
case wasm.OpcodeCall:
case wasm.OpcodeCall, wasm.OpcodeTailCallReturnCall:
return c.funcTypeToSigs.get(c.funcs[index], false /* direct */), nil
case wasm.OpcodeCallIndirect:
case wasm.OpcodeCallIndirect, wasm.OpcodeTailCallReturnCallIndirect:
return c.funcTypeToSigs.get(index, true /* call_indirect */), nil
case wasm.OpcodeCallRef, wasm.OpcodeReturnCallRef:
return c.funcTypeToSigs.getCallRef(index), nil
case wasm.OpcodeRefAsNonNull:
// Pop a ref (i64), push it back (i64). Traps if null at runtime.
return signature_I64_I64, nil
case wasm.OpcodeBrOnNull:
// Pop a ref (i64). If null, branch; if non-null, push ref back.
return signature_I64_None, nil
case wasm.OpcodeBrOnNonNull:
// Pop a ref (i64). If non-null, push and branch; if null, fall through.
return signature_I64_None, nil
case wasm.OpcodeDrop:
return signature_Unknown_None, nil
case wasm.OpcodeSelect, wasm.OpcodeTypedSelect:
@@ -464,7 +478,7 @@ func (c *Compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
// table.set takes table's offset and the ref type value of opaque pointer as i64 value.
return signature_I32I64_None, nil
case wasm.OpcodeRefFunc:
// ref.func is translated as pushing the compiled function's opaque pointer (uint64) at wazeroir layer.
// ref.func is translated as pushing the compiled function's opaque pointer (uint64) at interpreterir layer.
return signature_None_I64, nil
case wasm.OpcodeRefIsNull:
// ref.is_null is translated as checking if the uint64 on the top of the stack (opaque pointer) is zero or not.
@@ -494,7 +508,7 @@ func (c *Compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
case wasm.OpcodeMiscTableFill:
return signature_I32I64I32_None, nil
default:
return nil, fmt.Errorf("unsupported misc instruction in wazeroir: 0x%x", op)
return nil, fmt.Errorf("unsupported misc instruction in interpreterir: 0x%x", op)
}
case wasm.OpcodeVecPrefix:
switch vecOp := c.body[c.pc+1]; vecOp {
@@ -604,7 +618,7 @@ func (c *Compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
wasm.OpcodeVecI8x16NarrowI16x8S, wasm.OpcodeVecI8x16NarrowI16x8U, wasm.OpcodeVecI16x8NarrowI32x4S, wasm.OpcodeVecI16x8NarrowI32x4U:
return signature_V128V128_V128, nil
default:
return nil, fmt.Errorf("unsupported vector instruction in wazeroir: %s", wasm.VectorInstructionName(vecOp))
return nil, fmt.Errorf("unsupported vector instruction in interpreterir: %s", wasm.VectorInstructionName(vecOp))
}
case wasm.OpcodeAtomicPrefix:
switch atomicOp := c.body[c.pc+1]; atomicOp {
@@ -638,10 +652,10 @@ func (c *Compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
case wasm.OpcodeAtomicI64RmwCmpxchg, wasm.OpcodeAtomicI64Rmw8CmpxchgU, wasm.OpcodeAtomicI64Rmw16CmpxchgU, wasm.OpcodeAtomicI64Rmw32CmpxchgU:
return signature_I32I64I64_I64, nil
default:
return nil, fmt.Errorf("unsupported atomic instruction in wazeroir: %s", wasm.AtomicInstructionName(atomicOp))
return nil, fmt.Errorf("unsupported atomic instruction in interpreterir: %s", wasm.AtomicInstructionName(atomicOp))
}
default:
return nil, fmt.Errorf("unsupported instruction in wazeroir: 0x%x", op)
return nil, fmt.Errorf("unsupported instruction in interpreterir: 0x%x", op)
}
}
@@ -650,6 +664,7 @@ func (c *Compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
type funcTypeToIRSignatures struct {
directCalls []*signature
indirectCalls []*signature
callRefCalls []*signature
wasmTypes []wasm.FunctionType
}
@@ -668,25 +683,25 @@ func (f *funcTypeToIRSignatures) get(typeIndex wasm.Index, indirect bool) *signa
tp := &f.wasmTypes[typeIndex]
if indirect {
sig = &signature{
in: make([]UnsignedType, 0, len(tp.Params)+1), // +1 to reserve space for call indirect index.
out: make([]UnsignedType, 0, len(tp.Results)),
in: make([]unsignedType, 0, len(tp.Params)+1), // +1 to reserve space for call indirect index.
out: make([]unsignedType, 0, len(tp.Results)),
}
} else {
sig = &signature{
in: make([]UnsignedType, 0, len(tp.Params)),
out: make([]UnsignedType, 0, len(tp.Results)),
in: make([]unsignedType, 0, len(tp.Params)),
out: make([]unsignedType, 0, len(tp.Results)),
}
}
for _, vt := range tp.Params {
sig.in = append(sig.in, wasmValueTypeToUnsignedType(vt))
sig.in = append(sig.in, wasmValueTypeTounsignedType(vt))
}
for _, vt := range tp.Results {
sig.out = append(sig.out, wasmValueTypeToUnsignedType(vt))
sig.out = append(sig.out, wasmValueTypeTounsignedType(vt))
}
if indirect {
sig.in = append(sig.in, UnsignedTypeI32)
sig.in = append(sig.in, unsignedTypeI32)
f.indirectCalls[typeIndex] = sig
} else {
f.directCalls[typeIndex] = sig
@@ -694,20 +709,48 @@ func (f *funcTypeToIRSignatures) get(typeIndex wasm.Index, indirect bool) *signa
return sig
}
func wasmValueTypeToUnsignedType(vt wasm.ValueType) UnsignedType {
// getCallRef returns the *signature for call_ref, which is like a direct call
// but with an extra i64 input for the funcref operand.
func (f *funcTypeToIRSignatures) getCallRef(typeIndex wasm.Index) *signature {
if sig := f.callRefCalls[typeIndex]; sig != nil {
return sig
}
tp := &f.wasmTypes[typeIndex]
sig := &signature{
in: make([]unsignedType, 0, len(tp.Params)+1),
out: make([]unsignedType, 0, len(tp.Results)),
}
for _, vt := range tp.Params {
sig.in = append(sig.in, wasmValueTypeTounsignedType(vt))
}
sig.in = append(sig.in, unsignedTypeI64) // funcref operand
for _, vt := range tp.Results {
sig.out = append(sig.out, wasmValueTypeTounsignedType(vt))
}
f.callRefCalls[typeIndex] = sig
return sig
}
func wasmValueTypeTounsignedType(vt wasm.ValueType) unsignedType {
switch vt {
case wasm.ValueTypeI32:
return UnsignedTypeI32
return unsignedTypeI32
case wasm.ValueTypeI64,
// From wazeroir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
return UnsignedTypeI64
// From interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return unsignedTypeI64
case wasm.ValueTypeF32:
return UnsignedTypeF32
return unsignedTypeF32
case wasm.ValueTypeF64:
return UnsignedTypeF64
return unsignedTypeF64
case wasm.ValueTypeV128:
return UnsignedTypeV128
return unsignedTypeV128
default:
// Concrete ref types (ref $t) have variable bit patterns.
if vt.IsRef() {
return unsignedTypeI64
}
}
panic("unreachable")
}
@@ -717,8 +760,9 @@ func wasmValueTypeToUnsignedOutSignature(vt wasm.ValueType) *signature {
case wasm.ValueTypeI32:
return signature_None_I32
case wasm.ValueTypeI64,
// From wazeroir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
// From interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return signature_None_I64
case wasm.ValueTypeF32:
return signature_None_F32
@@ -726,6 +770,10 @@ func wasmValueTypeToUnsignedOutSignature(vt wasm.ValueType) *signature {
return signature_None_F64
case wasm.ValueTypeV128:
return signature_None_V128
default:
if vt.IsRef() {
return signature_None_I64
}
}
panic("unreachable")
}
@@ -735,8 +783,9 @@ func wasmValueTypeToUnsignedInSignature(vt wasm.ValueType) *signature {
case wasm.ValueTypeI32:
return signature_I32_None
case wasm.ValueTypeI64,
// From wazeroir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
// From interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return signature_I64_None
case wasm.ValueTypeF32:
return signature_F32_None
@@ -744,6 +793,10 @@ func wasmValueTypeToUnsignedInSignature(vt wasm.ValueType) *signature {
return signature_F64_None
case wasm.ValueTypeV128:
return signature_V128_None
default:
if vt.IsRef() {
return signature_I64_None
}
}
panic("unreachable")
}
@@ -753,8 +806,9 @@ func wasmValueTypeToUnsignedInOutSignature(vt wasm.ValueType) *signature {
case wasm.ValueTypeI32:
return signature_I32_I32
case wasm.ValueTypeI64,
// From wazeroir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
// At interpreterir layer, ref type values are opaque 64-bit pointers.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref,
wasm.ValueTypeExnref:
return signature_I64_I64
case wasm.ValueTypeF32:
return signature_F32_F32
@@ -762,6 +816,10 @@ func wasmValueTypeToUnsignedInOutSignature(vt wasm.ValueType) *signature {
return signature_F64_F64
case wasm.ValueTypeV128:
return signature_V128_V128
default:
if vt.IsRef() {
return signature_I64_I64
}
}
panic("unreachable")
}
@@ -1 +0,0 @@
wazevo is the new optimizing compiler engine which only supports arm64. AMD64 is under active development.
@@ -69,7 +69,7 @@ type Compiler interface {
AllocateVReg(typ ssa.Type) regalloc.VReg
// ValueDefinition returns the definition of the given value.
ValueDefinition(ssa.Value) *SSAValueDefinition
ValueDefinition(ssa.Value) SSAValueDefinition
// VRegOf returns the virtual register of the given ssa.Value.
VRegOf(value ssa.Value) regalloc.VReg
@@ -79,16 +79,16 @@ type Compiler interface {
// MatchInstr returns true if the given definition is from an instruction with the given opcode, the current group ID,
// and a refcount of 1. That means, the instruction can be merged/swapped within the current instruction group.
MatchInstr(def *SSAValueDefinition, opcode ssa.Opcode) bool
MatchInstr(def SSAValueDefinition, opcode ssa.Opcode) bool
// MatchInstrOneOf is the same as MatchInstr but for multiple opcodes. If it matches one of ssa.Opcode,
// this returns the opcode. Otherwise, this returns ssa.OpcodeInvalid.
//
// Note: caller should be careful to avoid excessive allocation on opcodes slice.
MatchInstrOneOf(def *SSAValueDefinition, opcodes []ssa.Opcode) ssa.Opcode
MatchInstrOneOf(def SSAValueDefinition, opcodes []ssa.Opcode) ssa.Opcode
// AddRelocationInfo appends the relocation information for the function reference at the current buffer offset.
AddRelocationInfo(funcRef ssa.FuncRef)
AddRelocationInfo(funcRef ssa.FuncRef, isTailCall bool)
// AddSourceOffsetInfo appends the source offset information for the given offset.
AddSourceOffsetInfo(executableOffset int64, sourceOffset ssa.SourceOffset)
@@ -115,6 +115,8 @@ type RelocationInfo struct {
Offset int64
// Target is the target function of the call instruction.
FuncRef ssa.FuncRef
// IsTailCall indicates whether the call instruction is a tail call.
IsTailCall bool
}
// compiler implements Compiler.
@@ -126,10 +128,7 @@ type compiler struct {
nextVRegID regalloc.VRegID
// ssaValueToVRegs maps ssa.ValueID to regalloc.VReg.
ssaValueToVRegs [] /* VRegID to */ regalloc.VReg
// ssaValueDefinitions maps ssa.ValueID to its definition.
ssaValueDefinitions []SSAValueDefinition
// ssaValueRefCounts is a cached list obtained by ssa.Builder.ValueRefCounts().
ssaValueRefCounts []int
ssaValuesInfo []ssa.ValueInfo
// returnVRegs is the list of virtual registers that store the return values.
returnVRegs []regalloc.VReg
varEdges [][2]regalloc.VReg
@@ -206,15 +205,10 @@ func (c *compiler) setCurrentGroupID(gid ssa.InstructionGroupID) {
// assignVirtualRegisters assigns a virtual register to each ssa.ValueID Valid in the ssa.Builder.
func (c *compiler) assignVirtualRegisters() {
builder := c.ssaBuilder
refCounts := builder.ValueRefCounts()
c.ssaValueRefCounts = refCounts
c.ssaValuesInfo = builder.ValuesInfo()
need := len(refCounts)
if need >= len(c.ssaValueToVRegs) {
c.ssaValueToVRegs = append(c.ssaValueToVRegs, make([]regalloc.VReg, need+1)...)
}
if need >= len(c.ssaValueDefinitions) {
c.ssaValueDefinitions = append(c.ssaValueDefinitions, make([]SSAValueDefinition, need+1)...)
if diff := len(c.ssaValuesInfo) - len(c.ssaValueToVRegs); diff > 0 {
c.ssaValueToVRegs = append(c.ssaValueToVRegs, make([]regalloc.VReg, diff+1)...)
}
for blk := builder.BlockIteratorReversePostOrderBegin(); blk != nil; blk = builder.BlockIteratorReversePostOrderNext() {
@@ -225,40 +219,26 @@ func (c *compiler) assignVirtualRegisters() {
typ := p.Type()
vreg := c.AllocateVReg(typ)
c.ssaValueToVRegs[pid] = vreg
c.ssaValueDefinitions[pid] = SSAValueDefinition{BlockParamValue: p, BlkParamVReg: vreg}
c.ssaTypeOfVRegID[vreg.ID()] = p.Type()
}
// Assigns each value to a virtual register produced by instructions.
for cur := blk.Root(); cur != nil; cur = cur.Next() {
r, rs := cur.Returns()
var N int
if r.Valid() {
id := r.ID()
ssaTyp := r.Type()
typ := r.Type()
vReg := c.AllocateVReg(typ)
c.ssaValueToVRegs[id] = vReg
c.ssaValueDefinitions[id] = SSAValueDefinition{
Instr: cur,
N: 0,
RefCount: refCounts[id],
}
c.ssaTypeOfVRegID[vReg.ID()] = ssaTyp
N++
}
for _, r := range rs {
id := r.ID()
ssaTyp := r.Type()
vReg := c.AllocateVReg(ssaTyp)
c.ssaValueToVRegs[id] = vReg
c.ssaValueDefinitions[id] = SSAValueDefinition{
Instr: cur,
N: N,
RefCount: refCounts[id],
}
c.ssaTypeOfVRegID[vReg.ID()] = ssaTyp
N++
}
}
}
@@ -299,8 +279,12 @@ func (c *compiler) Init() {
}
// ValueDefinition implements Compiler.ValueDefinition.
func (c *compiler) ValueDefinition(value ssa.Value) *SSAValueDefinition {
return &c.ssaValueDefinitions[value.ID()]
func (c *compiler) ValueDefinition(value ssa.Value) SSAValueDefinition {
return SSAValueDefinition{
V: value,
Instr: c.ssaBuilder.InstructionOfValue(value),
RefCount: c.ssaValuesInfo[value.ID()].RefCount,
}
}
// VRegOf implements Compiler.VRegOf.
@@ -319,7 +303,7 @@ func (c *compiler) TypeOf(v regalloc.VReg) ssa.Type {
}
// MatchInstr implements Compiler.MatchInstr.
func (c *compiler) MatchInstr(def *SSAValueDefinition, opcode ssa.Opcode) bool {
func (c *compiler) MatchInstr(def SSAValueDefinition, opcode ssa.Opcode) bool {
instr := def.Instr
return def.IsFromInstr() &&
instr.Opcode() == opcode &&
@@ -328,7 +312,7 @@ func (c *compiler) MatchInstr(def *SSAValueDefinition, opcode ssa.Opcode) bool {
}
// MatchInstrOneOf implements Compiler.MatchInstrOneOf.
func (c *compiler) MatchInstrOneOf(def *SSAValueDefinition, opcodes []ssa.Opcode) ssa.Opcode {
func (c *compiler) MatchInstrOneOf(def SSAValueDefinition, opcodes []ssa.Opcode) ssa.Opcode {
instr := def.Instr
if !def.IsFromInstr() {
return ssa.OpcodeInvalid
@@ -370,10 +354,11 @@ func (c *compiler) SourceOffsetInfo() []SourceOffsetInfo {
}
// AddRelocationInfo implements Compiler.AddRelocationInfo.
func (c *compiler) AddRelocationInfo(funcRef ssa.FuncRef) {
func (c *compiler) AddRelocationInfo(funcRef ssa.FuncRef, isTailCall bool) {
c.relocations = append(c.relocations, RelocationInfo{
Offset: int64(len(c.buf)),
FuncRef: funcRef,
Offset: int64(len(c.buf)),
FuncRef: funcRef,
IsTailCall: isTailCall,
})
}
@@ -9,7 +9,7 @@ import (
func (c *compiler) Lower() {
c.assignVirtualRegisters()
c.mach.SetCurrentABI(c.GetFunctionABI(c.ssaBuilder.Signature()))
c.mach.ExecutableContext().StartLoweringFunction(c.ssaBuilder.BlockIDMax())
c.mach.StartLoweringFunction(c.ssaBuilder.BlockIDMax())
c.lowerBlocks()
}
@@ -20,12 +20,11 @@ func (c *compiler) lowerBlocks() {
c.lowerBlock(blk)
}
ectx := c.mach.ExecutableContext()
// After lowering all blocks, we need to link adjacent blocks to layout one single instruction list.
var prev ssa.BasicBlock
for next := builder.BlockIteratorReversePostOrderBegin(); next != nil; next = builder.BlockIteratorReversePostOrderNext() {
if prev != nil {
ectx.LinkAdjacentBlocks(prev, next)
c.mach.LinkAdjacentBlocks(prev, next)
}
prev = next
}
@@ -33,8 +32,7 @@ func (c *compiler) lowerBlocks() {
func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
mach := c.mach
ectx := mach.ExecutableContext()
ectx.StartBlock(blk)
mach.StartBlock(blk)
// We traverse the instructions in reverse order because we might want to lower multiple
// instructions together.
@@ -76,7 +74,7 @@ func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
default:
mach.LowerInstr(cur)
}
ectx.FlushPendingInstructions()
mach.FlushPendingInstructions()
}
// Finally, if this is the entry block, we have to insert copies of arguments from the real location to the VReg.
@@ -84,7 +82,7 @@ func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
c.lowerFunctionArguments(blk)
}
ectx.EndBlock()
mach.EndBlock()
}
// lowerBranches is called right after StartBlock and before any LowerInstr call if
@@ -93,23 +91,24 @@ func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
//
// See ssa.Instruction IsBranching, and the comment on ssa.BasicBlock.
func (c *compiler) lowerBranches(br0, br1 *ssa.Instruction) {
ectx := c.mach.ExecutableContext()
mach := c.mach
c.setCurrentGroupID(br0.GroupID())
c.mach.LowerSingleBranch(br0)
ectx.FlushPendingInstructions()
mach.FlushPendingInstructions()
if br1 != nil {
c.setCurrentGroupID(br1.GroupID())
c.mach.LowerConditionalBranch(br1)
ectx.FlushPendingInstructions()
mach.FlushPendingInstructions()
}
if br0.Opcode() == ssa.OpcodeJump {
_, args, target := br0.BranchData()
_, args, targetBlockID := br0.BranchData()
argExists := len(args) != 0
if argExists && br1 != nil {
panic("BUG: critical edge split failed")
}
target := c.ssaBuilder.BasicBlock(targetBlockID)
if argExists && target.ReturnBlock() {
if len(args) > 0 {
c.mach.LowerReturns(args)
@@ -118,24 +117,25 @@ func (c *compiler) lowerBranches(br0, br1 *ssa.Instruction) {
c.lowerBlockArguments(args, target)
}
}
ectx.FlushPendingInstructions()
mach.FlushPendingInstructions()
}
func (c *compiler) lowerFunctionArguments(entry ssa.BasicBlock) {
ectx := c.mach.ExecutableContext()
mach := c.mach
c.tmpVals = c.tmpVals[:0]
data := c.ssaBuilder.ValuesInfo()
for i := 0; i < entry.Params(); i++ {
p := entry.Param(i)
if c.ssaValueRefCounts[p.ID()] > 0 {
if data[p.ID()].RefCount > 0 {
c.tmpVals = append(c.tmpVals, p)
} else {
// If the argument is not used, we can just pass an invalid value.
c.tmpVals = append(c.tmpVals, ssa.ValueInvalid)
}
}
c.mach.LowerParams(c.tmpVals)
ectx.FlushPendingInstructions()
mach.LowerParams(c.tmpVals)
mach.FlushPendingInstructions()
}
// lowerBlockArguments lowers how to pass arguments to the given successor block.
@@ -152,12 +152,12 @@ func (c *compiler) lowerBlockArguments(args []ssa.Value, succ ssa.BasicBlock) {
src := args[i]
dstReg := c.VRegOf(dst)
srcDef := c.ssaValueDefinitions[src.ID()]
if srcDef.IsFromInstr() && srcDef.Instr.Constant() {
srcInstr := c.ssaBuilder.InstructionOfValue(src)
if srcInstr != nil && srcInstr.Constant() {
c.constEdges = append(c.constEdges, struct {
cInst *ssa.Instruction
dst regalloc.VReg
}{cInst: srcDef.Instr, dst: dstReg})
}{cInst: srcInstr, dst: dstReg})
} else {
srcReg := c.VRegOf(src)
// Even when the src=dst, insert the move so that we can keep such registers keep-alive.
@@ -1,219 +0,0 @@
package backend
import (
"fmt"
"math"
"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
)
type ExecutableContext interface {
// StartLoweringFunction is called when the lowering of the given function is started.
// maximumBlockID is the maximum value of ssa.BasicBlockID existing in the function.
StartLoweringFunction(maximumBlockID ssa.BasicBlockID)
// LinkAdjacentBlocks is called after finished lowering all blocks in order to create one single instruction list.
LinkAdjacentBlocks(prev, next ssa.BasicBlock)
// StartBlock is called when the compilation of the given block is started.
// The order of this being called is the reverse post order of the ssa.BasicBlock(s) as we iterate with
// ssa.Builder BlockIteratorReversePostOrderBegin and BlockIteratorReversePostOrderEnd.
StartBlock(ssa.BasicBlock)
// EndBlock is called when the compilation of the current block is finished.
EndBlock()
// FlushPendingInstructions flushes the pending instructions to the buffer.
// This will be called after the lowering of each SSA Instruction.
FlushPendingInstructions()
}
type ExecutableContextT[Instr any] struct {
CurrentSSABlk ssa.BasicBlock
// InstrPool is the InstructionPool of instructions.
InstructionPool wazevoapi.Pool[Instr]
asNop func(*Instr)
setNext func(*Instr, *Instr)
setPrev func(*Instr, *Instr)
// RootInstr is the root instruction of the executable.
RootInstr *Instr
labelPositionPool wazevoapi.Pool[LabelPosition[Instr]]
NextLabel Label
// LabelPositions maps a label to the instructions of the region which the label represents.
LabelPositions map[Label]*LabelPosition[Instr]
OrderedBlockLabels []*LabelPosition[Instr]
// PerBlockHead and PerBlockEnd are the head and tail of the instruction list per currently-compiled ssa.BasicBlock.
PerBlockHead, PerBlockEnd *Instr
// PendingInstructions are the instructions which are not yet emitted into the instruction list.
PendingInstructions []*Instr
// SsaBlockIDToLabels maps an SSA block ID to the label.
SsaBlockIDToLabels []Label
}
func NewExecutableContextT[Instr any](
resetInstruction func(*Instr),
setNext func(*Instr, *Instr),
setPrev func(*Instr, *Instr),
asNop func(*Instr),
) *ExecutableContextT[Instr] {
return &ExecutableContextT[Instr]{
InstructionPool: wazevoapi.NewPool[Instr](resetInstruction),
asNop: asNop,
setNext: setNext,
setPrev: setPrev,
labelPositionPool: wazevoapi.NewPool[LabelPosition[Instr]](resetLabelPosition[Instr]),
LabelPositions: make(map[Label]*LabelPosition[Instr]),
NextLabel: LabelInvalid,
}
}
func resetLabelPosition[T any](l *LabelPosition[T]) {
*l = LabelPosition[T]{}
}
// StartLoweringFunction implements ExecutableContext.
func (e *ExecutableContextT[Instr]) StartLoweringFunction(max ssa.BasicBlockID) {
imax := int(max)
if len(e.SsaBlockIDToLabels) <= imax {
// Eagerly allocate labels for the blocks since the underlying slice will be used for the next iteration.
e.SsaBlockIDToLabels = append(e.SsaBlockIDToLabels, make([]Label, imax+1)...)
}
}
func (e *ExecutableContextT[Instr]) StartBlock(blk ssa.BasicBlock) {
e.CurrentSSABlk = blk
l := e.SsaBlockIDToLabels[e.CurrentSSABlk.ID()]
if l == LabelInvalid {
l = e.AllocateLabel()
e.SsaBlockIDToLabels[blk.ID()] = l
}
end := e.allocateNop0()
e.PerBlockHead, e.PerBlockEnd = end, end
labelPos, ok := e.LabelPositions[l]
if !ok {
labelPos = e.AllocateLabelPosition(l)
e.LabelPositions[l] = labelPos
}
e.OrderedBlockLabels = append(e.OrderedBlockLabels, labelPos)
labelPos.Begin, labelPos.End = end, end
labelPos.SB = blk
}
// EndBlock implements ExecutableContext.
func (e *ExecutableContextT[T]) EndBlock() {
// Insert nop0 as the head of the block for convenience to simplify the logic of inserting instructions.
e.insertAtPerBlockHead(e.allocateNop0())
l := e.SsaBlockIDToLabels[e.CurrentSSABlk.ID()]
e.LabelPositions[l].Begin = e.PerBlockHead
if e.CurrentSSABlk.EntryBlock() {
e.RootInstr = e.PerBlockHead
}
}
func (e *ExecutableContextT[T]) insertAtPerBlockHead(i *T) {
if e.PerBlockHead == nil {
e.PerBlockHead = i
e.PerBlockEnd = i
return
}
e.setNext(i, e.PerBlockHead)
e.setPrev(e.PerBlockHead, i)
e.PerBlockHead = i
}
// FlushPendingInstructions implements ExecutableContext.
func (e *ExecutableContextT[T]) FlushPendingInstructions() {
l := len(e.PendingInstructions)
if l == 0 {
return
}
for i := l - 1; i >= 0; i-- { // reverse because we lower instructions in reverse order.
e.insertAtPerBlockHead(e.PendingInstructions[i])
}
e.PendingInstructions = e.PendingInstructions[:0]
}
func (e *ExecutableContextT[T]) Reset() {
e.labelPositionPool.Reset()
e.InstructionPool.Reset()
for l := Label(0); l <= e.NextLabel; l++ {
delete(e.LabelPositions, l)
}
e.PendingInstructions = e.PendingInstructions[:0]
e.OrderedBlockLabels = e.OrderedBlockLabels[:0]
e.RootInstr = nil
e.SsaBlockIDToLabels = e.SsaBlockIDToLabels[:0]
e.PerBlockHead, e.PerBlockEnd = nil, nil
e.NextLabel = LabelInvalid
}
// AllocateLabel allocates an unused label.
func (e *ExecutableContextT[T]) AllocateLabel() Label {
e.NextLabel++
return e.NextLabel
}
func (e *ExecutableContextT[T]) AllocateLabelPosition(la Label) *LabelPosition[T] {
l := e.labelPositionPool.Allocate()
l.L = la
return l
}
func (e *ExecutableContextT[T]) GetOrAllocateSSABlockLabel(blk ssa.BasicBlock) Label {
if blk.ReturnBlock() {
return LabelReturn
}
l := e.SsaBlockIDToLabels[blk.ID()]
if l == LabelInvalid {
l = e.AllocateLabel()
e.SsaBlockIDToLabels[blk.ID()] = l
}
return l
}
func (e *ExecutableContextT[T]) allocateNop0() *T {
i := e.InstructionPool.Allocate()
e.asNop(i)
return i
}
// LinkAdjacentBlocks implements backend.Machine.
func (e *ExecutableContextT[T]) LinkAdjacentBlocks(prev, next ssa.BasicBlock) {
prevLabelPos := e.LabelPositions[e.GetOrAllocateSSABlockLabel(prev)]
nextLabelPos := e.LabelPositions[e.GetOrAllocateSSABlockLabel(next)]
e.setNext(prevLabelPos.End, nextLabelPos.Begin)
}
// LabelPosition represents the regions of the generated code which the label represents.
type LabelPosition[Instr any] struct {
SB ssa.BasicBlock
L Label
Begin, End *Instr
BinaryOffset int64
}
// Label represents a position in the generated code which is either
// a real instruction or the constant InstructionPool (e.g. jump tables).
//
// This is exactly the same as the traditional "label" in assembly code.
type Label uint32
const (
LabelInvalid Label = 0
LabelReturn Label = math.MaxUint32
)
// String implements backend.Machine.
func (l Label) String() string {
return fmt.Sprintf("L%d", l)
}
@@ -7,7 +7,7 @@ import (
)
// For the details of the ABI, see:
// https://github.com/golang/go/blob/49d42128fd8594c172162961ead19ac95e247d24/src/cmd/compile/abi-internal.md#amd64-architecture
// https://github.com/golang/go/blob/go1.24.0/src/cmd/compile/abi-internal.md#amd64-architecture
var (
intArgResultRegs = []regalloc.RealReg{rax, rbx, rcx, rdi, rsi, r8, r9, r10, r11}
@@ -14,7 +14,6 @@ var calleeSavedVRegs = []regalloc.VReg{
// CompileGoFunctionTrampoline implements backend.Machine.
func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *ssa.Signature, needModuleContextPtr bool) []byte {
ectx := m.ectx
argBegin := 1 // Skips exec context by default.
if needModuleContextPtr {
argBegin++
@@ -25,7 +24,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
m.currentABI = abi
cur := m.allocateNop()
ectx.RootInstr = cur
m.rootInstr = cur
// Execution context is always the first argument.
execCtrPtr := raxVReg
@@ -272,7 +271,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
cur = m.revertRBPRSP(cur)
linkInstr(cur, m.allocateInstr().asRet())
m.encodeWithoutSSA(ectx.RootInstr)
m.encodeWithoutSSA(m.rootInstr)
return m.c.Buf()
}
@@ -347,10 +346,8 @@ var stackGrowSaveVRegs = []regalloc.VReg{
// CompileStackGrowCallSequence implements backend.Machine.
func (m *machine) CompileStackGrowCallSequence() []byte {
ectx := m.ectx
cur := m.allocateNop()
ectx.RootInstr = cur
m.rootInstr = cur
cur = m.setupRBPRSP(cur)
@@ -379,7 +376,7 @@ func (m *machine) CompileStackGrowCallSequence() []byte {
cur = m.revertRBPRSP(cur)
linkInstr(cur, m.allocateInstr().asRet())
m.encodeWithoutSSA(ectx.RootInstr)
m.encodeWithoutSSA(m.rootInstr)
return m.c.Buf()
}
@@ -17,28 +17,17 @@ type instruction struct {
kind instructionKind
}
// Next implements regalloc.Instr.
func (i *instruction) Next() regalloc.Instr {
return i.next
}
// Prev implements regalloc.Instr.
func (i *instruction) Prev() regalloc.Instr {
return i.prev
}
// IsCall implements regalloc.Instr.
func (i *instruction) IsCall() bool { return i.kind == call }
// IsIndirectCall implements regalloc.Instr.
func (i *instruction) IsIndirectCall() bool { return i.kind == callIndirect }
func (i *instruction) IsIndirectCall() bool {
return i.kind == callIndirect
}
// IsReturn implements regalloc.Instr.
func (i *instruction) IsReturn() bool { return i.kind == ret }
// AddedBeforeRegAlloc implements regalloc.Instr.
func (i *instruction) AddedBeforeRegAlloc() bool { return i.addedBeforeRegAlloc }
// String implements regalloc.Instr.
func (i *instruction) String() string {
switch i.kind {
@@ -298,8 +287,13 @@ func (i *instruction) String() string {
}
return fmt.Sprintf("lock xadd.%s %s, %s", suffix, i.op1.format(true), i.op2.format(true))
case keepAlive:
return fmt.Sprintf("keepAlive %s", i.op1.format(true))
case nopUseReg:
return fmt.Sprintf("nop_use_reg %s", i.op1.format(true))
case tailCall:
return fmt.Sprintf("tailCall %s", ssa.FuncRef(i.u1))
case tailCallIndirect:
return fmt.Sprintf("tailCallIndirect %s", i.op1.format(true))
default:
panic(fmt.Sprintf("BUG: %d", int(i.kind)))
@@ -370,7 +364,7 @@ func (i *instruction) Uses(regs *[]regalloc.VReg) []regalloc.VReg {
default:
panic(fmt.Sprintf("BUG: invalid operand: %s", i))
}
case useKindCallInd:
case useKindCallInd, useKindTailCallInd:
op := i.op1
switch op.kind {
case operandKindReg:
@@ -441,13 +435,16 @@ func (i *instruction) Uses(regs *[]regalloc.VReg) []regalloc.VReg {
func (i *instruction) AssignUse(index int, v regalloc.VReg) {
switch uk := useKinds[i.kind]; uk {
case useKindNone:
case useKindCallInd:
case useKindCallInd, useKindTailCallInd:
if index != 0 {
panic("BUG")
}
op := &i.op1
switch op.kind {
case operandKindReg:
if uk == useKindTailCallInd && v != r11VReg {
panic("BUG")
}
op.setReg(v)
case operandKindMem:
op.addressMode().assignUses(index, v)
@@ -651,26 +648,14 @@ func resetInstruction(i *instruction) {
*i = instruction{}
}
func setNext(i *instruction, next *instruction) {
i.next = next
}
func setPrev(i *instruction, prev *instruction) {
i.prev = prev
}
func asNop(i *instruction) {
i.kind = nop0
}
func (i *instruction) asNop0WithLabel(label backend.Label) *instruction { //nolint
func (i *instruction) asNop0WithLabel(label label) *instruction { //nolint
i.kind = nop0
i.u1 = uint64(label)
return i
}
func (i *instruction) nop0Label() backend.Label {
return backend.Label(i.u1)
func (i *instruction) nop0Label() label {
return label(i.u1)
}
type instructionKind byte
@@ -860,8 +845,14 @@ const (
// lockxadd is xadd https://www.felixcloutier.com/x86/xadd with a lock prefix.
lockxadd
// keepAlive is a meta instruction that uses one register and does nothing.
keepAlive
// nopUseReg is a meta instruction that uses one register and does nothing.
nopUseReg
// tailCall is a meta instruction that emits a tail call.
tailCall
// tailCallIndirect is a meta instruction that emits a tail call with an indirect call.
tailCallIndirect
instrMax
)
@@ -871,8 +862,8 @@ func (i *instruction) asMFence() *instruction {
return i
}
func (i *instruction) asKeepAlive(r regalloc.VReg) *instruction {
i.kind = keepAlive
func (i *instruction) asNopUseReg(r regalloc.VReg) *instruction {
i.kind = nopUseReg
i.op1 = newOperandReg(r)
return i
}
@@ -1104,6 +1095,10 @@ func (k instructionKind) String() string {
return "lockcmpxchg"
case lockxadd:
return "lockxadd"
case tailCall:
return "tailCall"
case tailCallIndirect:
return "tailCallIndirect"
default:
panic("BUG")
}
@@ -1161,7 +1156,7 @@ func (i *instruction) asJmp(target operand) *instruction {
return i
}
func (i *instruction) jmpLabel() backend.Label {
func (i *instruction) jmpLabel() label {
switch i.kind {
case jmp, jmpIf, lea, xmmUnaryRmR:
return i.op1.label()
@@ -1198,6 +1193,27 @@ func (i *instruction) asCallIndirect(ptr operand, abi *backend.FunctionABI) *ins
return i
}
func (i *instruction) asTailCallReturnCall(ref ssa.FuncRef, abi *backend.FunctionABI) *instruction {
i.kind = tailCall
i.u1 = uint64(ref)
if abi != nil {
i.u2 = abi.ABIInfoAsUint64()
}
return i
}
func (i *instruction) asTailCallReturnCallIndirect(ptr operand, abi *backend.FunctionABI) *instruction {
if ptr.kind != operandKindReg && ptr.kind != operandKindMem {
panic("BUG")
}
i.kind = tailCallIndirect
i.op1 = ptr
if abi != nil {
i.u2 = abi.ABIInfoAsUint64()
}
return i
}
func (i *instruction) asRet() *instruction {
i.kind = ret
return i
@@ -2366,7 +2382,9 @@ var defKinds = [instrMax]defKind{
lockcmpxchg: defKindNone,
lockxadd: defKindNone,
neg: defKindNone,
keepAlive: defKindNone,
nopUseReg: defKindNone,
tailCall: defKindCall,
tailCallIndirect: defKindCall,
}
// String implements fmt.Stringer.
@@ -2400,6 +2418,7 @@ const (
useKindBlendvpd
useKindCall
useKindCallInd
useKindTailCallInd
useKindFcvtToSintSequence
useKindFcvtToUintSequence
)
@@ -2449,7 +2468,9 @@ var useKinds = [instrMax]useKind{
lockcmpxchg: useKindRaxOp1RegOp2,
lockxadd: useKindOp1RegOp2,
neg: useKindOp1,
keepAlive: useKindOp1,
nopUseReg: useKindOp1,
tailCall: useKindCall,
tailCallIndirect: useKindTailCallInd,
}
func (u useKind) String() string {
@@ -2466,6 +2487,8 @@ func (u useKind) String() string {
return "call"
case useKindCallInd:
return "callInd"
case useKindTailCallInd:
return "tailCallInd"
default:
return "invalid"
}
@@ -11,7 +11,7 @@ import (
func (i *instruction) encode(c backend.Compiler) (needsLabelResolution bool) {
switch kind := i.kind; kind {
case nop0, sourceOffsetInfo, defineUninitializedReg, fcvtToSintSequence, fcvtToUintSequence, keepAlive:
case nop0, sourceOffsetInfo, defineUninitializedReg, fcvtToSintSequence, fcvtToUintSequence, nopUseReg:
case ret:
encodeRet(c)
case imm:
@@ -1211,7 +1211,7 @@ func (i *instruction) encode(c backend.Compiler) (needsLabelResolution bool) {
case call:
c.EmitByte(0xe8)
// Meaning that the call target is a function value, and requires relocation.
c.AddRelocationInfo(ssa.FuncRef(i.u1))
c.AddRelocationInfo(ssa.FuncRef(i.u1), false)
// Note that this is zero as a placeholder for the call target if it's a function value.
c.Emit4Bytes(uint32(i.u2))
@@ -1244,6 +1244,37 @@ func (i *instruction) encode(c backend.Compiler) (needsLabelResolution bool) {
panic("BUG: invalid operand kind")
}
case tailCall:
// Encode as jmp.
c.EmitByte(0xe9)
// Meaning that the call target is a function value, and requires relocation.
c.AddRelocationInfo(ssa.FuncRef(i.u1), true)
// Note that this is zero as a placeholder for the call target if it's a function value.
c.Emit4Bytes(uint32(i.u2))
case tailCallIndirect:
op := i.op1
const opcodeNum = 1
const opcode = 0xff
const regMemSubOpcode = 4
rex := rexInfo(0).clearW()
switch op.kind {
// Indirect tail calls always take a register as the target.
// Note: the register should be a callee-saved register (usually r11).
case operandKindReg:
dst := regEncodings[op.reg().RealReg()]
encodeRegReg(c,
legacyPrefixesNone,
opcode, opcodeNum,
regMemSubOpcode,
dst,
rex,
)
default:
panic("BUG: invalid operand kind")
}
case xchg:
src, dst := regEncodings[i.op1.reg().RealReg()], i.op2
size := i.u1
@@ -130,9 +130,9 @@ func (m *machine) lowerAddendsToAmode(x, y addend, offBase uint32) *amode {
}
}
func (m *machine) lowerAddend(x *backend.SSAValueDefinition) addend {
if x.IsFromBlockParam() {
return addend{x.BlkParamVReg, 0, 0}
func (m *machine) lowerAddend(x backend.SSAValueDefinition) addend {
if !x.IsFromInstr() {
return addend{m.c.VRegOf(x.V), 0, 0}
}
// Ensure the addend is not referenced in multiple places; we will discard nested Iadds.
op := m.c.MatchInstrOneOf(x, addendsMatchOpcodes[:])
@@ -16,18 +16,13 @@ import (
// NewBackend returns a new backend for arm64.
func NewBackend() backend.Machine {
ectx := backend.NewExecutableContextT[instruction](
resetInstruction,
setNext,
setPrev,
asNop,
)
return &machine{
ectx: ectx,
m := &machine{
cpuFeatures: platform.CpuFeatures,
regAlloc: regalloc.NewAllocator(regInfo),
regAlloc: regalloc.NewAllocator[*instruction, *labelPosition, *regAllocFn](regInfo),
spillSlots: map[regalloc.VRegID]int64{},
amodePool: wazevoapi.NewPool[amode](nil),
labelPositionPool: wazevoapi.NewIDedPool[labelPosition](resetLabelPosition),
instrPool: wazevoapi.NewPool[instruction](resetInstruction),
constSwizzleMaskConstIndex: -1,
constSqmulRoundSatIndex: -1,
constI8x16SHLMaskTableIndex: -1,
@@ -41,23 +36,46 @@ func NewBackend() backend.Machine {
constExtAddPairwiseI16x8uMask1Index: -1,
constExtAddPairwiseI16x8uMask2Index: -1,
}
m.regAllocFn.m = m
return m
}
type (
// machine implements backend.Machine for amd64.
machine struct {
c backend.Compiler
ectx *backend.ExecutableContextT[instruction]
stackBoundsCheckDisabled bool
instrPool wazevoapi.Pool[instruction]
amodePool wazevoapi.Pool[amode]
cpuFeatures platform.CpuFeatureFlags
regAlloc regalloc.Allocator
regAllocFn *backend.RegAllocFunction[*instruction, *machine]
regAlloc regalloc.Allocator[*instruction, *labelPosition, *regAllocFn]
regAllocFn regAllocFn
regAllocStarted bool
// labelPositionPool is the pool of labelPosition. The id is the label where
// if the label is less than the maxSSABlockID, it's the ssa.BasicBlockID.
labelPositionPool wazevoapi.IDedPool[labelPosition]
// nextLabel is the next label to be allocated. The first free label comes after maxSSABlockID
// so that we can have an identical label for the SSA block ID, which is useful for debugging.
nextLabel label
// rootInstr is the first instruction of the function.
rootInstr *instruction
// currentLabelPos is the currently-compiled ssa.BasicBlock's labelPosition.
currentLabelPos *labelPosition
// orderedSSABlockLabelPos is the ordered list of labelPosition in the generated code for each ssa.BasicBlock.
orderedSSABlockLabelPos []*labelPosition
// returnLabelPos is the labelPosition for the return block.
returnLabelPos labelPosition
// perBlockHead and perBlockEnd are the head and tail of the instruction list per currently-compiled ssa.BasicBlock.
perBlockHead, perBlockEnd *instruction
// pendingInstructions are the instructions which are not yet emitted into the instruction list.
pendingInstructions []*instruction
// maxSSABlockID is the maximum ssa.BasicBlockID in the current function.
maxSSABlockID label
spillSlotSize int64
spillSlots map[regalloc.VRegID]int64
currentABI *backend.FunctionABI
@@ -67,8 +85,11 @@ type (
labelResolutionPends []labelResolutionPend
// jmpTableTargets holds the labels of the jump table targets.
jmpTableTargets [][]uint32
consts []_const
// jmpTableTargetNext is the index to the jmpTableTargets slice to be used for the next jump table.
jmpTableTargetsNext int
consts []_const
constSwizzleMaskConstIndex, constSqmulRoundSatIndex,
constI8x16SHLMaskTableIndex, constI8x16LogicalSHRMaskTableIndex,
@@ -79,9 +100,10 @@ type (
}
_const struct {
lo, hi uint64
_var []byte
label *labelPosition
lo, hi uint64
_var []byte
label label
labelPos *labelPosition
}
labelResolutionPend struct {
@@ -90,22 +112,73 @@ type (
// imm32Offset is the offset of the last 4 bytes of the instruction.
imm32Offset int64
}
labelPosition = backend.LabelPosition[instruction]
)
func (m *machine) getOrAllocateConstLabel(i *int, _var []byte) backend.Label {
type (
// label represents a position in the generated code which is either
// a real instruction or the constant InstructionPool (e.g. jump tables).
//
// This is exactly the same as the traditional "label" in assembly code.
label uint32
// labelPosition represents the regions of the generated code which the label represents.
// This implements regalloc.Block.
labelPosition struct {
// sb is not nil if this corresponds to a ssa.BasicBlock.
sb ssa.BasicBlock
// cur is used to walk through the instructions in the block during the register allocation.
cur,
// begin and end are the first and last instructions of the block.
begin, end *instruction
// binaryOffset is the offset in the binary where the label is located.
binaryOffset int64
}
)
// String implements backend.Machine.
func (l label) String() string {
return fmt.Sprintf("L%d", l)
}
func resetLabelPosition(l *labelPosition) {
*l = labelPosition{}
}
const labelReturn = math.MaxUint32
func ssaBlockLabel(sb ssa.BasicBlock) label {
if sb.ReturnBlock() {
return labelReturn
}
return label(sb.ID())
}
// getOrAllocateSSABlockLabelPosition returns the labelPosition for the given basic block.
func (m *machine) getOrAllocateSSABlockLabelPosition(sb ssa.BasicBlock) *labelPosition {
if sb.ReturnBlock() {
m.returnLabelPos.sb = sb
return &m.returnLabelPos
}
l := ssaBlockLabel(sb)
pos := m.labelPositionPool.GetOrAllocate(int(l))
pos.sb = sb
return pos
}
func (m *machine) getOrAllocateConstLabel(i *int, _var []byte) label {
index := *i
if index == -1 {
label := m.allocateLabel()
l, pos := m.allocateLabel()
index = len(m.consts)
m.consts = append(m.consts, _const{
_var: _var,
label: label,
_var: _var,
label: l,
labelPos: pos,
})
*i = index
}
return m.consts[index].label.L
return m.consts[index].label
}
// Reset implements backend.Machine.
@@ -120,18 +193,20 @@ func (m *machine) Reset() {
}
m.stackBoundsCheckDisabled = false
m.ectx.Reset()
m.regAllocFn.Reset()
m.regAlloc.Reset()
m.labelPositionPool.Reset()
m.instrPool.Reset()
m.regAllocStarted = false
m.clobberedRegs = m.clobberedRegs[:0]
m.spillSlotSize = 0
m.maxRequiredStackSizeForCalls = 0
m.perBlockHead, m.perBlockEnd, m.rootInstr = nil, nil, nil
m.pendingInstructions = m.pendingInstructions[:0]
m.orderedSSABlockLabelPos = m.orderedSSABlockLabelPos[:0]
m.amodePool.Reset()
m.jmpTableTargets = m.jmpTableTargets[:0]
m.jmpTableTargetsNext = 0
m.constSwizzleMaskConstIndex = -1
m.constSqmulRoundSatIndex = -1
m.constI8x16SHLMaskTableIndex = -1
@@ -146,8 +221,63 @@ func (m *machine) Reset() {
m.constExtAddPairwiseI16x8uMask2Index = -1
}
// ExecutableContext implements backend.Machine.
func (m *machine) ExecutableContext() backend.ExecutableContext { return m.ectx }
// StartLoweringFunction implements backend.Machine StartLoweringFunction.
func (m *machine) StartLoweringFunction(maxBlockID ssa.BasicBlockID) {
m.maxSSABlockID = label(maxBlockID)
m.nextLabel = label(maxBlockID) + 1
}
// LinkAdjacentBlocks implements backend.Machine.
func (m *machine) LinkAdjacentBlocks(prev, next ssa.BasicBlock) {
prevPos, nextPos := m.getOrAllocateSSABlockLabelPosition(prev), m.getOrAllocateSSABlockLabelPosition(next)
prevPos.end.next = nextPos.begin
}
// StartBlock implements backend.Machine.
func (m *machine) StartBlock(blk ssa.BasicBlock) {
m.currentLabelPos = m.getOrAllocateSSABlockLabelPosition(blk)
labelPos := m.currentLabelPos
end := m.allocateNop()
m.perBlockHead, m.perBlockEnd = end, end
labelPos.begin, labelPos.end = end, end
m.orderedSSABlockLabelPos = append(m.orderedSSABlockLabelPos, labelPos)
}
// EndBlock implements ExecutableContext.
func (m *machine) EndBlock() {
// Insert nop0 as the head of the block for convenience to simplify the logic of inserting instructions.
m.insertAtPerBlockHead(m.allocateNop())
m.currentLabelPos.begin = m.perBlockHead
if m.currentLabelPos.sb.EntryBlock() {
m.rootInstr = m.perBlockHead
}
}
func (m *machine) insertAtPerBlockHead(i *instruction) {
if m.perBlockHead == nil {
m.perBlockHead = i
m.perBlockEnd = i
return
}
i.next = m.perBlockHead
m.perBlockHead.prev = i
m.perBlockHead = i
}
// FlushPendingInstructions implements backend.Machine.
func (m *machine) FlushPendingInstructions() {
l := len(m.pendingInstructions)
if l == 0 {
return
}
for i := l - 1; i >= 0; i-- { // reverse because we lower instructions in reverse order.
m.insertAtPerBlockHead(m.pendingInstructions[i])
}
m.pendingInstructions = m.pendingInstructions[:0]
}
// DisableStackCheck implements backend.Machine.
func (m *machine) DisableStackCheck() { m.stackBoundsCheckDisabled = true }
@@ -155,23 +285,17 @@ func (m *machine) DisableStackCheck() { m.stackBoundsCheckDisabled = true }
// SetCompiler implements backend.Machine.
func (m *machine) SetCompiler(c backend.Compiler) {
m.c = c
m.regAllocFn = backend.NewRegAllocFunction[*instruction, *machine](m, c.SSABuilder(), c)
m.regAllocFn.ssaB = c.SSABuilder()
}
// SetCurrentABI implements backend.Machine.
func (m *machine) SetCurrentABI(abi *backend.FunctionABI) {
m.currentABI = abi
}
func (m *machine) SetCurrentABI(abi *backend.FunctionABI) { m.currentABI = abi }
// RegAlloc implements backend.Machine.
func (m *machine) RegAlloc() {
rf := m.regAllocFn
for _, pos := range m.ectx.OrderedBlockLabels {
rf.AddBlock(pos.SB, pos.L, pos.Begin, pos.End)
}
m.regAllocStarted = true
m.regAlloc.DoAllocation(rf)
m.regAlloc.DoAllocation(&rf)
// Now that we know the final spill slot size, we must align spillSlotSize to 16 bytes.
m.spillSlotSize = (m.spillSlotSize + 15) &^ 15
}
@@ -184,49 +308,54 @@ func (m *machine) InsertReturn() {
// LowerSingleBranch implements backend.Machine.
func (m *machine) LowerSingleBranch(b *ssa.Instruction) {
ectx := m.ectx
switch b.Opcode() {
case ssa.OpcodeJump:
_, _, targetBlk := b.BranchData()
_, _, targetBlkID := b.BranchData()
if b.IsFallthroughJump() {
return
}
jmp := m.allocateInstr()
target := ectx.GetOrAllocateSSABlockLabel(targetBlk)
if target == backend.LabelReturn {
target := ssaBlockLabel(m.c.SSABuilder().BasicBlock(targetBlkID))
if target == labelReturn {
jmp.asRet()
} else {
jmp.asJmp(newOperandLabel(target))
}
m.insert(jmp)
case ssa.OpcodeBrTable:
index, target := b.BrTableData()
m.lowerBrTable(index, target)
index, targetBlkIDs := b.BrTableData()
m.lowerBrTable(index, targetBlkIDs)
default:
panic("BUG: unexpected branch opcode" + b.Opcode().String())
}
}
func (m *machine) addJmpTableTarget(targets []ssa.BasicBlock) (index int) {
// TODO: reuse the slice!
labels := make([]uint32, len(targets))
for j, target := range targets {
labels[j] = uint32(m.ectx.GetOrAllocateSSABlockLabel(target))
func (m *machine) addJmpTableTarget(targets ssa.Values) (index int) {
if m.jmpTableTargetsNext == len(m.jmpTableTargets) {
m.jmpTableTargets = append(m.jmpTableTargets, make([]uint32, 0, len(targets.View())))
}
index = m.jmpTableTargetsNext
m.jmpTableTargetsNext++
m.jmpTableTargets[index] = m.jmpTableTargets[index][:0]
for _, targetBlockID := range targets.View() {
target := m.c.SSABuilder().BasicBlock(ssa.BasicBlockID(targetBlockID))
m.jmpTableTargets[index] = append(m.jmpTableTargets[index], uint32(ssaBlockLabel(target)))
}
index = len(m.jmpTableTargets)
m.jmpTableTargets = append(m.jmpTableTargets, labels)
return
}
var condBranchMatches = [...]ssa.Opcode{ssa.OpcodeIcmp, ssa.OpcodeFcmp}
func (m *machine) lowerBrTable(index ssa.Value, targets []ssa.BasicBlock) {
func (m *machine) lowerBrTable(index ssa.Value, targets ssa.Values) {
_v := m.getOperand_Reg(m.c.ValueDefinition(index))
v := m.copyToTmp(_v.reg())
targetCount := len(targets.View())
// First, we need to do the bounds check.
maxIndex := m.c.AllocateVReg(ssa.TypeI32)
m.lowerIconst(maxIndex, uint64(len(targets)-1), false)
m.lowerIconst(maxIndex, uint64(targetCount-1), false)
cmp := m.allocateInstr().asCmpRmiR(true, newOperandReg(maxIndex), v, false)
m.insert(cmp)
@@ -255,23 +384,22 @@ func (m *machine) lowerBrTable(index ssa.Value, targets []ssa.BasicBlock) {
jmpTable := m.allocateInstr()
targetSliceIndex := m.addJmpTableTarget(targets)
jmpTable.asJmpTableSequence(targetSliceIndex, len(targets))
jmpTable.asJmpTableSequence(targetSliceIndex, targetCount)
m.insert(jmpTable)
}
// LowerConditionalBranch implements backend.Machine.
func (m *machine) LowerConditionalBranch(b *ssa.Instruction) {
exctx := m.ectx
cval, args, targetBlk := b.BranchData()
cval, args, targetBlkID := b.BranchData()
if len(args) > 0 {
panic(fmt.Sprintf(
"conditional branch shouldn't have args; likely a bug in critical edge splitting: from %s to %s",
exctx.CurrentSSABlk,
targetBlk,
m.currentLabelPos.sb,
targetBlkID,
))
}
target := exctx.GetOrAllocateSSABlockLabel(targetBlk)
target := ssaBlockLabel(m.c.SSABuilder().BasicBlock(targetBlkID))
cvalDef := m.c.ValueDefinition(cval)
switch m.c.MatchInstrOneOf(cvalDef, condBranchMatches[:]) {
@@ -939,7 +1067,7 @@ func (m *machine) LowerInstr(instr *ssa.Instruction) {
dst := m.c.VRegOf(instr.Return())
// At this point, the ptr is ensured to be aligned, so using a normal load is atomic.
// https://github.com/golang/go/blob/adead1a93f472affa97c494ef19f2f492ee6f34a/src/runtime/internal/atomic/atomic_amd64.go#L30
// https://github.com/golang/go/blob/go1.24.0/src/internal/runtime/atomic/atomic_amd64.go#L29
mem := newOperandMem(m.lowerToAddressMode(ptr, 0))
load := m.allocateInstr()
switch size {
@@ -981,6 +1109,9 @@ func (m *machine) LowerInstr(instr *ssa.Instruction) {
atomicOp, size := instr.AtomicRmwData()
m.lowerAtomicRmw(atomicOp, addr, val, size, instr.Return())
case ssa.OpcodeTailCallReturnCall, ssa.OpcodeTailCallReturnCallIndirect:
m.lowerTailCall(instr)
default:
panic("TODO: lowering " + op.String())
}
@@ -1051,7 +1182,7 @@ func (m *machine) lowerAtomicRmw(op ssa.AtomicRmwOp, addr, val ssa.Value, size u
}
// valCopied must be alive at the end of the loop.
m.insert(m.allocateInstr().asKeepAlive(valCopied))
m.insert(m.allocateInstr().asNopUseReg(valCopied))
// At this point, accumulator contains the result.
m.clearHigherBitsForAtomic(accumulator, size, ret.Type())
@@ -1272,14 +1403,14 @@ func (m *machine) lowerVconst(dst regalloc.VReg, lo, hi uint64) {
}
load := m.allocateInstr()
constLabel := m.allocateLabel()
m.consts = append(m.consts, _const{label: constLabel, lo: lo, hi: hi})
load.asXmmUnaryRmR(sseOpcodeMovdqu, newOperandMem(m.newAmodeRipRel(constLabel.L)), dst)
l, pos := m.allocateLabel()
m.consts = append(m.consts, _const{label: l, labelPos: pos, lo: lo, hi: hi})
load.asXmmUnaryRmR(sseOpcodeMovdqu, newOperandMem(m.newAmodeRipRel(l)), dst)
m.insert(load)
}
func (m *machine) lowerCtz(instr *ssa.Instruction) {
if m.cpuFeatures.HasExtra(platform.CpuExtraFeatureAmd64ABM) {
if m.cpuFeatures.Has(platform.CpuFeatureAmd64BMI1) {
m.lowerUnaryRmR(instr, unaryRmROpcodeTzcnt)
} else {
// On processors that do not support TZCNT, the BSF instruction is
@@ -1333,7 +1464,7 @@ func (m *machine) lowerCtz(instr *ssa.Instruction) {
}
func (m *machine) lowerClz(instr *ssa.Instruction) {
if m.cpuFeatures.HasExtra(platform.CpuExtraFeatureAmd64ABM) {
if m.cpuFeatures.Has(platform.CpuFeatureAmd64ABM) {
m.lowerUnaryRmR(instr, unaryRmROpcodeLzcnt)
} else {
// On processors that do not support LZCNT, we combine BSR (calculating
@@ -1473,21 +1604,24 @@ func (m *machine) lowerExitIfTrueWithCode(execCtx regalloc.VReg, cond ssa.Value,
jmpIf.asJmpIf(condFromSSAIntCmpCond(c).invert(), newOperandLabel(l))
}
func (m *machine) tryLowerBandToFlag(x, y *backend.SSAValueDefinition) (ok bool) {
var target *backend.SSAValueDefinition
func (m *machine) tryLowerBandToFlag(x, y backend.SSAValueDefinition) (ok bool) {
var target backend.SSAValueDefinition
var got bool
if x.IsFromInstr() && x.Instr.Constant() && x.Instr.ConstantVal() == 0 {
if m.c.MatchInstr(y, ssa.OpcodeBand) {
target = y
got = true
}
}
if y.IsFromInstr() && y.Instr.Constant() && y.Instr.ConstantVal() == 0 {
if m.c.MatchInstr(x, ssa.OpcodeBand) {
target = x
got = true
}
}
if target == nil {
if !got {
return false
}
@@ -1522,7 +1656,7 @@ func (m *machine) allocateExitInstructions(execCtx, exitCodeReg regalloc.VReg) (
return
}
func (m *machine) lowerExitWithCode(execCtx regalloc.VReg, code wazevoapi.ExitCode) (afterLabel backend.Label) {
func (m *machine) lowerExitWithCode(execCtx regalloc.VReg, code wazevoapi.ExitCode) (afterLabel label) {
exitCodeReg := rbpVReg
saveRsp, saveRbp, setExitCode := m.allocateExitInstructions(execCtx, exitCodeReg)
@@ -1754,14 +1888,49 @@ func (m *machine) lowerStore(si *ssa.Instruction) {
func (m *machine) lowerCall(si *ssa.Instruction) {
isDirectCall := si.Opcode() == ssa.OpcodeCall
indirectCalleePtr, directCallee, isMemmove, calleeABI, stackSlotSize := m.prepareCall(si, isDirectCall)
if isMemmove {
// Go's memmove *might* use all xmm0-xmm15, so we need to release them.
// https://github.com/golang/go/blob/go1.24.0/src/cmd/compile/abi-internal.md#architecture-specifics
// https://github.com/golang/go/blob/go1.24.0/src/runtime/memmove_amd64.s#L286-L301
for i := regalloc.RealReg(0); i < 16; i++ {
m.insert(m.allocateInstr().asDefineUninitializedReg(regInfo.RealRegToVReg[xmm0+i]))
}
// Since Go 1.24 it may also use DX, which is not reserved for the function call's 3 args.
// https://github.com/golang/go/blob/go1.24.0/src/runtime/memmove_amd64.s#L123
m.insert(m.allocateInstr().asDefineUninitializedReg(regInfo.RealRegToVReg[rdx]))
}
if isDirectCall {
call := m.allocateInstr().asCall(directCallee, calleeABI)
m.insert(call)
} else {
ptrOp := m.getOperand_Mem_Reg(m.c.ValueDefinition(indirectCalleePtr))
callInd := m.allocateInstr().asCallIndirect(ptrOp, calleeABI)
m.insert(callInd)
}
if isMemmove {
for i := regalloc.RealReg(0); i < 16; i++ {
m.insert(m.allocateInstr().asNopUseReg(regInfo.RealRegToVReg[xmm0+i]))
}
m.insert(m.allocateInstr().asNopUseReg(regInfo.RealRegToVReg[rdx]))
}
m.insertReturns(si, calleeABI, stackSlotSize)
}
func (m *machine) prepareCall(si *ssa.Instruction, isDirectCall bool) (ssa.Value, ssa.FuncRef, bool, *backend.FunctionABI, int64) {
var indirectCalleePtr ssa.Value
var directCallee ssa.FuncRef
var sigID ssa.SignatureID
var args []ssa.Value
var isMemmove bool
if isDirectCall {
directCallee, sigID, args = si.CallData()
} else {
indirectCalleePtr, sigID, args = si.CallIndirectData()
indirectCalleePtr, sigID, args, isMemmove = si.CallIndirectData()
}
calleeABI := m.c.GetFunctionABI(m.c.SSABuilder().ResolveSignature(sigID))
@@ -1778,16 +1947,10 @@ func (m *machine) lowerCall(si *ssa.Instruction) {
def := m.c.ValueDefinition(arg)
m.callerGenVRegToFunctionArg(calleeABI, i, reg, def, stackSlotSize)
}
return indirectCalleePtr, directCallee, isMemmove, calleeABI, stackSlotSize
}
if isDirectCall {
call := m.allocateInstr().asCall(directCallee, calleeABI)
m.insert(call)
} else {
ptrOp := m.getOperand_Mem_Reg(m.c.ValueDefinition(indirectCalleePtr))
callInd := m.allocateInstr().asCallIndirect(ptrOp, calleeABI)
m.insert(callInd)
}
func (m *machine) insertReturns(si *ssa.Instruction, calleeABI *backend.FunctionABI, stackSlotSize int64) {
var index int
r1, rs := si.Returns()
if r1.Valid() {
@@ -1801,11 +1964,48 @@ func (m *machine) lowerCall(si *ssa.Instruction) {
}
}
func (m *machine) lowerTailCall(si *ssa.Instruction) {
isDirectCall := si.Opcode() == ssa.OpcodeTailCallReturnCall
indirectCalleePtr, directCallee, isMemmove, calleeABI, stackSlotSize := m.prepareCall(si, isDirectCall)
if isMemmove {
panic("memmove not supported in tail calls")
}
isAllRegs := stackSlotSize == 0
switch {
case isDirectCall && isAllRegs:
call := m.allocateInstr().asTailCallReturnCall(directCallee, calleeABI)
m.insert(call)
case !isDirectCall && isAllRegs:
// In a tail call we insert the epilogue before the jump instruction,
// so an arbitrary register might be overwritten while restoring the stack.
// So, as compared to a regular indirect call, we ensure the pointer is stored
// in a caller-saved register (r11).
// For details, see internal/engine/RATIONALE.md
ptrOp := m.getOperand_Reg(m.c.ValueDefinition(indirectCalleePtr))
tmpJmp := r11VReg
m.InsertMove(tmpJmp, ptrOp.reg(), ssa.TypeI64)
callInd := m.allocateInstr().asTailCallReturnCallIndirect(newOperandReg(tmpJmp), calleeABI)
m.insert(callInd)
case isDirectCall && !isAllRegs:
call := m.allocateInstr().asCall(directCallee, calleeABI)
m.insert(call)
case !isDirectCall && !isAllRegs:
ptrOp := m.getOperand_Mem_Reg(m.c.ValueDefinition(indirectCalleePtr))
callInd := m.allocateInstr().asCallIndirect(ptrOp, calleeABI)
m.insert(callInd)
}
// If this is a proper tail call, returns will be cleared in the postRegAlloc phase.
m.insertReturns(si, calleeABI, stackSlotSize)
}
// callerGenVRegToFunctionArg is the opposite of GenFunctionArgToVReg, which is used to generate the
// caller side of the function call.
func (m *machine) callerGenVRegToFunctionArg(a *backend.FunctionABI, argIndex int, reg regalloc.VReg, def *backend.SSAValueDefinition, stackSlotSize int64) {
func (m *machine) callerGenVRegToFunctionArg(a *backend.FunctionABI, argIndex int, reg regalloc.VReg, def backend.SSAValueDefinition, stackSlotSize int64) {
arg := &a.Args[argIndex]
if def != nil && def.IsFromInstr() {
if def.IsFromInstr() {
// Constant instructions are inlined.
if inst := def.Instr; inst.Constant() {
m.insertLoadConstant(inst, reg)
@@ -1888,23 +2088,20 @@ func (m *machine) InsertMove(dst, src regalloc.VReg, typ ssa.Type) {
// Format implements backend.Machine.
func (m *machine) Format() string {
ectx := m.ectx
begins := map[*instruction]backend.Label{}
for l, pos := range ectx.LabelPositions {
begins[pos.Begin] = l
}
irBlocks := map[backend.Label]ssa.BasicBlockID{}
for i, l := range ectx.SsaBlockIDToLabels {
irBlocks[l] = ssa.BasicBlockID(i)
begins := map[*instruction]label{}
for l := label(0); l < m.nextLabel; l++ {
pos := m.labelPositionPool.Get(int(l))
if pos != nil {
begins[pos.begin] = l
}
}
var lines []string
for cur := ectx.RootInstr; cur != nil; cur = cur.next {
for cur := m.rootInstr; cur != nil; cur = cur.next {
if l, ok := begins[cur]; ok {
var labelStr string
if blkID, ok := irBlocks[l]; ok {
labelStr = fmt.Sprintf("%s (SSA Block: %s):", l, blkID)
if l <= m.maxSSABlockID {
labelStr = fmt.Sprintf("%s (SSA Block: blk%d):", l, l)
} else {
labelStr = fmt.Sprintf("%s:", l)
}
@@ -1917,9 +2114,9 @@ func (m *machine) Format() string {
}
for _, vc := range m.consts {
if vc._var == nil {
lines = append(lines, fmt.Sprintf("%s: const [%d %d]", vc.label.L, vc.lo, vc.hi))
lines = append(lines, fmt.Sprintf("%s: const [%d %d]", vc.label, vc.lo, vc.hi))
} else {
lines = append(lines, fmt.Sprintf("%s: const %#x", vc.label.L, vc._var))
lines = append(lines, fmt.Sprintf("%s: const %#x", vc.label, vc._var))
}
}
return "\n" + strings.Join(lines, "\n") + "\n"
@@ -1927,15 +2124,14 @@ func (m *machine) Format() string {
func (m *machine) encodeWithoutSSA(root *instruction) {
m.labelResolutionPends = m.labelResolutionPends[:0]
ectx := m.ectx
bufPtr := m.c.BufPtr()
for cur := root; cur != nil; cur = cur.next {
offset := int64(len(*bufPtr))
if cur.kind == nop0 {
l := cur.nop0Label()
if pos, ok := ectx.LabelPositions[l]; ok {
pos.BinaryOffset = offset
pos := m.labelPositionPool.Get(int(l))
if pos != nil {
pos.binaryOffset = offset
}
}
@@ -1952,7 +2148,7 @@ func (m *machine) encodeWithoutSSA(root *instruction) {
switch p.instr.kind {
case jmp, jmpIf, lea:
target := p.instr.jmpLabel()
targetOffset := ectx.LabelPositions[target].BinaryOffset
targetOffset := m.labelPositionPool.Get(int(target)).binaryOffset
imm32Offset := p.imm32Offset
jmpOffset := int32(targetOffset - (p.imm32Offset + 4)) // +4 because RIP points to the next instruction.
binary.LittleEndian.PutUint32((*bufPtr)[imm32Offset:], uint32(jmpOffset))
@@ -1964,33 +2160,33 @@ func (m *machine) encodeWithoutSSA(root *instruction) {
// Encode implements backend.Machine Encode.
func (m *machine) Encode(ctx context.Context) (err error) {
ectx := m.ectx
bufPtr := m.c.BufPtr()
var fn string
var fnIndex int
var labelToSSABlockID map[backend.Label]ssa.BasicBlockID
var labelPosToLabel map[*labelPosition]label
if wazevoapi.PerfMapEnabled {
fn = wazevoapi.GetCurrentFunctionName(ctx)
labelToSSABlockID = make(map[backend.Label]ssa.BasicBlockID)
for i, l := range ectx.SsaBlockIDToLabels {
labelToSSABlockID[l] = ssa.BasicBlockID(i)
labelPosToLabel = make(map[*labelPosition]label)
for i := 0; i <= m.labelPositionPool.MaxIDEncountered(); i++ {
pos := m.labelPositionPool.Get(i)
labelPosToLabel[pos] = label(i)
}
fnIndex = wazevoapi.GetCurrentFunctionIndex(ctx)
}
m.labelResolutionPends = m.labelResolutionPends[:0]
for _, pos := range ectx.OrderedBlockLabels {
for _, pos := range m.orderedSSABlockLabelPos {
offset := int64(len(*bufPtr))
pos.BinaryOffset = offset
for cur := pos.Begin; cur != pos.End.next; cur = cur.next {
pos.binaryOffset = offset
for cur := pos.begin; cur != pos.end.next; cur = cur.next {
offset := int64(len(*bufPtr))
switch cur.kind {
case nop0:
l := cur.nop0Label()
if pos, ok := ectx.LabelPositions[l]; ok {
pos.BinaryOffset = offset
if pos := m.labelPositionPool.Get(int(l)); pos != nil {
pos.binaryOffset = offset
}
case sourceOffsetInfo:
m.c.AddSourceOffsetInfo(offset, cur.sourceOffsetInfo())
@@ -2005,22 +2201,16 @@ func (m *machine) Encode(ctx context.Context) (err error) {
}
if wazevoapi.PerfMapEnabled {
l := pos.L
var labelStr string
if blkID, ok := labelToSSABlockID[l]; ok {
labelStr = fmt.Sprintf("%s::SSA_Block[%s]", l, blkID)
} else {
labelStr = l.String()
}
l := labelPosToLabel[pos]
size := int64(len(*bufPtr)) - offset
wazevoapi.PerfMap.AddModuleEntry(fnIndex, offset, uint64(size), fmt.Sprintf("%s:::::%s", fn, labelStr))
wazevoapi.PerfMap.AddModuleEntry(fnIndex, offset, uint64(size), fmt.Sprintf("%s:::::%s", fn, l))
}
}
for i := range m.consts {
offset := int64(len(*bufPtr))
vc := &m.consts[i]
vc.label.BinaryOffset = offset
vc.labelPos.binaryOffset = offset
if vc._var == nil {
lo, hi := vc.lo, vc.hi
m.c.Emit8Bytes(lo)
@@ -2038,7 +2228,7 @@ func (m *machine) Encode(ctx context.Context) (err error) {
switch p.instr.kind {
case jmp, jmpIf, lea, xmmUnaryRmR:
target := p.instr.jmpLabel()
targetOffset := ectx.LabelPositions[target].BinaryOffset
targetOffset := m.labelPositionPool.Get(int(target)).binaryOffset
imm32Offset := p.imm32Offset
jmpOffset := int32(targetOffset - (p.imm32Offset + 4)) // +4 because RIP points to the next instruction.
binary.LittleEndian.PutUint32(buf[imm32Offset:], uint32(jmpOffset))
@@ -2047,7 +2237,7 @@ func (m *machine) Encode(ctx context.Context) (err error) {
// Each entry is the offset from the beginning of the jmpTableIsland instruction in 8 bytes.
targets := m.jmpTableTargets[p.instr.u1]
for i, l := range targets {
targetOffset := ectx.LabelPositions[backend.Label(l)].BinaryOffset
targetOffset := m.labelPositionPool.Get(int(l)).binaryOffset
jmpOffset := targetOffset - tableBegin
binary.LittleEndian.PutUint64(buf[tableBegin+int64(i)*8:], uint64(jmpOffset))
}
@@ -2059,7 +2249,7 @@ func (m *machine) Encode(ctx context.Context) (err error) {
}
// ResolveRelocations implements backend.Machine.
func (m *machine) ResolveRelocations(refToBinaryOffset map[ssa.FuncRef]int, binary []byte, relocations []backend.RelocationInfo, _ []int) {
func (m *machine) ResolveRelocations(refToBinaryOffset []int, _ int, binary []byte, relocations []backend.RelocationInfo, _ []int) {
for _, r := range relocations {
offset := r.Offset
calleeFnOffset := refToBinaryOffset[r.FuncRef]
@@ -2076,7 +2266,7 @@ func (m *machine) ResolveRelocations(refToBinaryOffset map[ssa.FuncRef]int, bina
// CallTrampolineIslandInfo implements backend.Machine CallTrampolineIslandInfo.
func (m *machine) CallTrampolineIslandInfo(_ int) (_, _ int, _ error) { return }
func (m *machine) lowerIcmpToFlag(xd, yd *backend.SSAValueDefinition, _64 bool) {
func (m *machine) lowerIcmpToFlag(xd, yd backend.SSAValueDefinition, _64 bool) {
x := m.getOperand_Reg(xd)
y := m.getOperand_Mem_Imm32_Reg(yd)
cmp := m.allocateInstr().asCmpRmiR(true, y, x.reg(), _64)
@@ -2119,7 +2309,7 @@ func (m *machine) lowerFcmpToFlags(instr *ssa.Instruction) (f1, f2 cond, and boo
// allocateInstr allocates an instruction.
func (m *machine) allocateInstr() *instruction {
instr := m.ectx.InstructionPool.Allocate()
instr := m.instrPool.Allocate()
if !m.regAllocStarted {
instr.addedBeforeRegAlloc = true
}
@@ -2133,25 +2323,22 @@ func (m *machine) allocateNop() *instruction {
}
func (m *machine) insert(i *instruction) {
ectx := m.ectx
ectx.PendingInstructions = append(ectx.PendingInstructions, i)
m.pendingInstructions = append(m.pendingInstructions, i)
}
func (m *machine) allocateBrTarget() (nop *instruction, l backend.Label) { //nolint
pos := m.allocateLabel()
l = pos.L
func (m *machine) allocateBrTarget() (nop *instruction, l label) { //nolint
l, pos := m.allocateLabel()
nop = m.allocateInstr()
nop.asNop0WithLabel(l)
pos.Begin, pos.End = nop, nop
pos.begin, pos.end = nop, nop
return
}
func (m *machine) allocateLabel() *labelPosition {
ectx := m.ectx
l := ectx.AllocateLabel()
pos := ectx.AllocateLabelPosition(l)
ectx.LabelPositions[l] = pos
return pos
func (m *machine) allocateLabel() (label, *labelPosition) {
l := m.nextLabel
pos := m.labelPositionPool.GetOrAllocate(int(l))
m.nextLabel++
return l, pos
}
func (m *machine) getVRegSpillSlotOffsetFromSP(id regalloc.VRegID, size byte) int64 {
@@ -3165,22 +3352,22 @@ func (m *machine) lowerShuffle(x, y ssa.Value, lo, hi uint64, ret ssa.Value) {
}
}
xmaskLabel := m.allocateLabel()
m.consts = append(m.consts, _const{lo: xMask[0], hi: xMask[1], label: xmaskLabel})
ymaskLabel := m.allocateLabel()
m.consts = append(m.consts, _const{lo: yMask[0], hi: yMask[1], label: ymaskLabel})
xl, xmaskPos := m.allocateLabel()
m.consts = append(m.consts, _const{lo: xMask[0], hi: xMask[1], label: xl, labelPos: xmaskPos})
yl, ymaskPos := m.allocateLabel()
m.consts = append(m.consts, _const{lo: yMask[0], hi: yMask[1], label: yl, labelPos: ymaskPos})
xx, yy := m.getOperand_Reg(m.c.ValueDefinition(x)), m.getOperand_Reg(m.c.ValueDefinition(y))
tmpX, tmpY := m.copyToTmp(xx.reg()), m.copyToTmp(yy.reg())
// Apply mask to X.
tmp := m.c.AllocateVReg(ssa.TypeV128)
loadMaskLo := m.allocateInstr().asXmmUnaryRmR(sseOpcodeMovdqu, newOperandMem(m.newAmodeRipRel(xmaskLabel.L)), tmp)
loadMaskLo := m.allocateInstr().asXmmUnaryRmR(sseOpcodeMovdqu, newOperandMem(m.newAmodeRipRel(xl)), tmp)
m.insert(loadMaskLo)
m.insert(m.allocateInstr().asXmmRmR(sseOpcodePshufb, newOperandReg(tmp), tmpX))
// Apply mask to Y.
loadMaskHi := m.allocateInstr().asXmmUnaryRmR(sseOpcodeMovdqu, newOperandMem(m.newAmodeRipRel(ymaskLabel.L)), tmp)
loadMaskHi := m.allocateInstr().asXmmUnaryRmR(sseOpcodeMovdqu, newOperandMem(m.newAmodeRipRel(yl)), tmp)
m.insert(loadMaskHi)
m.insert(m.allocateInstr().asXmmRmR(sseOpcodePshufb, newOperandReg(tmp), tmpY))
@@ -12,7 +12,7 @@ func (m *machine) PostRegAlloc() {
}
func (m *machine) setupPrologue() {
cur := m.ectx.RootInstr
cur := m.rootInstr
prevInitInst := cur.next
// At this point, we have the stack layout as follows:
@@ -130,14 +130,13 @@ func (m *machine) setupPrologue() {
// 3. Inserts the dec/inc RSP instruction right before/after the call instruction.
// 4. Lowering that is supposed to be done after regalloc.
func (m *machine) postRegAlloc() {
ectx := m.ectx
for cur := ectx.RootInstr; cur != nil; cur = cur.next {
for cur := m.rootInstr; cur != nil; cur = cur.next {
switch k := cur.kind; k {
case ret:
m.setupEpilogueAfter(cur.prev)
continue
case fcvtToSintSequence, fcvtToUintSequence:
m.ectx.PendingInstructions = m.ectx.PendingInstructions[:0]
m.pendingInstructions = m.pendingInstructions[:0]
if k == fcvtToSintSequence {
m.lowerFcvtToSintSequenceAfterRegalloc(cur)
} else {
@@ -146,29 +145,29 @@ func (m *machine) postRegAlloc() {
prev := cur.prev
next := cur.next
cur := prev
for _, instr := range m.ectx.PendingInstructions {
for _, instr := range m.pendingInstructions {
cur = linkInstr(cur, instr)
}
linkInstr(cur, next)
continue
case xmmCMov:
m.ectx.PendingInstructions = m.ectx.PendingInstructions[:0]
m.pendingInstructions = m.pendingInstructions[:0]
m.lowerXmmCmovAfterRegAlloc(cur)
prev := cur.prev
next := cur.next
cur := prev
for _, instr := range m.ectx.PendingInstructions {
for _, instr := range m.pendingInstructions {
cur = linkInstr(cur, instr)
}
linkInstr(cur, next)
continue
case idivRemSequence:
m.ectx.PendingInstructions = m.ectx.PendingInstructions[:0]
m.pendingInstructions = m.pendingInstructions[:0]
m.lowerIDivRemSequenceAfterRegAlloc(cur)
prev := cur.prev
next := cur.next
cur := prev
for _, instr := range m.ectx.PendingInstructions {
for _, instr := range m.pendingInstructions {
cur = linkInstr(cur, instr)
}
linkInstr(cur, next)
@@ -189,6 +188,23 @@ func (m *machine) postRegAlloc() {
linkInstr(inc, next)
}
continue
case tailCall, tailCallIndirect:
// At this point, reg alloc is done, therefore we can safely insert dec RPS instruction
// right before the tail call (jump) instruction. If this is done before reg alloc, the stack slot
// can point to the wrong location and therefore results in a wrong value.
tailCall := cur
_, _, _, _, size := backend.ABIInfoFromUint64(tailCall.u2)
if size > 0 {
dec := m.allocateInstr().asAluRmiR(aluRmiROpcodeSub, newOperandImm32(size), rspVReg, true)
linkInstr(tailCall.prev, dec)
linkInstr(dec, tailCall)
}
// In a tail call, we insert the epilogue before the jump instruction.
m.setupEpilogueAfter(tailCall.prev)
// If this has been encoded as a proper tail call, we can remove the trailing instructions
// For details, see internal/engine/RATIONALE.md
m.removeUntilRet(cur.next)
continue
}
// Removes the redundant copy instruction.
@@ -279,6 +295,20 @@ func (m *machine) setupEpilogueAfter(cur *instruction) {
linkInstr(cur, prevNext)
}
// removeUntilRet removes the instructions starting from `cur` until the first `ret` instruction.
func (m *machine) removeUntilRet(cur *instruction) {
for ; cur != nil; cur = cur.next {
prev, next := cur.prev, cur.next
prev.next = next
if next != nil {
next.prev = prev
}
if cur.kind == ret {
return
}
}
}
func (m *machine) addRSP(offset int32, cur *instruction) *instruction {
if offset == 0 {
return cur
@@ -1,13 +1,226 @@
package amd64
import (
"github.com/tetratelabs/wazero/internal/engine/wazevo/backend"
"github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc"
"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
)
// InsertMoveBefore implements backend.RegAllocFunctionMachine.
func (m *machine) InsertMoveBefore(dst, src regalloc.VReg, instr *instruction) {
// regAllocFn implements regalloc.Function.
type regAllocFn struct {
ssaB ssa.Builder
m *machine
loopNestingForestRoots []ssa.BasicBlock
blockIter int
}
// PostOrderBlockIteratorBegin implements regalloc.Function.
func (f *regAllocFn) PostOrderBlockIteratorBegin() *labelPosition {
f.blockIter = len(f.m.orderedSSABlockLabelPos) - 1
return f.PostOrderBlockIteratorNext()
}
// PostOrderBlockIteratorNext implements regalloc.Function.
func (f *regAllocFn) PostOrderBlockIteratorNext() *labelPosition {
if f.blockIter < 0 {
return nil
}
b := f.m.orderedSSABlockLabelPos[f.blockIter]
f.blockIter--
return b
}
// ReversePostOrderBlockIteratorBegin implements regalloc.Function.
func (f *regAllocFn) ReversePostOrderBlockIteratorBegin() *labelPosition {
f.blockIter = 0
return f.ReversePostOrderBlockIteratorNext()
}
// ReversePostOrderBlockIteratorNext implements regalloc.Function.
func (f *regAllocFn) ReversePostOrderBlockIteratorNext() *labelPosition {
if f.blockIter >= len(f.m.orderedSSABlockLabelPos) {
return nil
}
b := f.m.orderedSSABlockLabelPos[f.blockIter]
f.blockIter++
return b
}
// ClobberedRegisters implements regalloc.Function.
func (f *regAllocFn) ClobberedRegisters(regs []regalloc.VReg) {
f.m.clobberedRegs = append(f.m.clobberedRegs[:0], regs...)
}
// LoopNestingForestRoots implements regalloc.Function.
func (f *regAllocFn) LoopNestingForestRoots() int {
f.loopNestingForestRoots = f.ssaB.LoopNestingForestRoots()
return len(f.loopNestingForestRoots)
}
// LoopNestingForestRoot implements regalloc.Function.
func (f *regAllocFn) LoopNestingForestRoot(i int) *labelPosition {
root := f.loopNestingForestRoots[i]
pos := f.m.getOrAllocateSSABlockLabelPosition(root)
return pos
}
// LowestCommonAncestor implements regalloc.Function.
func (f *regAllocFn) LowestCommonAncestor(blk1, blk2 *labelPosition) *labelPosition {
sb := f.ssaB.LowestCommonAncestor(blk1.sb, blk2.sb)
pos := f.m.getOrAllocateSSABlockLabelPosition(sb)
return pos
}
// Idom implements regalloc.Function.
func (f *regAllocFn) Idom(blk *labelPosition) *labelPosition {
sb := f.ssaB.Idom(blk.sb)
pos := f.m.getOrAllocateSSABlockLabelPosition(sb)
return pos
}
// SwapBefore implements regalloc.Function.
func (f *regAllocFn) SwapBefore(x1, x2, tmp regalloc.VReg, instr *instruction) {
f.m.swap(instr.prev, x1, x2, tmp)
}
// StoreRegisterBefore implements regalloc.Function.
func (f *regAllocFn) StoreRegisterBefore(v regalloc.VReg, instr *instruction) {
m := f.m
m.insertStoreRegisterAt(v, instr, false)
}
// StoreRegisterAfter implements regalloc.Function.
func (f *regAllocFn) StoreRegisterAfter(v regalloc.VReg, instr *instruction) {
m := f.m
m.insertStoreRegisterAt(v, instr, true)
}
// ReloadRegisterBefore implements regalloc.Function.
func (f *regAllocFn) ReloadRegisterBefore(v regalloc.VReg, instr *instruction) {
m := f.m
m.insertReloadRegisterAt(v, instr, false)
}
// ReloadRegisterAfter implements regalloc.Function.
func (f *regAllocFn) ReloadRegisterAfter(v regalloc.VReg, instr *instruction) {
m := f.m
m.insertReloadRegisterAt(v, instr, true)
}
// InsertMoveBefore implements regalloc.Function.
func (f *regAllocFn) InsertMoveBefore(dst, src regalloc.VReg, instr *instruction) {
f.m.insertMoveBefore(dst, src, instr)
}
// LoopNestingForestChild implements regalloc.Function.
func (f *regAllocFn) LoopNestingForestChild(pos *labelPosition, i int) *labelPosition {
childSB := pos.sb.LoopNestingForestChildren()[i]
return f.m.getOrAllocateSSABlockLabelPosition(childSB)
}
// Succ implements regalloc.Block.
func (f *regAllocFn) Succ(pos *labelPosition, i int) *labelPosition {
succSB := pos.sb.Succ(i)
if succSB.ReturnBlock() {
return nil
}
return f.m.getOrAllocateSSABlockLabelPosition(succSB)
}
// Pred implements regalloc.Block.
func (f *regAllocFn) Pred(pos *labelPosition, i int) *labelPosition {
predSB := pos.sb.Pred(i)
return f.m.getOrAllocateSSABlockLabelPosition(predSB)
}
// BlockParams implements regalloc.Function.
func (f *regAllocFn) BlockParams(pos *labelPosition, regs *[]regalloc.VReg) []regalloc.VReg {
c := f.m.c
*regs = (*regs)[:0]
for i := 0; i < pos.sb.Params(); i++ {
v := c.VRegOf(pos.sb.Param(i))
*regs = append(*regs, v)
}
return *regs
}
// ID implements regalloc.Block.
func (pos *labelPosition) ID() int32 {
return int32(pos.sb.ID())
}
// InstrIteratorBegin implements regalloc.Block.
func (pos *labelPosition) InstrIteratorBegin() *instruction {
ret := pos.begin
pos.cur = ret
return ret
}
// InstrIteratorNext implements regalloc.Block.
func (pos *labelPosition) InstrIteratorNext() *instruction {
for {
if pos.cur == pos.end {
return nil
}
instr := pos.cur.next
pos.cur = instr
if instr == nil {
return nil
} else if instr.addedBeforeRegAlloc {
// Only concerned about the instruction added before regalloc.
return instr
}
}
}
// InstrRevIteratorBegin implements regalloc.Block.
func (pos *labelPosition) InstrRevIteratorBegin() *instruction {
pos.cur = pos.end
return pos.cur
}
// InstrRevIteratorNext implements regalloc.Block.
func (pos *labelPosition) InstrRevIteratorNext() *instruction {
for {
if pos.cur == pos.begin {
return nil
}
instr := pos.cur.prev
pos.cur = instr
if instr == nil {
return nil
} else if instr.addedBeforeRegAlloc {
// Only concerned about the instruction added before regalloc.
return instr
}
}
}
// FirstInstr implements regalloc.Block.
func (pos *labelPosition) FirstInstr() *instruction { return pos.begin }
// LastInstrForInsertion implements regalloc.Block.
func (pos *labelPosition) LastInstrForInsertion() *instruction {
return lastInstrForInsertion(pos.begin, pos.end)
}
// Preds implements regalloc.Block.
func (pos *labelPosition) Preds() int { return pos.sb.Preds() }
// Entry implements regalloc.Block.
func (pos *labelPosition) Entry() bool { return pos.sb.EntryBlock() }
// Succs implements regalloc.Block.
func (pos *labelPosition) Succs() int { return pos.sb.Succs() }
// LoopHeader implements regalloc.Block.
func (pos *labelPosition) LoopHeader() bool { return pos.sb.LoopHeader() }
// LoopNestingForestChildren implements regalloc.Block.
func (pos *labelPosition) LoopNestingForestChildren() int {
return len(pos.sb.LoopNestingForestChildren())
}
func (m *machine) insertMoveBefore(dst, src regalloc.VReg, instr *instruction) {
typ := src.RegType()
if typ != dst.RegType() {
panic("BUG: src and dst must have the same type")
@@ -26,8 +239,7 @@ func (m *machine) InsertMoveBefore(dst, src regalloc.VReg, instr *instruction) {
linkInstr(cur, prevNext)
}
// InsertStoreRegisterAt implements backend.RegAllocFunctionMachine.
func (m *machine) InsertStoreRegisterAt(v regalloc.VReg, instr *instruction, after bool) *instruction {
func (m *machine) insertStoreRegisterAt(v regalloc.VReg, instr *instruction, after bool) *instruction {
if !v.IsRealReg() {
panic("BUG: VReg must be backed by real reg to be stored")
}
@@ -61,8 +273,7 @@ func (m *machine) InsertStoreRegisterAt(v regalloc.VReg, instr *instruction, aft
return linkInstr(cur, prevNext)
}
// InsertReloadRegisterAt implements backend.RegAllocFunctionMachine.
func (m *machine) InsertReloadRegisterAt(v regalloc.VReg, instr *instruction, after bool) *instruction {
func (m *machine) insertReloadRegisterAt(v regalloc.VReg, instr *instruction, after bool) *instruction {
if !v.IsRealReg() {
panic("BUG: VReg must be backed by real reg to be stored")
}
@@ -98,13 +309,7 @@ func (m *machine) InsertReloadRegisterAt(v regalloc.VReg, instr *instruction, af
return linkInstr(cur, prevNext)
}
// ClobberedRegisters implements backend.RegAllocFunctionMachine.
func (m *machine) ClobberedRegisters(regs []regalloc.VReg) {
m.clobberedRegs = append(m.clobberedRegs[:0], regs...)
}
// Swap implements backend.RegAllocFunctionMachine.
func (m *machine) Swap(cur *instruction, x1, x2, tmp regalloc.VReg) {
func (m *machine) swap(cur *instruction, x1, x2, tmp regalloc.VReg) {
if x1.RegType() == regalloc.RegTypeInt {
prevNext := cur.next
xc := m.allocateInstr().asXCHG(x1, newOperandReg(x2), 8)
@@ -113,25 +318,24 @@ func (m *machine) Swap(cur *instruction, x1, x2, tmp regalloc.VReg) {
} else {
if tmp.Valid() {
prevNext := cur.next
m.InsertMoveBefore(tmp, x1, prevNext)
m.InsertMoveBefore(x1, x2, prevNext)
m.InsertMoveBefore(x2, tmp, prevNext)
m.insertMoveBefore(tmp, x1, prevNext)
m.insertMoveBefore(x1, x2, prevNext)
m.insertMoveBefore(x2, tmp, prevNext)
} else {
prevNext := cur.next
r2 := x2.RealReg()
// Temporarily spill x1 to stack.
cur = m.InsertStoreRegisterAt(x1, cur, true).prev
cur = m.insertStoreRegisterAt(x1, cur, true).prev
// Then move x2 to x1.
cur = linkInstr(cur, m.allocateInstr().asXmmUnaryRmR(sseOpcodeMovdqa, newOperandReg(x2), x1))
linkInstr(cur, prevNext)
// Then reload the original value on x1 from stack to r2.
m.InsertReloadRegisterAt(x1.SetRealReg(r2), cur, true)
m.insertReloadRegisterAt(x1.SetRealReg(r2), cur, true)
}
}
}
// LastInstrForInsertion implements backend.RegAllocFunctionMachine.
func (m *machine) LastInstrForInsertion(begin, end *instruction) *instruction {
func lastInstrForInsertion(begin, end *instruction) *instruction {
cur := end
for cur.kind == nop0 {
cur = cur.prev
@@ -146,8 +350,3 @@ func (m *machine) LastInstrForInsertion(begin, end *instruction) *instruction {
return end
}
}
// SSABlockLabel implements backend.RegAllocFunctionMachine.
func (m *machine) SSABlockLabel(id ssa.BasicBlockID) backend.Label {
return m.ectx.SsaBlockIDToLabels[id]
}
@@ -127,7 +127,7 @@ func (m *machine) lowerSqmulRoundSat(x, y, ret ssa.Value) {
tmpX := m.copyToTmp(xx.reg())
m.insert(m.allocateInstr().asXmmRmR(sseOpcodePmulhrsw, yy, tmpX))
m.insert(m.allocateInstr().asXmmRmR(sseOpcodePcmpeqd, newOperandReg(tmpX), tmp))
m.insert(m.allocateInstr().asXmmRmR(sseOpcodePcmpeqw, newOperandReg(tmpX), tmp))
m.insert(m.allocateInstr().asXmmRmR(sseOpcodePxor, newOperandReg(tmp), tmpX))
m.copyTo(tmpX, m.c.VRegOf(ret))
@@ -59,7 +59,7 @@ func (o *operand) format(_64 bool) string {
case operandKindImm32:
return fmt.Sprintf("$%d", int32(o.imm32()))
case operandKindLabel:
return backend.Label(o.imm32()).String()
return label(o.imm32()).String()
default:
panic(fmt.Sprintf("BUG: invalid operand: %s", o.kind))
}
@@ -85,22 +85,22 @@ func (o *operand) imm32() uint32 {
return uint32(o.data)
}
func (o *operand) label() backend.Label {
func (o *operand) label() label {
switch o.kind {
case operandKindLabel:
return backend.Label(o.data)
return label(o.data)
case operandKindMem:
mem := o.addressMode()
if mem.kind() != amodeRipRel {
panic("BUG: invalid label")
}
return backend.Label(mem.imm32)
return label(mem.imm32)
default:
panic("BUG: invalid operand kind")
}
}
func newOperandLabel(label backend.Label) operand {
func newOperandLabel(label label) operand {
return operand{kind: operandKindLabel, data: uint64(label)}
}
@@ -221,7 +221,7 @@ func (m *machine) newAmodeRegRegShift(imm32 uint32, base, index regalloc.VReg, s
return ret
}
func (m *machine) newAmodeRipRel(label backend.Label) *amode {
func (m *machine) newAmodeRipRel(label label) *amode {
ret := m.amodePool.Allocate()
*ret = amode{kindWithShift: uint32(amodeRipRel), imm32: uint32(label)}
return ret
@@ -246,18 +246,18 @@ func (a *amode) String() string {
"%d(%s,%s,%d)",
int32(a.imm32), formatVRegSized(a.base, true), formatVRegSized(a.index, true), shift)
case amodeRipRel:
return fmt.Sprintf("%s(%%rip)", backend.Label(a.imm32))
return fmt.Sprintf("%s(%%rip)", label(a.imm32))
default:
panic("BUG: invalid amode kind")
}
}
func (m *machine) getOperand_Mem_Reg(def *backend.SSAValueDefinition) (op operand) {
if def.IsFromBlockParam() {
return newOperandReg(def.BlkParamVReg)
func (m *machine) getOperand_Mem_Reg(def backend.SSAValueDefinition) (op operand) {
if !def.IsFromInstr() {
return newOperandReg(m.c.VRegOf(def.V))
}
if def.SSAValue().Type() == ssa.TypeV128 {
if def.V.Type() == ssa.TypeV128 {
// SIMD instructions require strict memory alignment, so we don't support the memory operand for V128 at the moment.
return m.getOperand_Reg(def)
}
@@ -272,9 +272,9 @@ func (m *machine) getOperand_Mem_Reg(def *backend.SSAValueDefinition) (op operan
return m.getOperand_Reg(def)
}
func (m *machine) getOperand_Mem_Imm32_Reg(def *backend.SSAValueDefinition) (op operand) {
if def.IsFromBlockParam() {
return newOperandReg(def.BlkParamVReg)
func (m *machine) getOperand_Mem_Imm32_Reg(def backend.SSAValueDefinition) (op operand) {
if !def.IsFromInstr() {
return newOperandReg(m.c.VRegOf(def.V))
}
if m.c.MatchInstr(def, ssa.OpcodeLoad) {
@@ -287,9 +287,9 @@ func (m *machine) getOperand_Mem_Imm32_Reg(def *backend.SSAValueDefinition) (op
return m.getOperand_Imm32_Reg(def)
}
func (m *machine) getOperand_Imm32_Reg(def *backend.SSAValueDefinition) (op operand) {
if def.IsFromBlockParam() {
return newOperandReg(def.BlkParamVReg)
func (m *machine) getOperand_Imm32_Reg(def backend.SSAValueDefinition) (op operand) {
if !def.IsFromInstr() {
return newOperandReg(m.c.VRegOf(def.V))
}
instr := def.Instr
@@ -323,24 +323,14 @@ func asImm32(val uint64, allowSignExt bool) (uint32, bool) {
return u32val, true
}
func (m *machine) getOperand_Reg(def *backend.SSAValueDefinition) (op operand) {
func (m *machine) getOperand_Reg(def backend.SSAValueDefinition) (op operand) {
var v regalloc.VReg
if def.IsFromBlockParam() {
v = def.BlkParamVReg
if instr := def.Instr; instr != nil && instr.Constant() {
// We inline all the constant instructions so that we could reduce the register usage.
v = m.lowerConstant(instr)
instr.MarkLowered()
} else {
instr := def.Instr
if instr.Constant() {
// We inline all the constant instructions so that we could reduce the register usage.
v = m.lowerConstant(instr)
instr.MarkLowered()
} else {
if n := def.N; n == 0 {
v = m.c.VRegOf(instr.Return())
} else {
_, rs := instr.Returns()
v = m.c.VRegOf(rs[n-1])
}
}
v = m.c.VRegOf(def.V)
}
return newOperandReg(v)
}
@@ -1,11 +0,0 @@
//go:build !tinygo
package amd64
import "reflect"
// setSliceLimits sets both Cap and Len for the given reflected slice.
func setSliceLimits(s *reflect.SliceHeader, limit uintptr) {
s.Len = int(limit)
s.Cap = int(limit)
}
@@ -1,11 +0,0 @@
//go:build tinygo
package amd64
import "reflect"
// setSliceLimits sets both Cap and Len for the given reflected slice.
func setSliceLimits(s *reflect.SliceHeader, limit uintptr) {
s.Len = limit
s.Len = limit
}
@@ -9,12 +9,14 @@ import (
)
func stackView(rbp, top uintptr) []byte {
l := int(top - rbp)
var stackBuf []byte
{
// TODO: use unsafe.Slice after floor version is set to Go 1.20.
//nolint:staticcheck
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&stackBuf))
hdr.Data = rbp
setSliceLimits(hdr, top-rbp)
hdr.Len = l
hdr.Cap = l
}
return stackBuf
}
@@ -72,9 +74,9 @@ func GoCallStackView(stackPointerBeforeGoCall *uint64) []uint64 {
// | SizeInBytes |
// +-----------------+ <---- stackPointerBeforeGoCall
// (low address)
data := unsafe.Pointer(uintptr(unsafe.Pointer(stackPointerBeforeGoCall)) + 8)
data := unsafe.Add(unsafe.Pointer(stackPointerBeforeGoCall), 8)
size := *stackPointerBeforeGoCall / 8
return unsafe.Slice((*uint64)(data), int(size))
return unsafe.Slice((*uint64)(data), size)
}
func AdjustClonedStack(oldRsp, oldTop, rsp, rbp, top uintptr) {
@@ -7,7 +7,7 @@ import (
)
// References:
// * https://github.com/golang/go/blob/49d42128fd8594c172162961ead19ac95e247d24/src/cmd/compile/abi-internal.md#arm64-architecture
// * https://github.com/golang/go/blob/go1.24.0/src/cmd/compile/abi-internal.md#arm64-architecture
// * https://developer.arm.com/documentation/102374/0101/Procedure-Call-Standard
var (
@@ -101,13 +101,14 @@ func (m *machine) LowerParams(args []ssa.Value) {
bits := arg.Type.Bits()
// At this point of compilation, we don't yet know how much space exist below the return address.
// So we instruct the address mode to add the `argStackOffset` to the offset at the later phase of compilation.
amode := addressMode{imm: arg.Offset, rn: spVReg, kind: addressModeKindArgStackSpace}
amode := m.amodePool.Allocate()
*amode = addressMode{imm: arg.Offset, rn: spVReg, kind: addressModeKindArgStackSpace}
load := m.allocateInstr()
switch arg.Type {
case ssa.TypeI32, ssa.TypeI64:
load.asULoad(operandNR(reg), amode, bits)
load.asULoad(reg, amode, bits)
case ssa.TypeF32, ssa.TypeF64, ssa.TypeV128:
load.asFpuLoad(operandNR(reg), amode, bits)
load.asFpuLoad(reg, amode, bits)
default:
panic("BUG")
}
@@ -169,7 +170,8 @@ func (m *machine) LowerReturns(rets []ssa.Value) {
// At this point of compilation, we don't yet know how much space exist below the return address.
// So we instruct the address mode to add the `retStackOffset` to the offset at the later phase of compilation.
amode := addressMode{imm: r.Offset, rn: spVReg, kind: addressModeKindResultStackSpace}
amode := m.amodePool.Allocate()
*amode = addressMode{imm: r.Offset, rn: spVReg, kind: addressModeKindResultStackSpace}
store := m.allocateInstr()
store.asStore(operandNR(reg), amode, bits)
m.insert(store)
@@ -180,9 +182,9 @@ func (m *machine) LowerReturns(rets []ssa.Value) {
// callerGenVRegToFunctionArg is the opposite of GenFunctionArgToVReg, which is used to generate the
// caller side of the function call.
func (m *machine) callerGenVRegToFunctionArg(a *backend.FunctionABI, argIndex int, reg regalloc.VReg, def *backend.SSAValueDefinition, slotBegin int64) {
func (m *machine) callerGenVRegToFunctionArg(a *backend.FunctionABI, argIndex int, reg regalloc.VReg, def backend.SSAValueDefinition, slotBegin int64) {
arg := &a.Args[argIndex]
if def != nil && def.IsFromInstr() {
if def.IsFromInstr() {
// Constant instructions are inlined.
if inst := def.Instr; inst.Constant() {
val := inst.Return()
@@ -215,9 +217,9 @@ func (m *machine) callerGenFunctionReturnVReg(a *backend.FunctionABI, retIndex i
ldr := m.allocateInstr()
switch r.Type {
case ssa.TypeI32, ssa.TypeI64:
ldr.asULoad(operandNR(reg), amode, r.Type.Bits())
ldr.asULoad(reg, amode, r.Type.Bits())
case ssa.TypeF32, ssa.TypeF64, ssa.TypeV128:
ldr.asFpuLoad(operandNR(reg), amode, r.Type.Bits())
ldr.asFpuLoad(reg, amode, r.Type.Bits())
default:
panic("BUG")
}
@@ -225,25 +227,24 @@ func (m *machine) callerGenFunctionReturnVReg(a *backend.FunctionABI, retIndex i
}
}
func (m *machine) resolveAddressModeForOffsetAndInsert(cur *instruction, offset int64, dstBits byte, rn regalloc.VReg, allowTmpRegUse bool) (*instruction, addressMode) {
exct := m.executableContext
exct.PendingInstructions = exct.PendingInstructions[:0]
func (m *machine) resolveAddressModeForOffsetAndInsert(cur *instruction, offset int64, dstBits byte, rn regalloc.VReg, allowTmpRegUse bool) (*instruction, *addressMode) {
m.pendingInstructions = m.pendingInstructions[:0]
mode := m.resolveAddressModeForOffset(offset, dstBits, rn, allowTmpRegUse)
for _, instr := range exct.PendingInstructions {
for _, instr := range m.pendingInstructions {
cur = linkInstr(cur, instr)
}
return cur, mode
}
func (m *machine) resolveAddressModeForOffset(offset int64, dstBits byte, rn regalloc.VReg, allowTmpRegUse bool) addressMode {
func (m *machine) resolveAddressModeForOffset(offset int64, dstBits byte, rn regalloc.VReg, allowTmpRegUse bool) *addressMode {
if rn.RegType() != regalloc.RegTypeInt {
panic("BUG: rn should be a pointer: " + formatVRegSized(rn, 64))
}
var amode addressMode
amode := m.amodePool.Allocate()
if offsetFitsInAddressModeKindRegUnsignedImm12(dstBits, offset) {
amode = addressMode{kind: addressModeKindRegUnsignedImm12, rn: rn, imm: offset}
*amode = addressMode{kind: addressModeKindRegUnsignedImm12, rn: rn, imm: offset}
} else if offsetFitsInAddressModeKindRegSignedImm9(offset) {
amode = addressMode{kind: addressModeKindRegSignedImm9, rn: rn, imm: offset}
*amode = addressMode{kind: addressModeKindRegSignedImm9, rn: rn, imm: offset}
} else {
var indexReg regalloc.VReg
if allowTmpRegUse {
@@ -253,13 +254,30 @@ func (m *machine) resolveAddressModeForOffset(offset int64, dstBits byte, rn reg
indexReg = m.compiler.AllocateVReg(ssa.TypeI64)
m.lowerConstantI64(indexReg, offset)
}
amode = addressMode{kind: addressModeKindRegReg, rn: rn, rm: indexReg, extOp: extendOpUXTX /* indicates index rm is 64-bit */}
*amode = addressMode{kind: addressModeKindRegReg, rn: rn, rm: indexReg, extOp: extendOpUXTX /* indicates index rm is 64-bit */}
}
return amode
}
func (m *machine) lowerCall(si *ssa.Instruction) {
isDirectCall := si.Opcode() == ssa.OpcodeCall
indirectCalleePtr, directCallee, calleeABI, stackSlotSize := m.prepareCall(si, isDirectCall)
if isDirectCall {
call := m.allocateInstr()
call.asCall(directCallee, calleeABI)
m.insert(call)
} else {
ptr := m.compiler.VRegOf(indirectCalleePtr)
callInd := m.allocateInstr()
callInd.asCallIndirect(ptr, calleeABI)
m.insert(callInd)
}
m.insertReturns(si, calleeABI, stackSlotSize)
}
func (m *machine) prepareCall(si *ssa.Instruction, isDirectCall bool) (ssa.Value, ssa.FuncRef, *backend.FunctionABI, int64) {
var indirectCalleePtr ssa.Value
var directCallee ssa.FuncRef
var sigID ssa.SignatureID
@@ -267,7 +285,7 @@ func (m *machine) lowerCall(si *ssa.Instruction) {
if isDirectCall {
directCallee, sigID, args = si.CallData()
} else {
indirectCalleePtr, sigID, args = si.CallIndirectData()
indirectCalleePtr, sigID, args, _ /* on arm64, the calling convention is compatible with the Go runtime */ = si.CallIndirectData()
}
calleeABI := m.compiler.GetFunctionABI(m.compiler.SSABuilder().ResolveSignature(sigID))
@@ -281,18 +299,10 @@ func (m *machine) lowerCall(si *ssa.Instruction) {
def := m.compiler.ValueDefinition(arg)
m.callerGenVRegToFunctionArg(calleeABI, i, reg, def, stackSlotSize)
}
return indirectCalleePtr, directCallee, calleeABI, stackSlotSize
}
if isDirectCall {
call := m.allocateInstr()
call.asCall(directCallee, calleeABI)
m.insert(call)
} else {
ptr := m.compiler.VRegOf(indirectCalleePtr)
callInd := m.allocateInstr()
callInd.asCallIndirect(ptr, calleeABI)
m.insert(callInd)
}
func (m *machine) insertReturns(si *ssa.Instruction, calleeABI *backend.FunctionABI, stackSlotSize int64) {
var index int
r1, rs := si.Returns()
if r1.Valid() {
@@ -306,6 +316,40 @@ func (m *machine) lowerCall(si *ssa.Instruction) {
}
}
func (m *machine) lowerTailCall(si *ssa.Instruction) {
isDirectCall := si.Opcode() == ssa.OpcodeTailCallReturnCall
indirectCalleePtr, directCallee, calleeABI, stackSlotSize := m.prepareCall(si, isDirectCall)
// We currently support tail calls only when the args are passed via registers
// otherwise we fall back to a plain call.
// For details, see internal/engine/RATIONALE.md
isAllRegs := stackSlotSize == 0
switch {
case isDirectCall && isAllRegs:
tailJump := m.allocateInstr()
tailJump.asTailCall(directCallee, calleeABI)
m.insert(tailJump)
case !isDirectCall && isAllRegs:
ptr := m.compiler.VRegOf(indirectCalleePtr)
callInd := m.allocateInstr()
callInd.asTailCallIndirect(ptr, calleeABI)
m.insert(callInd)
case isDirectCall && !isAllRegs:
tailJump := m.allocateInstr()
tailJump.asCall(directCallee, calleeABI)
m.insert(tailJump)
case !isDirectCall && !isAllRegs:
ptr := m.compiler.VRegOf(indirectCalleePtr)
callInd := m.allocateInstr()
callInd.asCallIndirect(ptr, calleeABI)
m.insert(callInd)
}
// If this is a proper tail call, returns will be cleared in the postRegAlloc phase.
m.insertReturns(si, calleeABI, stackSlotSize)
}
func (m *machine) insertAddOrSubStackPointer(rd regalloc.VReg, diff int64, add bool) {
if imm12Operand, ok := asImm12Operand(uint64(diff)); ok {
alu := m.allocateInstr()
@@ -315,7 +359,7 @@ func (m *machine) insertAddOrSubStackPointer(rd regalloc.VReg, diff int64, add b
} else {
ao = aluOpSub
}
alu.asALU(ao, operandNR(rd), operandNR(spVReg), imm12Operand, true)
alu.asALU(ao, rd, operandNR(spVReg), imm12Operand, true)
m.insert(alu)
} else {
m.lowerConstantI64(tmpRegVReg, diff)
@@ -326,7 +370,7 @@ func (m *machine) insertAddOrSubStackPointer(rd regalloc.VReg, diff int64, add b
} else {
ao = aluOpSub
}
alu.asALU(ao, operandNR(rd), operandNR(spVReg), operandNR(tmpRegVReg), true)
alu.asALU(ao, rd, operandNR(spVReg), operandNR(tmpRegVReg), true)
m.insert(alu)
}
}
@@ -59,25 +59,26 @@ func (m *machine) goEntryPreamblePassArg(cur *instruction, paramSlicePtr regallo
} else {
postIndexImm = 8
}
loadMode := addressMode{kind: addressModeKindPostIndex, rn: paramSlicePtr, imm: postIndexImm}
loadMode := m.amodePool.Allocate()
*loadMode = addressMode{kind: addressModeKindPostIndex, rn: paramSlicePtr, imm: postIndexImm}
instr := m.allocateInstr()
switch typ {
case ssa.TypeI32:
instr.asULoad(loadTargetReg, loadMode, 32)
instr.asULoad(loadTargetReg.reg(), loadMode, 32)
case ssa.TypeI64:
instr.asULoad(loadTargetReg, loadMode, 64)
instr.asULoad(loadTargetReg.reg(), loadMode, 64)
case ssa.TypeF32:
instr.asFpuLoad(loadTargetReg, loadMode, 32)
instr.asFpuLoad(loadTargetReg.reg(), loadMode, 32)
case ssa.TypeF64:
instr.asFpuLoad(loadTargetReg, loadMode, 64)
instr.asFpuLoad(loadTargetReg.reg(), loadMode, 64)
case ssa.TypeV128:
instr.asFpuLoad(loadTargetReg, loadMode, 128)
instr.asFpuLoad(loadTargetReg.reg(), loadMode, 128)
}
cur = linkInstr(cur, instr)
if isStackArg {
var storeMode addressMode
var storeMode *addressMode
cur, storeMode = m.resolveAddressModeForOffsetAndInsert(cur, argStartOffsetFromSP+arg.Offset, bits, spVReg, true)
toStack := m.allocateInstr()
toStack.asStore(loadTargetReg, storeMode, bits)
@@ -113,21 +114,22 @@ func (m *machine) goEntryPreamblePassResult(cur *instruction, resultSlicePtr reg
}
if isStackArg {
var loadMode addressMode
var loadMode *addressMode
cur, loadMode = m.resolveAddressModeForOffsetAndInsert(cur, resultStartOffsetFromSP+result.Offset, bits, spVReg, true)
toReg := m.allocateInstr()
switch typ {
case ssa.TypeI32, ssa.TypeI64:
toReg.asULoad(storeTargetReg, loadMode, bits)
toReg.asULoad(storeTargetReg.reg(), loadMode, bits)
case ssa.TypeF32, ssa.TypeF64, ssa.TypeV128:
toReg.asFpuLoad(storeTargetReg, loadMode, bits)
toReg.asFpuLoad(storeTargetReg.reg(), loadMode, bits)
default:
panic("TODO?")
}
cur = linkInstr(cur, toReg)
}
mode := addressMode{kind: addressModeKindPostIndex, rn: resultSlicePtr, imm: postIndexImm}
mode := m.amodePool.Allocate()
*mode = addressMode{kind: addressModeKindPostIndex, rn: resultSlicePtr, imm: postIndexImm}
instr := m.allocateInstr()
instr.asStore(storeTargetReg, mode, bits)
cur = linkInstr(cur, instr)
@@ -214,11 +216,12 @@ func (m *machine) move64(dst, src regalloc.VReg, prev *instruction) *instruction
func (m *machine) loadOrStoreAtExecutionContext(d regalloc.VReg, offset wazevoapi.Offset, store bool, prev *instruction) *instruction {
instr := m.allocateInstr()
mode := addressMode{kind: addressModeKindRegUnsignedImm12, rn: savedExecutionContextPtr, imm: offset.I64()}
mode := m.amodePool.Allocate()
*mode = addressMode{kind: addressModeKindRegUnsignedImm12, rn: savedExecutionContextPtr, imm: offset.I64()}
if store {
instr.asStore(operandNR(d), mode, 64)
} else {
instr.asULoad(operandNR(d), mode, 64)
instr.asULoad(d, mode, 64)
}
return linkInstr(prev, instr)
}
@@ -14,7 +14,6 @@ var calleeSavedRegistersSorted = []regalloc.VReg{
// CompileGoFunctionTrampoline implements backend.Machine.
func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *ssa.Signature, needModuleContextPtr bool) []byte {
exct := m.executableContext
argBegin := 1 // Skips exec context by default.
if needModuleContextPtr {
argBegin++
@@ -26,7 +25,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
cur := m.allocateInstr()
cur.asNop0()
exct.RootInstr = cur
m.rootInstr = cur
// Execution context is always the first argument.
execCtrPtr := x0VReg
@@ -87,7 +86,8 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
// Module context is always the second argument.
moduleCtrPtr := x1VReg
store := m.allocateInstr()
amode := addressMode{kind: addressModeKindRegUnsignedImm12, rn: execCtrPtr, imm: offset}
amode := m.amodePool.Allocate()
*amode = addressMode{kind: addressModeKindRegUnsignedImm12, rn: execCtrPtr, imm: offset}
store.asStore(operandNR(moduleCtrPtr), amode, 64)
cur = linkInstr(cur, store)
}
@@ -120,11 +120,9 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
} else {
sizeInBits = 64
}
store.asStore(operandNR(v),
addressMode{
kind: addressModeKindPostIndex,
rn: arg0ret0AddrReg, imm: int64(sizeInBits / 8),
}, sizeInBits)
amode := m.amodePool.Allocate()
*amode = addressMode{kind: addressModeKindPostIndex, rn: arg0ret0AddrReg, imm: int64(sizeInBits / 8)}
store.asStore(operandNR(v), amode, sizeInBits)
cur = linkInstr(cur, store)
}
@@ -139,7 +137,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
frameSizeReg = xzrVReg
sliceSizeReg = xzrVReg
}
_amode := addressModePreOrPostIndex(spVReg, -16, true)
_amode := addressModePreOrPostIndex(m, spVReg, -16, true)
storeP := m.allocateInstr()
storeP.asStorePair64(frameSizeReg, sliceSizeReg, _amode)
cur = linkInstr(cur, storeP)
@@ -165,8 +163,8 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
cur = m.addsAddOrSubStackPointer(cur, spVReg, frameInfoSize+goCallStackSize, true)
ldr := m.allocateInstr()
// And load the return address.
ldr.asULoad(operandNR(lrVReg),
addressModePreOrPostIndex(spVReg, 16 /* stack pointer must be 16-byte aligned. */, false /* increment after loads */), 64)
amode := addressModePreOrPostIndex(m, spVReg, 16 /* stack pointer must be 16-byte aligned. */, false /* increment after loads */)
ldr.asULoad(lrVReg, amode, 64)
cur = linkInstr(cur, ldr)
originalRet0Reg := x17VReg // Caller save, so we can use it for whatever we want.
@@ -183,23 +181,24 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
r := &abi.Rets[i]
if r.Kind == backend.ABIArgKindReg {
loadIntoReg := m.allocateInstr()
mode := addressMode{kind: addressModeKindPostIndex, rn: arg0ret0AddrReg}
mode := m.amodePool.Allocate()
*mode = addressMode{kind: addressModeKindPostIndex, rn: arg0ret0AddrReg}
switch r.Type {
case ssa.TypeI32:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoReg.asULoad(operandNR(r.Reg), mode, 32)
loadIntoReg.asULoad(r.Reg, mode, 32)
case ssa.TypeI64:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoReg.asULoad(operandNR(r.Reg), mode, 64)
loadIntoReg.asULoad(r.Reg, mode, 64)
case ssa.TypeF32:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoReg.asFpuLoad(operandNR(r.Reg), mode, 32)
loadIntoReg.asFpuLoad(r.Reg, mode, 32)
case ssa.TypeF64:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoReg.asFpuLoad(operandNR(r.Reg), mode, 64)
loadIntoReg.asFpuLoad(r.Reg, mode, 64)
case ssa.TypeV128:
mode.imm = 16
loadIntoReg.asFpuLoad(operandNR(r.Reg), mode, 128)
loadIntoReg.asFpuLoad(r.Reg, mode, 128)
default:
panic("TODO")
}
@@ -208,28 +207,29 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
// First we need to load the value to a temporary just like ^^.
intTmp, floatTmp := x11VReg, v11VReg
loadIntoTmpReg := m.allocateInstr()
mode := addressMode{kind: addressModeKindPostIndex, rn: arg0ret0AddrReg}
mode := m.amodePool.Allocate()
*mode = addressMode{kind: addressModeKindPostIndex, rn: arg0ret0AddrReg}
var resultReg regalloc.VReg
switch r.Type {
case ssa.TypeI32:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoTmpReg.asULoad(operandNR(intTmp), mode, 32)
loadIntoTmpReg.asULoad(intTmp, mode, 32)
resultReg = intTmp
case ssa.TypeI64:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoTmpReg.asULoad(operandNR(intTmp), mode, 64)
loadIntoTmpReg.asULoad(intTmp, mode, 64)
resultReg = intTmp
case ssa.TypeF32:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoTmpReg.asFpuLoad(operandNR(floatTmp), mode, 32)
loadIntoTmpReg.asFpuLoad(floatTmp, mode, 32)
resultReg = floatTmp
case ssa.TypeF64:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
loadIntoTmpReg.asFpuLoad(operandNR(floatTmp), mode, 64)
loadIntoTmpReg.asFpuLoad(floatTmp, mode, 64)
resultReg = floatTmp
case ssa.TypeV128:
mode.imm = 16
loadIntoTmpReg.asFpuLoad(operandNR(floatTmp), mode, 128)
loadIntoTmpReg.asFpuLoad(floatTmp, mode, 128)
resultReg = floatTmp
default:
panic("TODO")
@@ -243,7 +243,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
ret.asRet()
linkInstr(cur, ret)
m.encode(m.executableContext.RootInstr)
m.encode(m.rootInstr)
return m.compiler.Buf()
}
@@ -258,12 +258,13 @@ func (m *machine) saveRegistersInExecutionContext(cur *instruction, regs []regal
case regalloc.RegTypeFloat:
sizeInBits = 128
}
store.asStore(operandNR(v),
addressMode{
kind: addressModeKindRegUnsignedImm12,
// Execution context is always the first argument.
rn: x0VReg, imm: offset,
}, sizeInBits)
mode := m.amodePool.Allocate()
*mode = addressMode{
kind: addressModeKindRegUnsignedImm12,
// Execution context is always the first argument.
rn: x0VReg, imm: offset,
}
store.asStore(operandNR(v), mode, sizeInBits)
store.prev = cur
cur.next = store
cur = store
@@ -276,7 +277,7 @@ func (m *machine) restoreRegistersInExecutionContext(cur *instruction, regs []re
offset := wazevoapi.ExecutionContextOffsetSavedRegistersBegin.I64()
for _, v := range regs {
load := m.allocateInstr()
var as func(dst operand, amode addressMode, sizeInBits byte)
var as func(dst regalloc.VReg, amode *addressMode, sizeInBits byte)
var sizeInBits byte
switch v.RegType() {
case regalloc.RegTypeInt:
@@ -286,12 +287,13 @@ func (m *machine) restoreRegistersInExecutionContext(cur *instruction, regs []re
as = load.asFpuLoad
sizeInBits = 128
}
as(operandNR(v),
addressMode{
kind: addressModeKindRegUnsignedImm12,
// Execution context is always the first argument.
rn: x0VReg, imm: offset,
}, sizeInBits)
mode := m.amodePool.Allocate()
*mode = addressMode{
kind: addressModeKindRegUnsignedImm12,
// Execution context is always the first argument.
rn: x0VReg, imm: offset,
}
as(v, mode, sizeInBits)
cur = linkInstr(cur, load)
offset += 16 // Imm12 must be aligned 16 for vector regs, so we unconditionally load regs at the offset of multiple of 16.
}
@@ -299,20 +301,18 @@ func (m *machine) restoreRegistersInExecutionContext(cur *instruction, regs []re
}
func (m *machine) lowerConstantI64AndInsert(cur *instruction, dst regalloc.VReg, v int64) *instruction {
exct := m.executableContext
exct.PendingInstructions = exct.PendingInstructions[:0]
m.pendingInstructions = m.pendingInstructions[:0]
m.lowerConstantI64(dst, v)
for _, instr := range exct.PendingInstructions {
for _, instr := range m.pendingInstructions {
cur = linkInstr(cur, instr)
}
return cur
}
func (m *machine) lowerConstantI32AndInsert(cur *instruction, dst regalloc.VReg, v int32) *instruction {
exct := m.executableContext
exct.PendingInstructions = exct.PendingInstructions[:0]
m.pendingInstructions = m.pendingInstructions[:0]
m.lowerConstantI32(dst, v)
for _, instr := range exct.PendingInstructions {
for _, instr := range m.pendingInstructions {
cur = linkInstr(cur, instr)
}
return cur
@@ -324,11 +324,9 @@ func (m *machine) setExitCode(cur *instruction, execCtr regalloc.VReg, exitCode
// Set the exit status on the execution context.
setExistStatus := m.allocateInstr()
setExistStatus.asStore(operandNR(constReg),
addressMode{
kind: addressModeKindRegUnsignedImm12,
rn: execCtr, imm: wazevoapi.ExecutionContextOffsetExitCodeOffset.I64(),
}, 32)
mode := m.amodePool.Allocate()
*mode = addressMode{kind: addressModeKindRegUnsignedImm12, rn: execCtr, imm: wazevoapi.ExecutionContextOffsetExitCodeOffset.I64()}
setExistStatus.asStore(operandNR(constReg), mode, 32)
cur = linkInstr(cur, setExistStatus)
return cur
}
@@ -340,12 +338,13 @@ func (m *machine) storeReturnAddressAndExit(cur *instruction) *instruction {
cur = linkInstr(cur, adr)
storeReturnAddr := m.allocateInstr()
storeReturnAddr.asStore(operandNR(tmpRegVReg),
addressMode{
kind: addressModeKindRegUnsignedImm12,
// Execution context is always the first argument.
rn: x0VReg, imm: wazevoapi.ExecutionContextOffsetGoCallReturnAddress.I64(),
}, 64)
mode := m.amodePool.Allocate()
*mode = addressMode{
kind: addressModeKindRegUnsignedImm12,
// Execution context is always the first argument.
rn: x0VReg, imm: wazevoapi.ExecutionContextOffsetGoCallReturnAddress.I64(),
}
storeReturnAddr.asStore(operandNR(tmpRegVReg), mode, 64)
cur = linkInstr(cur, storeReturnAddr)
// Exit the execution.
@@ -364,11 +363,12 @@ func (m *machine) saveCurrentStackPointer(cur *instruction, execCtr regalloc.VRe
cur = linkInstr(cur, movSp)
strSp := m.allocateInstr()
strSp.asStore(operandNR(tmpRegVReg),
addressMode{
kind: addressModeKindRegUnsignedImm12,
rn: execCtr, imm: wazevoapi.ExecutionContextOffsetStackPointerBeforeGoCall.I64(),
}, 64)
mode := m.amodePool.Allocate()
*mode = addressMode{
kind: addressModeKindRegUnsignedImm12,
rn: execCtr, imm: wazevoapi.ExecutionContextOffsetStackPointerBeforeGoCall.I64(),
}
strSp.asStore(operandNR(tmpRegVReg), mode, 64)
cur = linkInstr(cur, strSp)
return cur
}
@@ -376,27 +376,28 @@ func (m *machine) saveCurrentStackPointer(cur *instruction, execCtr regalloc.VRe
func (m *machine) goFunctionCallLoadStackArg(cur *instruction, originalArg0Reg regalloc.VReg, arg *backend.ABIArg, intVReg, floatVReg regalloc.VReg) (*instruction, regalloc.VReg) {
load := m.allocateInstr()
var result regalloc.VReg
mode := addressMode{kind: addressModeKindPostIndex, rn: originalArg0Reg}
mode := m.amodePool.Allocate()
*mode = addressMode{kind: addressModeKindPostIndex, rn: originalArg0Reg}
switch arg.Type {
case ssa.TypeI32:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
load.asULoad(operandNR(intVReg), mode, 32)
load.asULoad(intVReg, mode, 32)
result = intVReg
case ssa.TypeI64:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
load.asULoad(operandNR(intVReg), mode, 64)
load.asULoad(intVReg, mode, 64)
result = intVReg
case ssa.TypeF32:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
load.asFpuLoad(operandNR(floatVReg), mode, 32)
load.asFpuLoad(floatVReg, mode, 32)
result = floatVReg
case ssa.TypeF64:
mode.imm = 8 // We use uint64 for all basic types, except SIMD v128.
load.asFpuLoad(operandNR(floatVReg), mode, 64)
load.asFpuLoad(floatVReg, mode, 64)
result = floatVReg
case ssa.TypeV128:
mode.imm = 16
load.asFpuLoad(operandNR(floatVReg), mode, 128)
load.asFpuLoad(floatVReg, mode, 128)
result = floatVReg
default:
panic("TODO")
@@ -408,7 +409,8 @@ func (m *machine) goFunctionCallLoadStackArg(cur *instruction, originalArg0Reg r
func (m *machine) goFunctionCallStoreStackResult(cur *instruction, originalRet0Reg regalloc.VReg, result *backend.ABIArg, resultVReg regalloc.VReg) *instruction {
store := m.allocateInstr()
mode := addressMode{kind: addressModeKindPostIndex, rn: originalRet0Reg}
mode := m.amodePool.Allocate()
*mode = addressMode{kind: addressModeKindPostIndex, rn: originalRet0Reg}
var sizeInBits byte
switch result.Type {
case ssa.TypeI32, ssa.TypeF32:
Loaded 100 of 282 files, more files were not shown because too many files have changed in this diff. Show more