Files
wasm2go/internal/codegen/wasip1_native_test.go
efa4ebf207 codegen: pass the resolved host to the WASI dial hook (#37)
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]>
2026-07-23 15:44:36 +09:00

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)
}
}