mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-10 17:09:17 -10:00
Compare commits
14
Commits
master
...
wasm/wasi-ext
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f255c7010 | ||
|
|
9f742634f5 | ||
|
|
1aafbc7793 | ||
|
|
f2952d7702 | ||
|
|
4dffd11945 | ||
|
|
979d5b8809 | ||
|
|
3f1103ef1b | ||
|
|
ec4d561762 | ||
|
|
6767493833 | ||
|
|
3b7168ac23 | ||
|
|
83ea905ce3 | ||
|
|
42e51c4843 | ||
|
|
ccb5dd5ebb | ||
|
|
cf6eeaf142 |
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+1724
-275
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -36,6 +36,7 @@ package-lock.json
|
||||
.vagrant
|
||||
|
||||
zig-cache/
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
|
||||
.DS_Store
|
||||
|
||||
+107
-103
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
# wazero: the zero dependency WebAssembly runtime for Go developers
|
||||
|
||||
[](https://github.com/tetratelabs/wazero/actions/workflows/spectest.yaml) [](https://pkg.go.dev/github.com/tetratelabs/wazero) [](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://pkg.go.dev/github.com/tetratelabs/wazero) [](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
-13
@@ -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 {
|
||||
|
||||
+19
@@ -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
@@ -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
|
||||
+19
@@ -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)
|
||||
}
|
||||
-6
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
//go:build !plan9 && !aix
|
||||
//go:build !(plan9 || aix)
|
||||
|
||||
package sys
|
||||
|
||||
|
||||
Generated
Vendored
+13
-38
@@ -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)
|
||||
|
||||
+16
@@ -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
|
||||
}
|
||||
+36
@@ -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
@@ -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.
|
||||
|
||||
Generated
Vendored
+2
-2
@@ -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
|
||||
|
||||
Generated
Vendored
+2
-2
@@ -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
|
||||
|
||||
Generated
Vendored
+2
-2
@@ -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
|
||||
|
||||
Generated
Vendored
+34
-32
@@ -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.
|
||||
//
|
||||
|
||||
Generated
Vendored
+24
-16
@@ -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) {
|
||||
|
||||
Generated
Vendored
+2
-2
@@ -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")
|
||||
Generated
Vendored
+1
-1
@@ -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
|
||||
|
||||
Generated
Vendored
+3
-3
@@ -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
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+934
-684
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+4
-6
@@ -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
|
||||
Generated
Vendored
+981
-607
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+2978
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+200
-142
@@ -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
@@ -1 +0,0 @@
|
||||
wazevo is the new optimizing compiler engine which only supports arm64. AMD64 is under active development.
|
||||
Generated
Vendored
+22
-37
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+19
-19
@@ -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.
|
||||
|
||||
Generated
Vendored
-219
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+1
-1
@@ -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}
|
||||
|
||||
implant/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/abi_go_call.go
Generated
Vendored
+4
-7
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+63
-40
@@ -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"
|
||||
}
|
||||
|
||||
Generated
Vendored
+33
-2
@@ -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
|
||||
|
||||
Generated
Vendored
+3
-3
@@ -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[:])
|
||||
|
||||
Generated
Vendored
+330
-143
@@ -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))
|
||||
|
||||
|
||||
Generated
Vendored
+39
-9
@@ -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
|
||||
|
||||
Generated
Vendored
+225
-26
@@ -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]
|
||||
}
|
||||
implant/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/amd64/machine_vec.go
Generated
Vendored
+1
-1
@@ -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))
|
||||
|
||||
Generated
Vendored
+23
-33
@@ -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)
|
||||
}
|
||||
Generated
Vendored
-11
@@ -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)
|
||||
}
|
||||
Generated
Vendored
-11
@@ -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
|
||||
}
|
||||
Generated
Vendored
+6
-4
@@ -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) {
|
||||
|
||||
Generated
Vendored
+76
-32
@@ -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)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+16
-13
@@ -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)
|
||||
}
|
||||
|
||||
implant/vendor/github.com/tetratelabs/wazero/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go
Generated
Vendored
+68
-66
@@ -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
Reference in new issue
Block a user