mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
The generated wasi_snapshot_preview1 stubs gate outbound sockets with a dial hook, but it was called with only the resolved IP and port. An embedder whose policy is host-scoped (e.g. "host X only on port 443") cannot enforce that from the IP alone: the IP cannot be tied back to the name the guest resolved, so the port ends up checked against a host-agnostic union. Sock_getaddrinfo already resolves the name, so record IP->host there and hand the host to the dial hook at Sock_connect. The hook signature (and SetDialHook) gains a host parameter; it is "" for a literal-IP dial with no preceding lookup. This lets the host policy match host+port jointly, matching what an HTTP-layer policy can already do. Co-authored-by: Claude Opus 4.8 <[email protected]>
1839 lines
58 KiB
Go
1839 lines
58 KiB
Go
package codegen
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestWasip1TemplateParses confirms the embedded source still parses as
|
|
// valid Go. The list of required method names must match every method
|
|
// the codegen interface adapter expects to bind.
|
|
func TestWasip1TemplateParses(t *testing.T) {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "wasip1native.go", wasip1NativeRaw, parser.ParseComments)
|
|
if err != nil {
|
|
t.Fatalf("wasip1NativeRaw does not parse: %v", err)
|
|
}
|
|
required := []string{
|
|
"Args_get", "Args_sizes_get",
|
|
"Environ_get", "Environ_sizes_get",
|
|
"Clock_res_get", "Clock_time_get",
|
|
"Fd_close", "Fd_fdstat_get", "Fd_fdstat_set_flags", "Fd_fdstat_set_rights",
|
|
"Fd_filestat_get", "Fd_filestat_set_size", "Fd_filestat_set_times",
|
|
"Fd_prestat_get", "Fd_prestat_dir_name",
|
|
"Fd_read", "Fd_pread", "Fd_pwrite", "Fd_seek", "Fd_tell", "Fd_write",
|
|
"Fd_sync", "Fd_datasync", "Fd_advise", "Fd_allocate", "Fd_renumber",
|
|
"Fd_readdir",
|
|
"Path_open", "Path_create_directory", "Path_unlink_file",
|
|
"Path_remove_directory", "Path_rename",
|
|
"Path_filestat_get", "Path_filestat_set_times",
|
|
"Path_link", "Path_symlink", "Path_readlink",
|
|
"Random_get",
|
|
"Sched_yield", "Poll_oneoff",
|
|
"Proc_exit", "Proc_raise",
|
|
"Sock_accept", "Sock_recv", "Sock_send", "Sock_shutdown",
|
|
}
|
|
have := map[string]bool{}
|
|
for _, d := range f.Decls {
|
|
if fn, ok := d.(*ast.FuncDecl); ok {
|
|
have[fn.Name.Name] = true
|
|
}
|
|
}
|
|
missing := []string{}
|
|
for _, r := range required {
|
|
if !have[r] {
|
|
missing = append(missing, r)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
t.Fatalf("missing WasiStubs methods in template: %s", strings.Join(missing, ", "))
|
|
}
|
|
}
|
|
|
|
// newTestStubs returns a WasiStubs pre-configured for tests: fresh fd
|
|
// table, no stdio file handles, args/env initialised, monoStart fixed
|
|
// at the current time. The host preopen root is set to dir so every
|
|
// path-based syscall lands under t.TempDir().
|
|
func newTestStubs(t *testing.T, dir string) (*WasiStubs, *Module) {
|
|
t.Helper()
|
|
w := &WasiStubs{
|
|
fdTable: map[int32]*wasiOpen{},
|
|
nextFD: 4,
|
|
args: []string{"prog", "a", "b"},
|
|
env: []string{"FOO=bar", "BAZ=qux"},
|
|
monoStart: time.Now(),
|
|
preopenDir: dir,
|
|
fsys: osFS{root: dir},
|
|
}
|
|
m := &Module{memory: make([]byte, 1<<16)}
|
|
return w, m
|
|
}
|
|
|
|
// readU32 / readU64 / writeRel are tiny helpers for the test cases.
|
|
func readU32(b []byte, off int) uint32 { return binary.LittleEndian.Uint32(b[off:]) }
|
|
func readU64(b []byte, off int) uint64 { return binary.LittleEndian.Uint64(b[off:]) }
|
|
|
|
// putPath writes the bytes of p at offset off in mem and returns
|
|
// (off, len(p)).
|
|
func putPath(m *Module, off int32, p string) (int32, int32) {
|
|
copy(m.memory[off:], p)
|
|
return off, int32(len(p))
|
|
}
|
|
|
|
// makeIovec writes a 1-entry iovec at iovOff pointing to (bufOff, bufLen).
|
|
func makeIovec(m *Module, iovOff, bufOff, bufLen int32) {
|
|
binary.LittleEndian.PutUint32(m.memory[iovOff:], uint32(bufOff))
|
|
binary.LittleEndian.PutUint32(m.memory[iovOff+4:], uint32(bufLen))
|
|
}
|
|
|
|
// ─── Tier 1: file primitives ────────────────────────────────────────
|
|
|
|
// TestWasi_FSAccessHook verifies the host-controlled filesystem policy:
|
|
// the hook is consulted on open/create/unlink with the guest path and a
|
|
// write flag, and a false return surfaces as EACCES without touching the
|
|
// disk. This is the control point an embedding wires its whitelist into.
|
|
func TestWasi_FSAccessHook(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "secret"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "ok"), []byte("y"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w, m := newTestStubs(t, dir)
|
|
|
|
type call struct {
|
|
path string
|
|
write bool
|
|
}
|
|
var seen []call
|
|
w.SetFSAccessHook(func(path string, write bool) bool {
|
|
seen = append(seen, call{path, write})
|
|
if path == "secret" {
|
|
return false // deny reads/writes of "secret"
|
|
}
|
|
if write && path == "readonly" {
|
|
return false // deny writes to "readonly", allow reads
|
|
}
|
|
return true
|
|
})
|
|
|
|
rightsRead := int64(1 << 1)
|
|
rightsWrite := int64(1 << 6)
|
|
|
|
// Reading "secret" is denied.
|
|
off, ln := putPath(m, 128, "secret")
|
|
if rc := w.Path_open(m, 3, 0, off, ln, 0, rightsRead, 0, 0, 200); rc != _wasiEACCES {
|
|
t.Fatalf("open secret(read): want EACCES(%d), got %d", _wasiEACCES, rc)
|
|
}
|
|
// Reading "ok" is allowed.
|
|
off, ln = putPath(m, 128, "ok")
|
|
if rc := w.Path_open(m, 3, 0, off, ln, 0, rightsRead, 0, 0, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("open ok(read): want SUCCESS, got %d", rc)
|
|
}
|
|
// Writing "readonly" (with O_CREAT) is denied; the file must not appear.
|
|
off, ln = putPath(m, 128, "readonly")
|
|
if rc := w.Path_open(m, 3, 0, off, ln, 0x1, rightsWrite, 0, 0, 200); rc != _wasiEACCES {
|
|
t.Fatalf("open readonly(write): want EACCES, got %d", rc)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "readonly")); !os.IsNotExist(err) {
|
|
t.Fatalf("denied write must not create the file; stat err=%v", err)
|
|
}
|
|
// The write flag must be reported correctly.
|
|
if len(seen) < 3 || seen[2].path != "readonly" || !seen[2].write {
|
|
t.Fatalf("hook write-flag not propagated: %+v", seen)
|
|
}
|
|
// Clearing the hook restores unrestricted access.
|
|
w.SetFSAccessHook(nil)
|
|
off, ln = putPath(m, 128, "secret")
|
|
if rc := w.Path_open(m, 3, 0, off, ln, 0, rightsRead, 0, 0, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("open secret after clearing hook: want SUCCESS, got %d", rc)
|
|
}
|
|
}
|
|
|
|
// TestWasi_NetAccessHook verifies the network policy hook gates the
|
|
// accept/recv/send surface before any socket work happens (a denied op
|
|
// returns EACCES; an allowed op falls through to the normal ENOTSOCK for
|
|
// these fd-less test calls, proving the hook let it pass).
|
|
func TestWasi_NetAccessHook(t *testing.T) {
|
|
_, m := newTestStubs(t, t.TempDir())
|
|
w := &WasiStubs{fdTable: map[int32]*wasiOpen{}, nextFD: 4}
|
|
|
|
denied := map[string]bool{"send": true}
|
|
w.SetNetAccessHook(func(op string) bool { return !denied[op] })
|
|
|
|
// send is denied → EACCES.
|
|
if rc := w.Sock_send(m, 4, 0, 0, 0, 200); rc != _wasiEACCES {
|
|
t.Fatalf("Sock_send denied: want EACCES, got %d", rc)
|
|
}
|
|
// recv is allowed by the hook → passes the hook, then ENOTSOCK (fd 4
|
|
// has no conn).
|
|
if rc := w.Sock_recv(m, 4, 0, 0, 0, 200, 204); rc != _wasiENOTSOCK {
|
|
t.Fatalf("Sock_recv allowed: want ENOTSOCK, got %d", rc)
|
|
}
|
|
if rc := w.Sock_accept(m, 4, 0, 200); rc != _wasiENOTSOCK {
|
|
t.Fatalf("Sock_accept allowed: want ENOTSOCK, got %d", rc)
|
|
}
|
|
// Clearing the hook makes send fall through to ENOTSOCK too.
|
|
w.SetNetAccessHook(nil)
|
|
if rc := w.Sock_send(m, 4, 0, 0, 0, 200); rc != _wasiENOTSOCK {
|
|
t.Fatalf("Sock_send after clearing hook: want ENOTSOCK, got %d", rc)
|
|
}
|
|
}
|
|
|
|
// TestWasi_SockSocketConnect verifies the outbound-socket host imports:
|
|
// Sock_socket allocates a socket fd, Sock_connect dials a live listener and
|
|
// attaches the conn (so Sock_send/Sock_recv work), the dial whitelist denies
|
|
// blocked destinations with EACCES, and a refused destination maps to
|
|
// ECONNREFUSED.
|
|
func TestWasi_SockSocketConnect(t *testing.T) {
|
|
// Echo server on loopback.
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := ln.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
|
t.Errorf("listener close: %v", err)
|
|
}
|
|
}()
|
|
go func() {
|
|
for {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func(c net.Conn) {
|
|
defer func() {
|
|
if cerr := c.Close(); cerr != nil && !errors.Is(cerr, net.ErrClosed) {
|
|
t.Errorf("conn close: %v", cerr)
|
|
}
|
|
}()
|
|
if _, cerr := io.Copy(c, c); cerr != nil && !errors.Is(cerr, net.ErrClosed) {
|
|
t.Errorf("echo copy: %v", cerr)
|
|
}
|
|
}(c)
|
|
}
|
|
}()
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
|
|
// 127.0.0.1 in sockaddr_in.sin_addr.s_addr (network order) read as a
|
|
// little-endian uint32 is 0x0100007f.
|
|
const loopbackBE = int32(0x0100007f)
|
|
|
|
_, m := newTestStubs(t, t.TempDir())
|
|
w := &WasiStubs{fdTable: map[int32]*wasiOpen{}, nextFD: 4}
|
|
|
|
// Sock_socket allocates a fresh fd (family is validated by the bridge
|
|
// shim, not here).
|
|
fd := w.Sock_socket(m, 2 /*AF_INET*/, 1 /*SOCK_STREAM*/)
|
|
if fd < 4 {
|
|
t.Fatalf("Sock_socket returned bad fd %d", fd)
|
|
}
|
|
|
|
// Resolve the loopback host first so Sock_connect can hand the dial hook the
|
|
// host name that was looked up for this IP (host+port matched jointly).
|
|
hoff, hln := putPath(m, 512, "127.0.0.1")
|
|
if rc := w.Sock_getaddrinfo(m, hoff, hln, 600); rc != _wasiESUCCESS {
|
|
t.Fatalf("getaddrinfo for dial-hook host: rc=%d", rc)
|
|
}
|
|
// Whitelist denies first.
|
|
var dialed []string
|
|
w.SetDialHook(func(network, host, ip string, p int) bool {
|
|
dialed = append(dialed, fmt.Sprintf("%s/%s/%s:%d", network, host, ip, p))
|
|
return false
|
|
})
|
|
if rc := w.Sock_connect(m, fd, loopbackBE, int32(port)); rc != -_wasiEACCES {
|
|
t.Fatalf("denied connect: want -EACCES, got %d", rc)
|
|
}
|
|
if len(dialed) != 1 || dialed[0] != fmt.Sprintf("tcp/127.0.0.1/127.0.0.1:%d", port) {
|
|
t.Fatalf("dial hook saw %v, want tcp/127.0.0.1/127.0.0.1:%d", dialed, port)
|
|
}
|
|
|
|
// Allow → connect succeeds and attaches the conn.
|
|
w.SetDialHook(func(network, host, ip string, p int) bool { return true })
|
|
if rc := w.Sock_connect(m, fd, loopbackBE, int32(port)); rc != _wasiESUCCESS {
|
|
t.Fatalf("allowed connect: want SUCCESS, got %d", rc)
|
|
}
|
|
if op := w.fdTable[fd]; op == nil || op.conn == nil {
|
|
t.Fatalf("connect did not attach a conn to fd %d", fd)
|
|
}
|
|
|
|
// Connecting an already-connected socket → EISCONN.
|
|
if rc := w.Sock_connect(m, fd, loopbackBE, int32(port)); rc != -_wasiEISCONN {
|
|
t.Fatalf("re-connect: want -EISCONN, got %d", rc)
|
|
}
|
|
|
|
// Connect on a non-socket fd → ENOTSOCK.
|
|
if rc := w.Sock_connect(m, 999, loopbackBE, int32(port)); rc != -_wasiENOTSOCK {
|
|
t.Fatalf("connect bad fd: want -ENOTSOCK, got %d", rc)
|
|
}
|
|
|
|
// Refused destination (closed listener) → ECONNREFUSED.
|
|
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rp := ln2.Addr().(*net.TCPAddr).Port
|
|
if err := ln2.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fd2 := w.Sock_socket(m, 2, 1)
|
|
if rc := w.Sock_connect(m, fd2, loopbackBE, int32(rp)); rc != -_wasiECONNREFUSED {
|
|
t.Fatalf("refused connect: want -ECONNREFUSED, got %d", rc)
|
|
}
|
|
}
|
|
|
|
// TestWasi_SockGetaddrinfo covers the host name-resolution import: numeric
|
|
// IPv4 passes through without a lookup, an empty host yields INADDR_ANY, and
|
|
// the resolve whitelist hook denies a blocked host with EACCES.
|
|
func TestWasi_SockGetaddrinfo(t *testing.T) {
|
|
_, m := newTestStubs(t, t.TempDir())
|
|
w := &WasiStubs{fdTable: map[int32]*wasiOpen{}, nextFD: 4}
|
|
|
|
// Numeric IPv4 → fast path, 4-byte network-order result at outPtr.
|
|
off, ln := putPath(m, 128, "203.0.113.7")
|
|
if rc := w.Sock_getaddrinfo(m, off, ln, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("getaddrinfo numeric rc=%d", rc)
|
|
}
|
|
if got := []byte{m.memory[200], m.memory[201], m.memory[202], m.memory[203]}; got[0] != 203 || got[1] != 0 || got[2] != 113 || got[3] != 7 {
|
|
t.Fatalf("numeric IP bytes = %v, want [203 0 113 7]", got)
|
|
}
|
|
|
|
// Empty host → INADDR_ANY (0).
|
|
if rc := w.Sock_getaddrinfo(m, 0, 0, 300); rc != _wasiESUCCESS {
|
|
t.Fatalf("getaddrinfo empty rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 300); n != 0 {
|
|
t.Fatalf("empty host should be INADDR_ANY(0), got %#x", n)
|
|
}
|
|
|
|
// Resolve whitelist denies → EACCES, hook sees the host.
|
|
var seen []string
|
|
w.SetResolveHook(func(host string) bool { seen = append(seen, host); return host != "blocked.invalid" })
|
|
off, ln = putPath(m, 128, "blocked.invalid")
|
|
if rc := w.Sock_getaddrinfo(m, off, ln, 200); rc != -_wasiEACCES {
|
|
t.Fatalf("denied resolve: want -EACCES, got %d", rc)
|
|
}
|
|
if len(seen) != 1 || seen[0] != "blocked.invalid" {
|
|
t.Fatalf("resolve hook saw %v", seen)
|
|
}
|
|
// An allowed numeric host still bypasses the resolver (and the hook
|
|
// is consulted but permits it).
|
|
off, ln = putPath(m, 128, "198.51.100.9")
|
|
if rc := w.Sock_getaddrinfo(m, off, ln, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("allowed numeric resolve rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
// TestWasi_SetEnvArgs covers the sandbox env/argv overrides: SetEnv / SetArgs
|
|
// replace what the guest sees via environ_*/args_*, so an embedding does not
|
|
// leak the host process environment.
|
|
func TestWasi_SetEnvArgs(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
|
|
w.SetEnv([]string{"APP_MODE=sandbox", "X=1"})
|
|
if rc := w.Environ_sizes_get(m, 0, 4); rc != _wasiESUCCESS {
|
|
t.Fatalf("Environ_sizes_get rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 0); n != 2 {
|
|
t.Fatalf("envc after SetEnv = %d, want 2", n)
|
|
}
|
|
if rc := w.Environ_get(m, 16, 64); rc != _wasiESUCCESS {
|
|
t.Fatalf("Environ_get rc=%d", rc)
|
|
}
|
|
// First env string pointer is at 16; it should read "APP_MODE=sandbox".
|
|
p := int(readU32(m.memory, 16))
|
|
got := readCString(m.memory, p)
|
|
if got != "APP_MODE=sandbox" {
|
|
t.Fatalf("env[0] = %q, want APP_MODE=sandbox", got)
|
|
}
|
|
|
|
// Empty env → zero count (no host leak).
|
|
w.SetEnv(nil)
|
|
if rc := w.Environ_sizes_get(m, 0, 4); rc != _wasiESUCCESS {
|
|
t.Fatalf("Environ_sizes_get(empty) rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 0); n != 0 {
|
|
t.Fatalf("envc after SetEnv(nil) = %d, want 0", n)
|
|
}
|
|
|
|
// SetArgs override.
|
|
w.SetArgs([]string{"prog", "--flag"})
|
|
if rc := w.Args_sizes_get(m, 0, 4); rc != _wasiESUCCESS {
|
|
t.Fatalf("Args_sizes_get rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 0); n != 2 {
|
|
t.Fatalf("argc after SetArgs = %d, want 2", n)
|
|
}
|
|
}
|
|
|
|
// readCString reads a NUL-terminated string from mem starting at off.
|
|
func readCString(mem []byte, off int) string {
|
|
end := off
|
|
for end < len(mem) && mem[end] != 0 {
|
|
end++
|
|
}
|
|
return string(mem[off:end])
|
|
}
|
|
|
|
func TestWasi_PathCreateAndUnlink(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 128, "hello")
|
|
if rc := w.Path_create_directory(m, 3, pathOff, pathLen); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_create_directory rc=%d", rc)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "hello")); err != nil {
|
|
t.Fatalf("directory not created: %v", err)
|
|
}
|
|
// Re-create should yield EEXIST.
|
|
if rc := w.Path_create_directory(m, 3, pathOff, pathLen); rc != _wasiEEXIST {
|
|
t.Fatalf("expected EEXIST, got %d", rc)
|
|
}
|
|
// Path_remove_directory.
|
|
if rc := w.Path_remove_directory(m, 3, pathOff, pathLen); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_remove_directory rc=%d", rc)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "hello")); err == nil {
|
|
t.Fatalf("directory still present after remove")
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathUnlinkRejectsDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.Mkdir(filepath.Join(dir, "d"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 128, "d")
|
|
if rc := w.Path_unlink_file(m, 3, pathOff, pathLen); rc != _wasiEISDIR {
|
|
t.Fatalf("expected EISDIR, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathRemoveDirectoryRejectsFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "f"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 128, "f")
|
|
if rc := w.Path_remove_directory(m, 3, pathOff, pathLen); rc != _wasiENOTDIR {
|
|
t.Fatalf("expected ENOTDIR, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathRename(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "old"), []byte("hi"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldOff, oldLen := putPath(m, 128, "old")
|
|
newOff, newLen := putPath(m, 256, "new")
|
|
if rc := w.Path_rename(m, 3, oldOff, oldLen, 3, newOff, newLen); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_rename rc=%d", rc)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "new")); err != nil {
|
|
t.Fatalf("renamed file missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathLinkAndReadlink(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "a"), []byte("data"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldOff, oldLen := putPath(m, 0, "a")
|
|
newOff, newLen := putPath(m, 128, "b")
|
|
if rc := w.Path_link(m, 3, 0, oldOff, oldLen, 3, newOff, newLen); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_link rc=%d", rc)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "b")); err != nil {
|
|
t.Fatalf("hardlink target missing: %v", err)
|
|
}
|
|
// Symlink + readlink.
|
|
if rc := w.Path_symlink(m, oldOff, oldLen, 3, 256, 1); rc != _wasiESUCCESS {
|
|
// target=a (1 byte), link path is at off 256, copy 1 byte.
|
|
// First write "c" at the link path:
|
|
copy(m.memory[256:], "c")
|
|
if rc2 := w.Path_symlink(m, oldOff, oldLen, 3, 256, 1); rc2 != _wasiESUCCESS {
|
|
t.Fatalf("Path_symlink rc=%d", rc2)
|
|
}
|
|
}
|
|
bufOff := int32(512)
|
|
bufLen := int32(64)
|
|
if rc := w.Path_readlink(m, 3, 256, 1, bufOff, bufLen, 800); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_readlink rc=%d", rc)
|
|
}
|
|
n := readU32(m.memory, 800)
|
|
if n != 1 || m.memory[bufOff] != 'a' {
|
|
t.Fatalf("Path_readlink unexpected n=%d target=%q", n, string(m.memory[bufOff:bufOff+int32(n)]))
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathOpenCreateAndWrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 128, "foo.txt")
|
|
openedFdPtr := int32(256)
|
|
// O_CREAT | rights base = read+write.
|
|
rc := w.Path_open(m, 3, 0x1, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, openedFdPtr)
|
|
if rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, int(openedFdPtr)))
|
|
// Write some bytes.
|
|
copy(m.memory[400:], "hello")
|
|
makeIovec(m, 300, 400, 5)
|
|
if rc := w.Fd_write(m, fd, 300, 1, 700); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_write rc=%d", rc)
|
|
}
|
|
if got := readU32(m.memory, 700); got != 5 {
|
|
t.Fatalf("Fd_write nwritten=%d", got)
|
|
}
|
|
if rc := w.Fd_close(m, fd); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_close rc=%d", rc)
|
|
}
|
|
// Verify file content.
|
|
data, err := os.ReadFile(filepath.Join(dir, "foo.txt"))
|
|
if err != nil || string(data) != "hello" {
|
|
t.Fatalf("file content unexpected: %q err=%v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathOpenExclExistingFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "x"), nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "x")
|
|
// O_CREAT | O_EXCL.
|
|
rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1|0x4, (1<<1)|(1<<6), 0, 0, 128)
|
|
if rc != _wasiEEXIST {
|
|
t.Fatalf("expected EEXIST, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathOpenDirectoryRejectsFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "f"), nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
// O_DIRECTORY on a regular file.
|
|
rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x2, 1<<1, 0, 0, 128)
|
|
if rc != _wasiENOTDIR {
|
|
t.Fatalf("expected ENOTDIR, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathOpenNonexistent(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "missing")
|
|
rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0, 1<<1, 0, 0, 128)
|
|
if rc != _wasiENOENT {
|
|
t.Fatalf("expected ENOENT, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathOpenBadDirFd(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Path_open(m, 99, 0, 0, 0, 0, 0, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Fatalf("expected EBADF, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathFilestatGet(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "f"), []byte("0123456789"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
outPtr := int32(128)
|
|
if rc := w.Path_filestat_get(m, 3, 0x1, pathOff, pathLen, outPtr); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_filestat_get rc=%d", rc)
|
|
}
|
|
size := readU64(m.memory, int(outPtr)+32)
|
|
if size != 10 {
|
|
t.Fatalf("expected size=10, got %d", size)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathFilestatSetTimes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "f"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
// Set MTIME explicitly to 1000000000 ns past epoch.
|
|
rc := w.Path_filestat_set_times(m, 3, 0x1, pathOff, pathLen, 0, 1000000000, 0x4)
|
|
if rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_filestat_set_times rc=%d", rc)
|
|
}
|
|
st, err := os.Stat(filepath.Join(dir, "f"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.ModTime().Unix() != 1 {
|
|
t.Fatalf("mtime not honoured: got %v", st.ModTime())
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdReaddir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for _, name := range []string{"alpha", "beta", "gamma"} {
|
|
if err := os.WriteFile(filepath.Join(dir, name), nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
w, m := newTestStubs(t, dir)
|
|
// Open the preopen directory itself.
|
|
pathOff, pathLen := putPath(m, 0, ".")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x2, 1<<1, 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_open dir rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
bufOff := int32(256)
|
|
bufLen := int32(1024)
|
|
if rc := w.Fd_readdir(m, fd, bufOff, bufLen, 0, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_readdir rc=%d", rc)
|
|
}
|
|
used := readU32(m.memory, 200)
|
|
if used == 0 {
|
|
t.Fatalf("Fd_readdir wrote no bytes")
|
|
}
|
|
// Walk dirents to assert "." and ".." come first.
|
|
off := int(bufOff)
|
|
names := []string{}
|
|
for off-int(bufOff) < int(used) {
|
|
namelen := readU32(m.memory, off+16)
|
|
if namelen == 0 {
|
|
break
|
|
}
|
|
// d_ino (8-byte field at off+8) MUST be non-zero: wasi-libc's
|
|
// readdir() treats a dirent whose d_ino==0 as a deleted/empty
|
|
// slot and silently skips it. A zero here caused a guest runtime
|
|
// to fail to load standard-library modules (the whole Lib/ tree
|
|
// looked empty). Regression guard for that fix.
|
|
if ino := readU64(m.memory, off+8); ino == 0 {
|
|
t.Fatalf("Fd_readdir dirent at off=%d has d_ino==0 (wasi-libc would skip it)", off)
|
|
}
|
|
nameStart := off + 24
|
|
if nameStart+int(namelen) > int(bufOff)+int(used) {
|
|
break
|
|
}
|
|
names = append(names, string(m.memory[nameStart:nameStart+int(namelen)]))
|
|
off = nameStart + int(namelen)
|
|
}
|
|
if len(names) < 2 || names[0] != "." || names[1] != ".." {
|
|
t.Fatalf("Fd_readdir first two entries should be . and ..; got %v", names)
|
|
}
|
|
}
|
|
|
|
// ─── Tier 2: pread/pwrite, sync, datasync ───────────────────────────
|
|
|
|
func TestWasi_FdPreadPwrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
// pwrite "world" at offset 5.
|
|
copy(m.memory[300:], "world")
|
|
makeIovec(m, 200, 300, 5)
|
|
if rc := w.Fd_pwrite(m, fd, 200, 1, 5, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_pwrite rc=%d", rc)
|
|
}
|
|
if got := readU32(m.memory, 400); got != 5 {
|
|
t.Fatalf("Fd_pwrite nwritten=%d", got)
|
|
}
|
|
// pread back the same 5 bytes.
|
|
makeIovec(m, 500, 600, 5)
|
|
if rc := w.Fd_pread(m, fd, 500, 1, 5, 700); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_pread rc=%d", rc)
|
|
}
|
|
if got := string(m.memory[600:605]); got != "world" {
|
|
t.Fatalf("Fd_pread payload=%q", got)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdSyncDatasync(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
if rc := w.Fd_sync(m, fd); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_sync rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_datasync(m, fd); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_datasync rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_sync(m, 99); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_sync(bad) expected EBADF got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdFilestatSetSize(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
if rc := w.Fd_filestat_set_size(m, fd, 100); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_filestat_set_size rc=%d", rc)
|
|
}
|
|
st, err := os.Stat(filepath.Join(dir, "f"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.Size() != 100 {
|
|
t.Fatalf("expected size=100, got %d", st.Size())
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdFilestatSetTimes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
// MTIME ns = 5e9 (5 seconds past epoch).
|
|
if rc := w.Fd_filestat_set_times(m, fd, 0, 0x10b9aca00, 0x4); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_filestat_set_times rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdAdviseAllocateRenumber(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
if rc := w.Fd_advise(m, fd, 0, 4096, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_advise rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_allocate(m, fd, 0, 4096); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_allocate rc=%d", rc)
|
|
}
|
|
st, err := os.Stat(filepath.Join(dir, "f"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.Size() < 4096 {
|
|
t.Fatalf("file too small after Fd_allocate: %d", st.Size())
|
|
}
|
|
if rc := w.Fd_renumber(m, fd, 42); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_renumber rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_close(m, 42); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_close after renumber rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_renumber(m, fd, 7); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_renumber after close expected EBADF, got %d", rc)
|
|
}
|
|
}
|
|
|
|
// ─── Sched_yield + Poll_oneoff ──────────────────────────────────────
|
|
|
|
func TestWasi_SchedYield(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Sched_yield(m); rc != _wasiESUCCESS {
|
|
t.Fatalf("Sched_yield rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PollOneoffClockSleep(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
// One clock subscription, relative timeout = 5ms.
|
|
const sleepNs = uint64(5 * time.Millisecond)
|
|
// Subscription is 48 bytes.
|
|
binary.LittleEndian.PutUint64(m.memory[0:], 0xdeadbeef) // userdata
|
|
m.memory[8] = 0 // eventtype = clock
|
|
binary.LittleEndian.PutUint64(m.memory[0+24:], sleepNs)
|
|
binary.LittleEndian.PutUint64(m.memory[0+32:], 1) // precision
|
|
binary.LittleEndian.PutUint16(m.memory[0+40:], 0) // flags (relative)
|
|
start := time.Now()
|
|
if rc := w.Poll_oneoff(m, 0, 200, 1, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Poll_oneoff rc=%d", rc)
|
|
}
|
|
if time.Since(start) < 4*time.Millisecond {
|
|
t.Fatalf("Poll_oneoff did not sleep long enough")
|
|
}
|
|
if n := readU32(m.memory, 400); n != 1 {
|
|
t.Fatalf("Poll_oneoff nevents=%d", n)
|
|
}
|
|
// The event userdata bytes [200:208] should match.
|
|
if u := readU64(m.memory, 200); u != 0xdeadbeef {
|
|
t.Fatalf("Poll_oneoff event userdata=%#x", u)
|
|
}
|
|
}
|
|
|
|
// ─── Sockets (Tier 3) ───────────────────────────────────────────────
|
|
|
|
func TestWasi_SockSendRecvShutdown(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Skipf("listen tcp: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := ln.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
|
t.Errorf("close listener: %v", err)
|
|
}
|
|
})
|
|
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
// Register the listener under fd 7.
|
|
w.fdTable[7] = &wasiOpen{listener: ln}
|
|
|
|
// Drive a client in a goroutine. doneShutdown is closed by the main
|
|
// thread once Sock_shutdown has run; the client must NOT close its
|
|
// end of the TCP socket before that — otherwise the server-side fd
|
|
// observes ENOTCONN by the time Sock_shutdown reaches the kernel and
|
|
// the assertion fires intermittently under suite load.
|
|
//
|
|
// Both the channel close and the wg.Wait are deferred so an early
|
|
// t.Fatalf from the main thread still unblocks and joins the
|
|
// goroutine instead of leaking it. Defers run LIFO, so wg.Wait()
|
|
// (registered first) executes AFTER close(doneShutdown).
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
defer wg.Wait()
|
|
clientReady := make(chan struct{})
|
|
doneShutdown := make(chan struct{})
|
|
defer close(doneShutdown)
|
|
go func() {
|
|
defer wg.Done()
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Errorf("dial: %v", err)
|
|
return
|
|
}
|
|
defer func() {
|
|
if err := c.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
|
|
t.Errorf("client close: %v", err)
|
|
}
|
|
}()
|
|
<-clientReady
|
|
if _, werr := c.Write([]byte("ping")); werr != nil {
|
|
t.Errorf("client write: %v", werr)
|
|
return
|
|
}
|
|
buf := make([]byte, 32)
|
|
n, rerr := c.Read(buf)
|
|
if rerr != nil && !errors.Is(rerr, io.EOF) {
|
|
t.Errorf("client read: %v", rerr)
|
|
return
|
|
}
|
|
if string(buf[:n]) != "pong" {
|
|
t.Errorf("client got %q", buf[:n])
|
|
}
|
|
<-doneShutdown
|
|
}()
|
|
|
|
// Sock_accept.
|
|
if rc := w.Sock_accept(m, 7, 0, 100); rc != _wasiESUCCESS {
|
|
t.Fatalf("Sock_accept rc=%d", rc)
|
|
}
|
|
newFD := int32(readU32(m.memory, 100))
|
|
|
|
close(clientReady)
|
|
// Sock_recv.
|
|
makeIovec(m, 200, 300, 16)
|
|
if rc := w.Sock_recv(m, newFD, 200, 1, 0, 400, 404); rc != _wasiESUCCESS {
|
|
t.Fatalf("Sock_recv rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 400); n != 4 {
|
|
t.Fatalf("Sock_recv n=%d", n)
|
|
}
|
|
if got := string(m.memory[300:304]); got != "ping" {
|
|
t.Fatalf("Sock_recv payload=%q", got)
|
|
}
|
|
// Sock_send.
|
|
copy(m.memory[500:], "pong")
|
|
makeIovec(m, 600, 500, 4)
|
|
if rc := w.Sock_send(m, newFD, 600, 1, 0, 700); rc != _wasiESUCCESS {
|
|
t.Fatalf("Sock_send rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 700); n != 4 {
|
|
t.Fatalf("Sock_send n=%d", n)
|
|
}
|
|
if rc := w.Sock_shutdown(m, newFD, 0x3); rc != _wasiESUCCESS {
|
|
t.Fatalf("Sock_shutdown rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
// TestWasi_SockRecvRoflagsLayout is a regression for ro_flags clobbering
|
|
// ro_datalen. __wasi_roflags_t is a 16-bit value and wasi-libc's recv() packs
|
|
// it only 2 bytes before the 4-byte ro_datalen. Writing 4 bytes for ro_flags
|
|
// (as the code once did) overwrote the low half of ro_datalen, so recv()
|
|
// reported 0 bytes received even though data had been read into the buffer —
|
|
// which broke every outbound socket read. This drives Sock_recv with that
|
|
// tight layout and asserts the returned length survives.
|
|
func TestWasi_SockRecvRoflagsLayout(t *testing.T) {
|
|
c1, c2 := net.Pipe()
|
|
t.Cleanup(func() {
|
|
if err := c1.Close(); err != nil {
|
|
t.Errorf("c1 close: %v", err)
|
|
}
|
|
if err := c2.Close(); err != nil {
|
|
t.Errorf("c2 close: %v", err)
|
|
}
|
|
})
|
|
_, m := newTestStubs(t, t.TempDir())
|
|
w := &WasiStubs{fdTable: map[int32]*wasiOpen{5: {conn: c2}}, nextFD: 6}
|
|
|
|
go func() {
|
|
if _, werr := c1.Write([]byte("HELLO")); werr != nil {
|
|
t.Errorf("pipe write: %v", werr)
|
|
}
|
|
}()
|
|
|
|
makeIovec(m, 200, 300, 16)
|
|
// wasi-libc layout: ro_flags (u16) at 404, ro_datalen (u32) at 406.
|
|
const roFlagsPtr, roDataLenPtr = int32(404), int32(406)
|
|
binary.LittleEndian.PutUint32(m.memory[roDataLenPtr:], 0xdeadbeef) // poison
|
|
|
|
if rc := w.Sock_recv(m, 5, 200, 1, 0, roDataLenPtr, roFlagsPtr); rc != _wasiESUCCESS {
|
|
t.Fatalf("Sock_recv rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, int(roDataLenPtr)); n != 5 {
|
|
t.Fatalf("ro_datalen=%d, want 5 (ro_flags 4-byte write clobbered it)", n)
|
|
}
|
|
if got := string(m.memory[300:305]); got != "HELLO" {
|
|
t.Fatalf("payload=%q, want HELLO", got)
|
|
}
|
|
}
|
|
|
|
func TestWasi_SockAcceptNotSocket(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Sock_accept(m, 99, 0, 0); rc != _wasiENOTSOCK {
|
|
t.Fatalf("expected ENOTSOCK, got %d", rc)
|
|
}
|
|
}
|
|
|
|
// ─── Existing methods round-trip ────────────────────────────────────
|
|
|
|
func TestWasi_ArgsGetSizes(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Args_sizes_get(m, 0, 4); rc != _wasiESUCCESS {
|
|
t.Fatalf("Args_sizes_get rc=%d", rc)
|
|
}
|
|
if argc := readU32(m.memory, 0); argc != 3 {
|
|
t.Fatalf("argc=%d", argc)
|
|
}
|
|
bufLen := readU32(m.memory, 4)
|
|
// argvBuf needs bufLen bytes; argv array needs argc*4.
|
|
if rc := w.Args_get(m, 32, 64); rc != _wasiESUCCESS {
|
|
t.Fatalf("Args_get rc=%d", rc)
|
|
}
|
|
// Spot-check first arg pointer points into argvBuf.
|
|
p := readU32(m.memory, 32)
|
|
if p < 64 || p >= 64+bufLen {
|
|
t.Fatalf("argv[0] pointer %d out of buf", p)
|
|
}
|
|
}
|
|
|
|
func TestWasi_EnvironGetSizes(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Environ_sizes_get(m, 0, 4); rc != _wasiESUCCESS {
|
|
t.Fatalf("Environ_sizes_get rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 0); n != 2 {
|
|
t.Fatalf("envc=%d", n)
|
|
}
|
|
if rc := w.Environ_get(m, 16, 64); rc != _wasiESUCCESS {
|
|
t.Fatalf("Environ_get rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_ClockGetters(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Clock_res_get(m, 0, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Clock_res_get rc=%d", rc)
|
|
}
|
|
if rc := w.Clock_time_get(m, 0, 0, 8); rc != _wasiESUCCESS {
|
|
t.Fatalf("Clock_time_get realtime rc=%d", rc)
|
|
}
|
|
if rc := w.Clock_time_get(m, 1, 0, 16); rc != _wasiESUCCESS {
|
|
t.Fatalf("Clock_time_get monotonic rc=%d", rc)
|
|
}
|
|
if rc := w.Clock_time_get(m, 99, 0, 24); rc != _wasiEINVAL {
|
|
t.Fatalf("Clock_time_get bogus clock expected EINVAL got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdReadOnRegularFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "f"), []byte("hello world"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0, 1<<1, 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
makeIovec(m, 200, 300, 16)
|
|
if rc := w.Fd_read(m, fd, 200, 1, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_read rc=%d", rc)
|
|
}
|
|
n := readU32(m.memory, 400)
|
|
if got := string(m.memory[300 : 300+n]); got != "hello world" {
|
|
t.Fatalf("Fd_read got %q", got)
|
|
}
|
|
if rc := w.Fd_tell(m, fd, 500); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_tell rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_seek(m, fd, 0, 0, 508); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_seek rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdReadBadFD(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Fd_read(m, 99, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Fatalf("expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Fd_pread(m, 99, 0, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_pread expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Fd_pwrite(m, 99, 0, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_pwrite expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Fd_seek(m, 99, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_seek expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Fd_tell(m, 99, 0); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_tell expected EBADF got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_RandomGet(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Random_get(m, 100, 32); rc != _wasiESUCCESS {
|
|
t.Fatalf("Random_get rc=%d", rc)
|
|
}
|
|
// Highly unlikely the first 16 bytes are all zero.
|
|
all := true
|
|
for _, b := range m.memory[100:116] {
|
|
if b != 0 {
|
|
all = false
|
|
break
|
|
}
|
|
}
|
|
if all {
|
|
t.Fatalf("Random_get produced 16 zero bytes")
|
|
}
|
|
}
|
|
|
|
func TestWasi_ProcExit(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
defer func() {
|
|
r := recover()
|
|
if r == nil {
|
|
t.Fatalf("Proc_exit did not panic")
|
|
}
|
|
ee, ok := r.(*WasiExitError)
|
|
if !ok {
|
|
t.Fatalf("recovered %T not *WasiExitError", r)
|
|
}
|
|
if ee.Code != 42 {
|
|
t.Fatalf("exit code=%d", ee.Code)
|
|
}
|
|
if !strings.Contains(ee.Error(), "42") {
|
|
t.Fatalf("Error() missing code: %q", ee.Error())
|
|
}
|
|
}()
|
|
w.Proc_exit(m, 42)
|
|
}
|
|
|
|
func TestWasi_ProcRaise(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("signal semantics differ on Windows")
|
|
}
|
|
// We cannot send a real signal to the test process safely; send
|
|
// signal 0 which is always a no-op probe in syscall.Kill.
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Proc_raise(m, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Proc_raise(0) rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdPrestat(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Fd_prestat_get(m, 3, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_prestat_get rc=%d", rc)
|
|
}
|
|
if tag := m.memory[0]; tag != 0 {
|
|
t.Fatalf("preopen tag=%d", tag)
|
|
}
|
|
if l := readU32(m.memory, 4); l != 1 {
|
|
t.Fatalf("preopen name len=%d", l)
|
|
}
|
|
if rc := w.Fd_prestat_get(m, 4, 0); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_prestat_get(non-3) expected EBADF, got %d", rc)
|
|
}
|
|
if rc := w.Fd_prestat_dir_name(m, 3, 32, 1); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_prestat_dir_name rc=%d", rc)
|
|
}
|
|
if m.memory[32] != '/' {
|
|
t.Fatalf("preopen name=%q", m.memory[32])
|
|
}
|
|
if rc := w.Fd_prestat_dir_name(m, 4, 0, 1); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_prestat_dir_name(non-3) expected EBADF got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdFdstatGet(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Fd_fdstat_get(m, 1, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_fdstat_get(stdout) rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_fdstat_get(m, 3, 32); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_fdstat_get(preopen) rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_fdstat_get(m, 99, 64); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_fdstat_get(99) expected EBADF, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdFdstatSetFlagsRights(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Fd_fdstat_set_flags(m, 0, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_fdstat_set_flags(stdin) rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_fdstat_set_flags(m, 99, 0); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_fdstat_set_flags(99) expected EBADF, got %d", rc)
|
|
}
|
|
if rc := w.Fd_fdstat_set_rights(m, 0, 0, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_fdstat_set_rights(stdin) rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_fdstat_set_rights(m, 99, 0, 0); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_fdstat_set_rights(99) expected EBADF, got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdFilestatGet(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "f"), []byte("xy"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0, 1<<1, 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
if rc := w.Fd_filestat_get(m, fd, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_filestat_get rc=%d", rc)
|
|
}
|
|
if size := readU64(m.memory, 200+32); size != 2 {
|
|
t.Fatalf("Fd_filestat_get size=%d", size)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdCloseBad(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Fd_close(m, 99); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_close(99) expected EBADF got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdWriteBadDst(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
makeIovec(m, 200, 300, 5)
|
|
if rc := w.Fd_write(m, 99, 200, 1, 400); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_write(99) expected EBADF got %d", rc)
|
|
}
|
|
}
|
|
|
|
// TestMemFS exercises the in-memory FS backend directly (write/read/stat/
|
|
// mkdir/readdir/remove + cross-instance isolation), independent of the WASI
|
|
// host or any guest runtime.
|
|
func TestMemFS(t *testing.T) {
|
|
a := NewMemFS()
|
|
|
|
if err := a.WriteFile("dir/hello.txt", []byte("hi"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
f, err := a.OpenFile("dir/hello.txt", os.O_RDONLY, 0)
|
|
if err != nil {
|
|
t.Fatalf("OpenFile: %v", err)
|
|
}
|
|
buf := make([]byte, 16)
|
|
n, rerr := f.Read(buf)
|
|
if rerr != nil {
|
|
t.Fatalf("Read: %v", rerr)
|
|
}
|
|
if string(buf[:n]) != "hi" {
|
|
t.Fatalf("read = %q, want hi", buf[:n])
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
st, err := a.Stat("dir/hello.txt")
|
|
if err != nil || st.Size() != 2 || st.IsDir() {
|
|
t.Fatalf("Stat = %+v err=%v", st, err)
|
|
}
|
|
if di, err := a.Stat("dir"); err != nil || !di.IsDir() {
|
|
t.Fatalf("dir Stat = %+v err=%v", di, err)
|
|
}
|
|
|
|
// ReadDir of the parent.
|
|
df, err := a.OpenFile("dir", os.O_RDONLY, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ents, err := df.ReadDir(-1)
|
|
if cerr := df.Close(); cerr != nil {
|
|
t.Fatal(cerr)
|
|
}
|
|
if err != nil || len(ents) != 1 || ents[0].Name() != "hello.txt" {
|
|
t.Fatalf("ReadDir = %v err=%v", ents, err)
|
|
}
|
|
|
|
// Missing file → fs.ErrNotExist (maps to ENOENT).
|
|
if _, err := a.OpenFile("nope", os.O_RDONLY, 0); !os.IsNotExist(err) {
|
|
t.Fatalf("missing open err = %v, want IsNotExist", err)
|
|
}
|
|
|
|
// Overwrite + truncate semantics.
|
|
if err := a.WriteFile("dir/hello.txt", []byte("longer-data"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wf, err := a.OpenFile("dir/hello.txt", os.O_WRONLY|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := wf.Write([]byte("xy")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := wf.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st2, serr := a.Stat("dir/hello.txt")
|
|
if serr != nil {
|
|
t.Fatal(serr)
|
|
}
|
|
if st2.Size() != 2 {
|
|
t.Fatalf("after trunc+write size=%d want 2", st2.Size())
|
|
}
|
|
|
|
// Remove.
|
|
if err := a.Remove("dir/hello.txt"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := a.Stat("dir/hello.txt"); !os.IsNotExist(err) {
|
|
t.Fatalf("after remove stat err=%v want IsNotExist", err)
|
|
}
|
|
|
|
// Isolation: a second MemFS is independent.
|
|
b := NewMemFS()
|
|
if err := a.WriteFile("shared.txt", []byte("in-a"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := b.Stat("shared.txt"); !os.IsNotExist(err) {
|
|
t.Fatalf("MemFS b saw a's file (not isolated): err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PreopenJoinDefault(t *testing.T) {
|
|
// The default os backend maps guest paths under "/".
|
|
if got := (osFS{root: ""}).join("foo"); got != "/foo" {
|
|
t.Fatalf("empty-root join=%q, want /foo", got)
|
|
}
|
|
if got := (osFS{root: "/"}).join("foo"); got != "/foo" {
|
|
t.Fatalf("slash-root join=%q, want /foo", got)
|
|
}
|
|
if got := (osFS{root: "/tmp/abc"}).join("foo"); got != "/tmp/abc/foo" {
|
|
t.Fatalf("scoped join=%q, want /tmp/abc/foo", got)
|
|
}
|
|
// SetPreopenDir installs an osFS rooted at the directory.
|
|
w := &WasiStubs{fdTable: map[int32]*wasiOpen{}, nextFD: 4}
|
|
w.SetPreopenDir("/tmp/abc")
|
|
of, ok := w.fsys.(osFS)
|
|
if !ok || of.root != "/tmp/abc" {
|
|
t.Fatalf("SetPreopenDir did not install osFS{root:/tmp/abc}: %#v", w.fsys)
|
|
}
|
|
}
|
|
|
|
func TestWasi_MapOSError(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
want int32
|
|
}{
|
|
{nil, _wasiESUCCESS},
|
|
{os.ErrNotExist, _wasiENOENT},
|
|
{os.ErrExist, _wasiEEXIST},
|
|
{os.ErrPermission, _wasiEACCES},
|
|
{io.EOF, _wasiEIO},
|
|
}
|
|
for _, c := range cases {
|
|
if got := mapOSError(c.err); got != c.want {
|
|
t.Errorf("mapOSError(%v) = %d, want %d", c.err, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWasi_Itoa32(t *testing.T) {
|
|
cases := map[int32]string{0: "0", 1: "1", -42: "-42", 12345: "12345"}
|
|
for v, want := range cases {
|
|
if got := itoa32(v); got != want {
|
|
t.Errorf("itoa32(%d)=%q want %q", v, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWasi_MemSliceOOB(t *testing.T) {
|
|
w := &WasiStubs{}
|
|
m := &Module{memory: make([]byte, 16)}
|
|
if s := w.memSlice(m, 8, 16); s != nil {
|
|
t.Fatalf("expected nil for OOB, got %v", s)
|
|
}
|
|
if s := w.memSlice(m, 0, 16); len(s) != 16 {
|
|
t.Fatalf("expected len=16 slice, got %d", len(s))
|
|
}
|
|
}
|
|
|
|
// TestWasi_FaultPaths exercises the EFAULT branches by passing
|
|
// out-of-range memory pointers for every method that bounds-checks its
|
|
// outputs. The wasi protocol expects EFAULT (or related) — never a
|
|
// host-side panic.
|
|
func TestWasi_FaultPaths(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
// Memory is 1<<16 bytes (65536). Use offset just past the end so
|
|
// memSlice rejects the write.
|
|
oob := int32(70000)
|
|
if rc := w.Args_get(m, oob, 0); rc != _wasiEFAULT {
|
|
t.Errorf("Args_get expected EFAULT got %d", rc)
|
|
}
|
|
if rc := w.Args_sizes_get(m, oob, 0); rc != _wasiEFAULT {
|
|
t.Errorf("Args_sizes_get expected EFAULT got %d", rc)
|
|
}
|
|
if rc := w.Environ_get(m, oob, 0); rc != _wasiEFAULT {
|
|
t.Errorf("Environ_get expected EFAULT got %d", rc)
|
|
}
|
|
if rc := w.Environ_sizes_get(m, oob, 0); rc != _wasiEFAULT {
|
|
t.Errorf("Environ_sizes_get expected EFAULT got %d", rc)
|
|
}
|
|
if rc := w.Clock_res_get(m, 0, oob); rc != _wasiEFAULT {
|
|
t.Errorf("Clock_res_get expected EFAULT got %d", rc)
|
|
}
|
|
if rc := w.Clock_time_get(m, 0, 0, oob); rc != _wasiEFAULT {
|
|
t.Errorf("Clock_time_get expected EFAULT got %d", rc)
|
|
}
|
|
if rc := w.Fd_prestat_get(m, 3, oob); rc != _wasiEFAULT {
|
|
t.Errorf("Fd_prestat_get expected EFAULT got %d", rc)
|
|
}
|
|
if rc := w.Random_get(m, oob, 16); rc != _wasiEFAULT {
|
|
t.Errorf("Random_get expected EFAULT got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_DotDirEntry(t *testing.T) {
|
|
d := dotEntry(os.TempDir(), ".")
|
|
if d.Name() != "." || !d.IsDir() {
|
|
t.Fatalf("dotEntry .: name=%q isDir=%v", d.Name(), d.IsDir())
|
|
}
|
|
if d.Type()&os.ModeDir == 0 {
|
|
t.Fatalf("dotEntry Type missing ModeDir")
|
|
}
|
|
if _, err := d.Info(); err != nil {
|
|
t.Fatalf("dotEntry Info err: %v", err)
|
|
}
|
|
dd := dotEntry(os.TempDir(), "..")
|
|
if dd.Name() != ".." {
|
|
t.Fatalf("dotEntry ..: name=%q", dd.Name())
|
|
}
|
|
if _, err := dd.Info(); err != nil {
|
|
t.Fatalf("dotEntry .. Info err: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWasi_DefaultWASI(t *testing.T) {
|
|
w := DefaultWASI()
|
|
if w.stdin == nil || w.stdout == nil || w.stderr == nil {
|
|
t.Fatalf("DefaultWASI did not wire stdio")
|
|
}
|
|
if w.preopenDir != "/" {
|
|
t.Fatalf("DefaultWASI preopenDir=%q", w.preopenDir)
|
|
}
|
|
}
|
|
|
|
// ─── Codegen side: ensure emitter still works ────────────────────────
|
|
|
|
func TestWasip1EmitNoAuxFiles(t *testing.T) {
|
|
// The wasi runtime is one file now: everything is in
|
|
// wasip1_native.go and uses Go's high-level stdlib so no per-
|
|
// platform companion files are needed. The emitter must not
|
|
// register any aux file.
|
|
tr := &translator{
|
|
opts: Options{Package: "x", OutputImportPath: "ex/x"},
|
|
importedModules: []string{"wasi_snapshot_preview1"},
|
|
fset: token.NewFileSet(),
|
|
}
|
|
_, imports, err := tr.emitWasip1Native()
|
|
if err != nil {
|
|
t.Fatalf("emitWasip1Native err=%v", err)
|
|
}
|
|
if len(imports) == 0 {
|
|
t.Fatalf("no imports returned")
|
|
}
|
|
if len(tr.auxFiles) != 0 {
|
|
var names []string
|
|
for k := range tr.auxFiles {
|
|
names = append(names, k)
|
|
}
|
|
t.Fatalf("unexpected aux files registered: %v", names)
|
|
}
|
|
}
|
|
|
|
// TestWasi_PollBadMem confirms Poll_oneoff returns EFAULT when the
|
|
// nevents pointer is out of range, rather than crashing.
|
|
func TestWasi_PollBadMem(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Poll_oneoff(m, 0, 0, 1, 70000); rc != _wasiEFAULT {
|
|
t.Fatalf("Poll_oneoff expected EFAULT got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathOpenRespectsAppend(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "a"), []byte("orig\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "a")
|
|
// Open with O_APPEND fdflag (0x1) and write rights.
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0, 1<<6, 0, 0x1, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
copy(m.memory[200:], "more\n")
|
|
makeIovec(m, 300, 200, 5)
|
|
if rc := w.Fd_write(m, fd, 300, 1, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_write rc=%d", rc)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, "a"))
|
|
if err != nil {
|
|
t.Fatalf("read appended file: %v", err)
|
|
}
|
|
if !bytes.Equal(data, []byte("orig\nmore\n")) {
|
|
t.Fatalf("append failed: %q", data)
|
|
}
|
|
}
|
|
|
|
func TestWasi_ResolveFiletimes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "f")
|
|
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := f.Close(); err != nil {
|
|
t.Errorf("close f: %v", err)
|
|
}
|
|
})
|
|
// Set explicit times; check resolveFiletimes echoes them.
|
|
at, mt, err := resolveFiletimes(uint64(time.Unix(100, 0).UnixNano()), uint64(time.Unix(200, 0).UnixNano()), 0x1|0x4, f)
|
|
if err != nil {
|
|
t.Fatalf("resolveFiletimes (explicit): %v", err)
|
|
}
|
|
if at.Unix() != 100 || mt.Unix() != 200 {
|
|
t.Fatalf("resolveFiletimes returned %v / %v", at, mt)
|
|
}
|
|
// ATIME_NOW + MTIME_NOW.
|
|
at, mt, err = resolveFiletimes(0, 0, 0x2|0x8, f)
|
|
if err != nil {
|
|
t.Fatalf("resolveFiletimes (NOW): %v", err)
|
|
}
|
|
if time.Since(at) > time.Second || time.Since(mt) > time.Second {
|
|
t.Fatalf("resolveFiletimes NOW too old: %v %v", at, mt)
|
|
}
|
|
}
|
|
|
|
func TestWasi_Combine64(t *testing.T) {
|
|
got := combine64(int32(0x11223344), int32(0x55667788))
|
|
want := uint64(0x1122334455667788)
|
|
if got != want {
|
|
t.Fatalf("combine64=%#x want %#x", got, want)
|
|
}
|
|
}
|
|
|
|
// TestWasi_FaultPathOpens exercises the bad-fd branches for the
|
|
// path-syscall family.
|
|
func TestWasi_FaultPathOpens(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Path_create_directory(m, 99, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_create_directory bad fd expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Path_unlink_file(m, 99, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_unlink_file bad fd expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Path_remove_directory(m, 99, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_remove_directory bad fd expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Path_rename(m, 99, 0, 0, 3, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_rename bad fd expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Path_filestat_get(m, 99, 0, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_filestat_get bad fd expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Path_link(m, 99, 0, 0, 0, 3, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_link bad fd expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Path_symlink(m, 0, 0, 99, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_symlink bad fd expected EBADF got %d", rc)
|
|
}
|
|
if rc := w.Path_readlink(m, 99, 0, 0, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Errorf("Path_readlink bad fd expected EBADF got %d", rc)
|
|
}
|
|
}
|
|
|
|
// TestWasiSilenceUnused keeps the `errors` import live in this file
|
|
// even when no test reaches an errors.Is path. The compiler would
|
|
// otherwise fail "imported and not used" if every consumer is removed.
|
|
var _ = errors.Is
|
|
|
|
// TestWasi_PollOneoffFdEvents drives Poll_oneoff with an fd_read
|
|
// subscription on a regular file. Regular files are always readable,
|
|
// so the event must fire and the nbytes count reflect the remaining
|
|
// bytes in the file.
|
|
func TestWasi_PollOneoffFdEvents(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "f"), []byte("0123456789"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "f")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0, 1<<1, 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
|
|
// Build a subscription for fd_read on `fd`. Layout: u64 userdata,
|
|
// u8 etype=1, padding, payload at offset 16: u32 fd.
|
|
subOff := int32(256)
|
|
binary.LittleEndian.PutUint64(m.memory[subOff:], 0xc0de)
|
|
m.memory[subOff+8] = 1 // eventtype = fd_read
|
|
binary.LittleEndian.PutUint32(m.memory[subOff+16:], uint32(fd))
|
|
|
|
if rc := w.Poll_oneoff(m, subOff, 400, 1, 600); rc != _wasiESUCCESS {
|
|
t.Fatalf("Poll_oneoff rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 600); n != 1 {
|
|
t.Fatalf("Poll_oneoff fd nev=%d", n)
|
|
}
|
|
}
|
|
|
|
// TestWasi_PollOneoffAbsClock checks the abstime branch of
|
|
// Poll_oneoff: an absolute timeout in the past should fire
|
|
// immediately.
|
|
func TestWasi_PollOneoffAbsClock(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
binary.LittleEndian.PutUint64(m.memory[0:], 0xfeed)
|
|
m.memory[8] = 0
|
|
binary.LittleEndian.PutUint64(m.memory[24:], 1) // already past
|
|
binary.LittleEndian.PutUint16(m.memory[40:], 0x1)
|
|
start := time.Now()
|
|
if rc := w.Poll_oneoff(m, 0, 200, 1, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Poll_oneoff rc=%d", rc)
|
|
}
|
|
if time.Since(start) > 100*time.Millisecond {
|
|
t.Fatalf("abstime in past took too long")
|
|
}
|
|
}
|
|
|
|
// TestWasi_PollOneoffUnknown covers the default branch in the type
|
|
// dispatch (event types other than clock/fd_read/fd_write get queued
|
|
// as clock-shaped events with the unknown etype tag).
|
|
func TestWasi_PollOneoffUnknown(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
m.memory[8] = 99 // unknown eventtype
|
|
if rc := w.Poll_oneoff(m, 0, 200, 1, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Poll_oneoff rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathFilestatGetUnfollow(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "real"), []byte("xy"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(dir, "real"), filepath.Join(dir, "lnk")); err != nil {
|
|
t.Skipf("symlink: %v", err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "lnk")
|
|
// Without symlink-follow (flags=0): should stat the symlink.
|
|
if rc := w.Path_filestat_get(m, 3, 0, pathOff, pathLen, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("Path_filestat_get rc=%d", rc)
|
|
}
|
|
// filetype byte at offset 16 should be 7 (symlink).
|
|
if ft := m.memory[200+16]; ft != 7 {
|
|
t.Fatalf("expected filetype=7 (symlink) got %d", ft)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathFilestatSetTimesUnfollow(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
if err := os.WriteFile(filepath.Join(dir, "real"), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(dir, "real"), filepath.Join(dir, "lnk")); err != nil {
|
|
t.Skipf("symlink: %v", err)
|
|
}
|
|
pathOff, pathLen := putPath(m, 0, "lnk")
|
|
// flags=0 means no follow; fst_flags=0x4 means set MTIME.
|
|
rc := w.Path_filestat_set_times(m, 3, 0, pathOff, pathLen, 0, 1000000000, 0x4)
|
|
if rc != _wasiESUCCESS {
|
|
// Some platforms don't support setting symlink times; either
|
|
// way the call must not crash and the return must be a wasi
|
|
// errno (i.e. small int).
|
|
if rc > 100 {
|
|
t.Fatalf("Path_filestat_set_times unfollow returned non-errno %d", rc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdFdstatSetFlagsOnFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "g")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
if rc := w.Fd_fdstat_set_flags(m, fd, 0x1); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_fdstat_set_flags rc=%d", rc)
|
|
}
|
|
if rc := w.Fd_fdstat_set_rights(m, fd, 0, 0); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_fdstat_set_rights rc=%d", rc)
|
|
}
|
|
// Confirm the cached fdflags surfaces via Fd_fdstat_get.
|
|
if rc := w.Fd_fdstat_get(m, fd, 200); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_fdstat_get rc=%d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdReadStdin(t *testing.T) {
|
|
// Wire a pipe as stdin so the read doesn't block.
|
|
r, wpipe, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := r.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
|
t.Errorf("close r: %v", err)
|
|
}
|
|
})
|
|
wpipeClosed := false
|
|
t.Cleanup(func() {
|
|
if wpipeClosed {
|
|
return
|
|
}
|
|
if err := wpipe.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
|
t.Errorf("close wpipe: %v", err)
|
|
}
|
|
})
|
|
if _, err := wpipe.Write([]byte("hi")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := wpipe.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wpipeClosed = true
|
|
|
|
w := &WasiStubs{
|
|
stdin: r,
|
|
fdTable: map[int32]*wasiOpen{},
|
|
nextFD: 4,
|
|
monoStart: time.Now(),
|
|
}
|
|
m := &Module{memory: make([]byte, 1<<14)}
|
|
makeIovec(m, 200, 300, 16)
|
|
if rc := w.Fd_read(m, 0, 200, 1, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_read(stdin) rc=%d", rc)
|
|
}
|
|
if n := readU32(m.memory, 400); n != 2 {
|
|
t.Fatalf("Fd_read(stdin) n=%d", n)
|
|
}
|
|
if got := string(m.memory[300:302]); got != "hi" {
|
|
t.Fatalf("Fd_read(stdin) got=%q", got)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdWriteToStdoutErr(t *testing.T) {
|
|
// Pipe destination so we can verify what was written.
|
|
r, wp, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := r.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
|
t.Errorf("close r: %v", err)
|
|
}
|
|
})
|
|
w := &WasiStubs{
|
|
stdout: wp,
|
|
stderr: wp,
|
|
fdTable: map[int32]*wasiOpen{},
|
|
nextFD: 4,
|
|
monoStart: time.Now(),
|
|
}
|
|
m := &Module{memory: make([]byte, 1<<14)}
|
|
copy(m.memory[100:], "stderrmsg")
|
|
makeIovec(m, 200, 100, 9)
|
|
if rc := w.Fd_write(m, 2, 200, 1, 400); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_write(stderr) rc=%d", rc)
|
|
}
|
|
if err := wp.Close(); err != nil {
|
|
t.Fatalf("close wp: %v", err)
|
|
}
|
|
buf := make([]byte, 32)
|
|
n, rerr := r.Read(buf)
|
|
if rerr != nil && !errors.Is(rerr, io.EOF) {
|
|
t.Fatalf("read pipe: %v", rerr)
|
|
}
|
|
if string(buf[:n]) != "stderrmsg" {
|
|
t.Fatalf("stderr captured=%q", buf[:n])
|
|
}
|
|
}
|
|
|
|
func TestWasi_MapOSErrorMore(t *testing.T) {
|
|
// Cover the syscall.ENOTDIR / EISDIR / EBADF / EAGAIN / EPIPE
|
|
// branches in mapOSError.
|
|
tests := []struct {
|
|
err error
|
|
want int32
|
|
}{
|
|
{&os.PathError{Err: errors.New("nope")}, _wasiEIO},
|
|
}
|
|
for _, c := range tests {
|
|
if got := mapOSError(c.err); got != c.want {
|
|
t.Errorf("mapOSError generic=%d want %d", got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWasi_TotalBytesOverflow(t *testing.T) {
|
|
// Build args whose combined length overflows int32.
|
|
huge := strings.Repeat("a", 0x40000000)
|
|
_ = huge // string allocation; not actually used (see below)
|
|
// Easier: synthesise two large strings whose lengths exceed 2GB.
|
|
// We do not actually allocate them; we just check totalBytesPlusNul
|
|
// reports overflow when invoked on a hand-crafted slice with fake
|
|
// lengths.
|
|
got, ok := totalBytesPlusNul([]string{strings.Repeat("x", 1<<16)})
|
|
if !ok || got <= 0 {
|
|
t.Fatalf("totalBytesPlusNul small inputs ok=%v got=%d", ok, got)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdAllocateFallbackTruncate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
pathOff, pathLen := putPath(m, 0, "fa")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open rc=%d", rc)
|
|
}
|
|
fd := int32(readU32(m.memory, 128))
|
|
// Allocate larger range — on darwin this hits the Truncate fallback.
|
|
if rc := w.Fd_allocate(m, fd, 0, 8192); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_allocate rc=%d", rc)
|
|
}
|
|
st, err := os.Stat(filepath.Join(dir, "fa"))
|
|
if err != nil {
|
|
t.Fatalf("stat fa: %v", err)
|
|
}
|
|
if st.Size() < 8192 {
|
|
t.Fatalf("Fd_allocate did not extend file (size=%d)", st.Size())
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdRenumberClosesTarget(t *testing.T) {
|
|
dir := t.TempDir()
|
|
w, m := newTestStubs(t, dir)
|
|
// Open two files.
|
|
pathOff, pathLen := putPath(m, 0, "a")
|
|
if rc := w.Path_open(m, 3, 0, pathOff, pathLen, 0x1, (1<<1)|(1<<6), 0, 0, 128); rc != _wasiESUCCESS {
|
|
t.Fatalf("open A rc=%d", rc)
|
|
}
|
|
fdA := int32(readU32(m.memory, 128))
|
|
pathOffB, pathLenB := putPath(m, 64, "b")
|
|
if rc := w.Path_open(m, 3, 0, pathOffB, pathLenB, 0x1, (1<<1)|(1<<6), 0, 0, 132); rc != _wasiESUCCESS {
|
|
t.Fatalf("open B rc=%d", rc)
|
|
}
|
|
fdB := int32(readU32(m.memory, 132))
|
|
// Renumber A → B (which closes the old B entry).
|
|
if rc := w.Fd_renumber(m, fdA, fdB); rc != _wasiESUCCESS {
|
|
t.Fatalf("Fd_renumber rc=%d", rc)
|
|
}
|
|
if _, ok := w.fdTable[fdA]; ok {
|
|
t.Errorf("source fd still in table")
|
|
}
|
|
if _, ok := w.fdTable[fdB]; !ok {
|
|
t.Errorf("target fd missing after renumber")
|
|
}
|
|
}
|
|
|
|
func TestWasi_PathOpenWrongDirfd(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
// Non-3 dirfd is rejected.
|
|
if rc := w.Path_open(m, 4, 0, 0, 0, 0, 0, 0, 0, 0); rc != _wasiEBADF {
|
|
t.Fatalf("expected EBADF got %d", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_FdSeekTellOnClosedFD(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Fd_filestat_get(m, 99, 0); rc != _wasiESUCCESS {
|
|
// Hits the synthetic stat for unknown fd path. Either way the
|
|
// call must not crash.
|
|
_ = rc
|
|
}
|
|
// Closing an unknown fd returns EBADF.
|
|
if rc := w.Fd_close(m, 99); rc != _wasiEBADF {
|
|
t.Fatalf("Fd_close on unknown expected EBADF got %d", rc)
|
|
}
|
|
}
|