mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* WASI runtime: fd-priority poll, spawn cwd, dup sharing, chmod/stat-mode imports Host-side gaps found while running the CPAN build toolchain (cpanm, ExtUtils::MakeMaker, Module::Build) on a transpiled perl: - Poll_oneoff slept the clock subscription's full timeout before reporting fd readiness, turning every select/poll with a timeout into that timeout. fd subscriptions now report immediately, and the clock paces the call only when it is the sole kind subscribed (its "fired" event is suppressed while ready fds are reported, as a real poll would). - Proc_spawn gains a cwd argument: the guest tracks its working directory in libc (WASI has no chdir syscall), so children previously started in the host process's cwd. The bridge shim passes getcwd(). - Child stdio resolution (and Fd_read/Fd_write's fdDstLocked/ fdSrcLocked) consult the fd table before falling back to the interpreter streams, so a guest that re-opened its stdout onto a file (cpanm logs child builds that way) hands children the redirection. - New fd_dup/fd_dup2 imports implement dup/dup2 sharing semantics (fd_renumber's move semantics cannot express save/redirect/restore): table slots share one entry with a reference count, and a bare stdio fd dups as an alias entry that never closes the real stream. - New path_chmod and path_filestat_mode imports back bridge-provided chmod/stat/lstat: WASI can neither change modes nor report permission bits, and build tools round-trip modes (Module::Build's make_executable does chmod((stat $f)[2] | 0111), which otherwise strips every permission). FS backends opt in via a Chmod method (osFS implements it; MemFS keeps no modes and reports ENOSYS). Co-Authored-By: Claude Fable 5 <[email protected]> * Format the Chmod capability assertion (gofmt) Co-Authored-By: Claude Fable 5 <[email protected]> * Cover the dup/stdio-redirection, chmod/stat-mode, and poll-preemption paths Exercises the runtime additions directly: dup sharing a single offset with deferred close, the save/redirect/restore stdio dance (including the child-stdio resolution a spawned process would get and the alias entries that protect the interpreter streams), path_chmod and path_filestat_mode round trips with the ENOSYS backend case, fd subscriptions preempting a poll timeout while a clock-only poll still paces the call, and the numeric/whitelisted getaddrinfo branches. Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]>
209 lines
6.6 KiB
Go
209 lines
6.6 KiB
Go
package codegen
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// writeCStr writes s plus a NUL at off and returns off.
|
|
func writeCStr(m *Module, off int32, s string) int32 {
|
|
copy(m.memory[off:], append([]byte(s), 0))
|
|
return off
|
|
}
|
|
|
|
// writeArgv lays out a NULL-terminated char** for argv at off, with each
|
|
// string packed starting at strOff. Returns the argv pointer.
|
|
func writeArgv(m *Module, off, strOff int32, argv []string) int32 {
|
|
p := strOff
|
|
ptrs := make([]int32, len(argv))
|
|
for i, a := range argv {
|
|
ptrs[i] = p
|
|
writeCStr(m, p, a)
|
|
p += int32(len(a)) + 1
|
|
}
|
|
for i, sp := range ptrs {
|
|
binary.LittleEndian.PutUint32(m.memory[off+int32(i*4):], uint32(sp))
|
|
}
|
|
binary.LittleEndian.PutUint32(m.memory[off+int32(len(ptrs)*4):], 0) // NULL term
|
|
return off
|
|
}
|
|
|
|
func TestWasi_ProcSpawnWait(t *testing.T) {
|
|
if _, err := os.Stat("/bin/echo"); err != nil {
|
|
t.Skip("/bin/echo not available")
|
|
}
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
var out bytes.Buffer
|
|
w.stdout = &out
|
|
w.stderr = &bytes.Buffer{}
|
|
w.stdin = bytes.NewReader(nil)
|
|
|
|
pathOff := writeCStr(m, 1000, "/bin/echo")
|
|
argvOff := writeArgv(m, 2000, 3000, []string{"/bin/echo", "hello-proc"})
|
|
const pidOff, statusOff int32 = 4000, 4004
|
|
|
|
if rc := w.Proc_spawn(m, pathOff, argvOff, 0, -1, -1, -1, 0, pidOff); rc != _wasiESUCCESS {
|
|
t.Fatalf("Proc_spawn rc=%d", rc)
|
|
}
|
|
pid := int32(binary.LittleEndian.Uint32(m.memory[pidOff:]))
|
|
if pid == 0 {
|
|
t.Fatal("pid not written")
|
|
}
|
|
if rc := w.Proc_wait(m, pid, 0, statusOff); rc != _wasiESUCCESS {
|
|
t.Fatalf("Proc_wait rc=%d", rc)
|
|
}
|
|
status := int32(binary.LittleEndian.Uint32(m.memory[statusOff:]))
|
|
if exit := (status >> 8) & 0xff; exit != 0 {
|
|
t.Errorf("exit code = %d, want 0 (status=%#x)", exit, status)
|
|
}
|
|
if got := out.String(); got != "hello-proc\n" {
|
|
t.Errorf("child stdout = %q, want %q", got, "hello-proc\n")
|
|
}
|
|
}
|
|
|
|
func TestWasi_ProcSpawnExitCode(t *testing.T) {
|
|
if _, err := os.Stat("/bin/sh"); err != nil {
|
|
t.Skip("/bin/sh not available")
|
|
}
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
w.stdout, w.stderr, w.stdin = &bytes.Buffer{}, &bytes.Buffer{}, bytes.NewReader(nil)
|
|
|
|
pathOff := writeCStr(m, 1000, "/bin/sh")
|
|
argvOff := writeArgv(m, 2000, 3000, []string{"/bin/sh", "-c", "exit 7"})
|
|
const pidOff, statusOff int32 = 4000, 4004
|
|
|
|
if rc := w.Proc_spawn(m, pathOff, argvOff, 0, -1, -1, -1, 0, pidOff); rc != _wasiESUCCESS {
|
|
t.Fatalf("Proc_spawn rc=%d", rc)
|
|
}
|
|
pid := int32(binary.LittleEndian.Uint32(m.memory[pidOff:]))
|
|
if rc := w.Proc_wait(m, pid, 0, statusOff); rc != _wasiESUCCESS {
|
|
t.Fatalf("Proc_wait rc=%d", rc)
|
|
}
|
|
status := int32(binary.LittleEndian.Uint32(m.memory[statusOff:]))
|
|
if exit := (status >> 8) & 0xff; exit != 7 {
|
|
t.Errorf("exit code = %d, want 7", exit)
|
|
}
|
|
}
|
|
|
|
func TestWasi_ProcSpawnExecHookDenies(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
w.stdout, w.stderr, w.stdin = &bytes.Buffer{}, &bytes.Buffer{}, bytes.NewReader(nil)
|
|
var seen string
|
|
w.SetExecHook(func(path string, argv []string) bool { seen = path; return false })
|
|
|
|
pathOff := writeCStr(m, 1000, "/bin/echo")
|
|
argvOff := writeArgv(m, 2000, 3000, []string{"/bin/echo", "x"})
|
|
if rc := w.Proc_spawn(m, pathOff, argvOff, 0, -1, -1, -1, 0, 4000); rc != -_wasiEACCES {
|
|
t.Fatalf("denied spawn rc=%d, want -EACCES", rc)
|
|
}
|
|
if seen != "/bin/echo" {
|
|
t.Errorf("exec hook saw %q", seen)
|
|
}
|
|
}
|
|
|
|
func TestWasi_ProcSpawnNotFound(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
w.stdout, w.stderr, w.stdin = &bytes.Buffer{}, &bytes.Buffer{}, bytes.NewReader(nil)
|
|
pathOff := writeCStr(m, 1000, "/no/such/binary-xyz")
|
|
argvOff := writeArgv(m, 2000, 3000, []string{"/no/such/binary-xyz"})
|
|
if rc := w.Proc_spawn(m, pathOff, argvOff, 0, -1, -1, -1, 0, 4000); rc != -_wasiENOENT {
|
|
t.Fatalf("missing binary rc=%d, want -ENOENT", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_ProcWaitUnknownPID(t *testing.T) {
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
if rc := w.Proc_wait(m, 999999, 0, 4000); rc != -_wasiECHILD {
|
|
t.Errorf("unknown pid rc=%d, want -ECHILD", rc)
|
|
}
|
|
}
|
|
|
|
func TestWasi_ProcWaitWNOHANG(t *testing.T) {
|
|
if _, err := os.Stat("/bin/sh"); err != nil {
|
|
t.Skip("/bin/sh not available")
|
|
}
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
w.stdout, w.stderr, w.stdin = &bytes.Buffer{}, &bytes.Buffer{}, bytes.NewReader(nil)
|
|
pathOff := writeCStr(m, 1000, "/bin/sh")
|
|
argvOff := writeArgv(m, 2000, 3000, []string{"/bin/sh", "-c", "sleep 0.3; exit 3"})
|
|
const pidOff, statusOff int32 = 4000, 4004
|
|
if rc := w.Proc_spawn(m, pathOff, argvOff, 0, -1, -1, -1, 0, pidOff); rc != _wasiESUCCESS {
|
|
t.Fatalf("spawn rc=%d", rc)
|
|
}
|
|
pid := int32(binary.LittleEndian.Uint32(m.memory[pidOff:]))
|
|
// Immediately: WNOHANG should report not-ready (EAGAIN), child still running.
|
|
if rc := w.Proc_wait(m, pid, 1, statusOff); rc != -_wasiEAGAIN {
|
|
t.Fatalf("WNOHANG early rc=%d, want -EAGAIN", rc)
|
|
}
|
|
// Block until done.
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
rc := w.Proc_wait(m, pid, 1, statusOff)
|
|
if rc == _wasiESUCCESS {
|
|
break
|
|
}
|
|
if rc != -_wasiEAGAIN {
|
|
t.Fatalf("WNOHANG poll rc=%d", rc)
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("child never finished")
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
status := int32(binary.LittleEndian.Uint32(m.memory[statusOff:]))
|
|
if exit := (status >> 8) & 0xff; exit != 3 {
|
|
t.Errorf("exit=%d, want 3", exit)
|
|
}
|
|
}
|
|
|
|
func TestWasi_PipeCapture(t *testing.T) {
|
|
if _, err := os.Stat("/bin/echo"); err != nil {
|
|
t.Skip("/bin/echo not available")
|
|
}
|
|
w, m := newTestStubs(t, t.TempDir())
|
|
w.stdin, w.stdout, w.stderr = bytes.NewReader(nil), &bytes.Buffer{}, &bytes.Buffer{}
|
|
|
|
const pipeOut int32 = 5000
|
|
if rc := w.Pipe(m, pipeOut); rc != _wasiESUCCESS {
|
|
t.Fatalf("Pipe rc=%d", rc)
|
|
}
|
|
readFd := int32(binary.LittleEndian.Uint32(m.memory[pipeOut:]))
|
|
writeFd := int32(binary.LittleEndian.Uint32(m.memory[pipeOut+4:]))
|
|
|
|
pathOff := writeCStr(m, 1000, "/bin/echo")
|
|
argvOff := writeArgv(m, 2000, 3000, []string{"/bin/echo", "captured-output"})
|
|
const pidOff, statusOff int32 = 4000, 4004
|
|
if rc := w.Proc_spawn(m, pathOff, argvOff, 0, -1, writeFd, -1, 0, pidOff); rc != _wasiESUCCESS {
|
|
t.Fatalf("Proc_spawn rc=%d", rc)
|
|
}
|
|
pid := int32(binary.LittleEndian.Uint32(m.memory[pidOff:]))
|
|
|
|
w.mu.Lock()
|
|
wop := w.fdTable[writeFd]
|
|
rop := w.fdTable[readFd]
|
|
w.mu.Unlock()
|
|
if wop != nil && wop.f != nil {
|
|
if err := wop.f.Close(); err != nil {
|
|
t.Fatalf("close write end: %v", err)
|
|
}
|
|
}
|
|
buf := make([]byte, 256)
|
|
var got []byte
|
|
for {
|
|
n, err := rop.f.Read(buf)
|
|
got = append(got, buf[:n]...)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
if rc := w.Proc_wait(m, pid, 0, statusOff); rc != _wasiESUCCESS {
|
|
t.Fatalf("Proc_wait rc=%d", rc)
|
|
}
|
|
if string(got) != "captured-output\n" {
|
|
t.Errorf("captured = %q, want %q", string(got), "captured-output\\n")
|
|
}
|
|
}
|