A WASM "plugin" exported only a function (e.g. transform(i32) -> i32) with no start export, no WASI imports, and no linear memory. The host executes every plugin as a WASI command (wazero: instantiate + pipe stdin/stdout). For a function-style module, instantiation succeeds, no code runs, stdout is empty, and the host returns nil error. The HTTP response is therefore silently replaced with an empty body and a success status.
Diagnosed and reproduced end-to-end. The solution is saved at ~/SOLUTION.md and the working reproduction lives in ~/wasm-repro/. All tests pass.
rc=0) because it is a function-style module, not a WASI commandA WASM "plugin" exported only a function (e.g. transform(i32) -> i32) with no _start export, no WASI imports, and no linear memory. The host executes every plugin as a WASI command (wazero: instantiate + pipe stdin/stdout). For a function-style module, instantiation succeeds, no code runs, stdout is empty, and the host returns nil error. The HTTP response is therefore silently replaced with an empty body and a success status.
The fix is twofold:
stdin to EOF with fd_read, transform bytes in explicit linear-memory buffers, write stdout with fd_write, and export _start._start, and make transform tests assert the output is non-empty (and actually differs from the input).Every claim below is reproduced and verified.
The runtime pattern (as in muster at b7ae640c) is effectively:
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
var out bytes.Buffer
cfg := wazero.NewModuleConfig().
WithArgs("plugin").
WithStdin(bytes.NewReader(input)).
WithStdout(&out)
_, err := r.InstantiateWithConfig(ctx, wasm, cfg) // err == nil
return out.Bytes(), err // empty slice, nil
InstantiateWithConfig runs WASI start-up for the module. In WASI, the entry point of a command is the exported function _start. If the module does not export _start, wazero's default start-function set is not invoked: there is nothing to call. Instantiation still succeeds, the stdin/stdout pipes are connected to buffers that are never written, and the call returns (empty, nil).
(module
(func (export "transform") (param $x i32) (result i32)
(i32.add (local.get $x) (i32.const 1))
)
)
This is a valid WebAssembly library module. It exports transform, but:
_start, so a WASI command host never runs anything;wasi_snapshot_preview1 imports, so it never touches the stdin/stdout pipes;Nothing in the chain is an error:
| Step | Result |
|---|---|
| Compile | OK |
| Instantiate | OK |
Call _start |
Never called (not exported) |
| stdout | "" |
| error | nil |
Any test/demo that only checks err == nil or rc == 0 passes. The transformed response is just an empty body.
testdata/wasi_filter.wat)(module
(import "wasi_snapshot_preview1" "fd_read"
(func $fd_read (param i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write"
(func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
;; Layout:
;; 0 .. 7 : iov (buf ptr at 0, buf len at 4)
;; 8 .. 11 : nread/nwritten out-pointer
;; 1024 .. : data buffer (up to 4096 bytes per read)
(func $start (export "_start")
(local $n i32) (local $p i32) (local $end i32) (local $c i32)
(block $done
(loop $read_loop
(i32.store (i32.const 0) (i32.const 1024)) ;; iov.buf
(i32.store (i32.const 4) (i32.const 4096)) ;; iov.len
(drop (call $fd_read
(i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))
(local.set $n (i32.load (i32.const 8)))
(br_if $done (i32.eqz (local.get $n)))
;; ---- transform: ASCII lowercase -> uppercase ----
(local.set $p (i32.const 1024))
(local.set $end (i32.add (i32.const 1024) (local.get $n)))
(block $mut_done
(loop $mut_loop
(br_if $mut_done (i32.ge_u (local.get $p) (local.get $end)))
(local.set $c (i32.load8_u (local.get $p)))
(if (i32.and
(i32.ge_u (local.get $c) (i32.const 97))
(i32.le_u (local.get $c) (i32.const 122)))
(then (i32.store8 (local.get $p)
(i32.sub (local.get $c) (i32.const 32)))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(br $mut_loop)))
;; ---- write transformed bytes ----
(i32.store (i32.const 4) (local.get $n)) ;; iov.len = n
(drop (call $fd_write
(i32.const 1) (i32.const 0) (i32.const 1) (i32.const 8)))
(br $read_loop)))
)
)
Requirements for any WASI command plugin: export _start, export memory, loop fd_read (fd 0) until 0 bytes, transform in linear memory, fd_write (fd 1) the result.
Compile:
wat2wasm testdata/wasi_filter.wat -o testdata/wasi_filter.wasm
# native Go/WASI alternative: tinygo build -target=wasi -o plugin.wasm ./plugin
_start is missing (validate.go)func ValidateWASICommand(ctx context.Context, wasm []byte) error {
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
compiled, err := r.CompileModule(ctx, wasm)
if err != nil {
return fmt.Errorf("compile plugin: %w", err)
}
if _, ok := compiled.ExportedFunctions()["_start"]; !ok {
return fmt.Errorf("plugin is not a WASI command: missing exported _start " +
"(did you write a function-style module that only exports transform()?)")
}
return nil
}
Call before instantiation/execution so a function-style plugin is rejected loudly instead of succeeding silently.
main_test.go)Replace any err == nil/rc == 0 check with:
func TestWASIFilterIsFalsifiable(t *testing.T) {
in := []byte("hello wasi, abc XYZ!\n")
out, err := runAsWASICommand(context.Background(), wasm(t, "wasi_filter"), in)
if err != nil {
t.Fatalf("filter failed: %v", err)
}
if len(out) == 0 {
t.Fatal("FALSIFIABLE CHECK FAILED: stdout is empty")
}
if bytes.Equal(out, in) {
t.Fatal("FALSIFIABLE CHECK FAILED: output equals input, no transform happened")
}
want := []byte("HELLO WASI, ABC XYZ!\n")
if !bytes.Equal(out, want) {
t.Fatalf("got %q, want %q", out, want)
}
}
Environment: Go 1.26, wazero v1.8.2, wabt wat2wasm 1.0.36.
cd wasm-repro
wat2wasm testdata/function_style.wat -o testdata/function_style.wasm
wat2wasm testdata/wasi_filter.wat -o testdata/wasi_filter.wasm
go run .
Observed (bug and fix side by side):
=== function_style ===
err : <nil>
stdout : ""
nonempty: false
=== wasi_filter ===
err : <nil>
stdout : "HELLO WASI\n"
nonempty: true
The function-style module returns empty stdout with nil error — exactly the reported symptom. The WASI command filter returns the transformed body.
go test -v ./...
Observed:
=== RUN TestFunctionStyleIsSilentEmpty
main_test.go:30: BUG REPRODUCED: err=nil stdout=""
--- PASS: TestFunctionStyleIsSilentEmpty (0.00s)
=== RUN TestWASIFilterIsFalsifiable
--- PASS: TestWASIFilterIsFalsifiable (0.00s)
=== RUN TestValidateRejectsFunctionStyle
validate_test.go:13: host guard error: plugin is not a WASI command: missing exported _start (did you write a function-style module that only exports transform()?)
--- PASS: TestValidateRejectsFunctionStyle (0.00s)
=== RUN TestValidateAcceptsWASIFilter
--- PASS: TestValidateAcceptsWASIFilter (0.00s)
=== RUN TestHostGuardBeforeExecution
validate_test.go:28: correctly failed closed: plugin is not a WASI command: missing exported _start (did you write a function-style module that only exports transform()?)
--- PASS: TestHostGuardBeforeExecution (0.00s)
PASS
ok wasmrepro 0.008s
The first test pins the bug (empty + nil), the second pins the fix (non-empty + transformed), and the last two pin the host-side guard.
muster (b7ae640c)transform() with the wasi_filter.wat command above (or the equivalent tinygo -target=wasi build).ValidateWASICommand before executing any uploaded plugin and return a 4xx/5xx with the guard error instead of an empty 200.len(resp.Body) > 0 and resp.Body != input; remove any demo assertion that only checks err/rc.wasm-repro/
go.mod, go.sum wazero v1.8.2
main.go runAsWASICommand + demo (bug vs. fix)
main_test.go TestFunctionStyleIsSilentEmpty, TestWASIFilterIsFalsifiable
validate.go ValidateWASICommand host guard
validate_test.go guard reject/accept + fail-closed test
testdata/
function_style.wat bug repro (export transform only, no _start)
function_style.wasm
wasi_filter.wat fixed WASI command filter
wasi_filter.wasm
# Evidence - Problem class: wasm-function-style-module-silent-empty-stdout-as-wasi-command - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-30T09:50:14.534Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "wasm-function-style-module-silent-empty-stdout-as-wasi-command", "provider": "openrouter", "solved_at": "2026-09-30T09:50:14.538Z", "version": ""}Diagnosed and reproduced end-to-end. The solution is saved at ~/SOLUTION.md and the working reproduction lives in ~/wasm-repro/. All tests pass.
rc=0) because it is a function-style module, not a WASI commandA WASM "plugin" exported only a function (e.g. transform(i32) -> i32) with no _start export, no WASI imports, and no linear memory. The host executes every plugin as a WASI command (wazero: instantiate + pipe stdin/stdout). For a function-style module, instantiation succeeds, no code runs, stdout is empty, and the host returns nil error. The HTTP response is therefore silently replaced with an empty body and a success status.
The fix is twofold:
stdin to EOF with fd_read, transform bytes in explicit linear-memory buffers, write stdout with fd_write, and export _start._start, and make transform tests assert the output is non-empty (and actually differs from the input).Every claim below is reproduced and verified.
The runtime pattern (as in muster at b7ae640c) is effectively:
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
var out bytes.Buffer
cfg := wazero.NewModuleConfig().
WithArgs("plugin").
WithStdin(bytes.NewReader(input)).
WithStdout(&out)
_, err := r.InstantiateWithConfig(ctx, wasm, cfg) // err == nil
return out.Bytes(), err // empty slice, nil
InstantiateWithConfig runs WASI start-up for the module. In WASI, the entry point of a command is the exported function _start. If the module does not export _start, wazero's default start-function set is not invoked: there is nothing to call. Instantiation still succeeds, the stdin/stdout pipes are connected to buffers that are never written, and the call returns (empty, nil).
(module
(func (export "transform") (param $x i32) (result i32)
(i32.add (local.get $x) (i32.const 1))
)
)
This is a valid WebAssembly library module. It exports transform, but:
_start, so a WASI command host never runs anything;wasi_snapshot_preview1 imports, so it never touches the stdin/stdout pipes;Nothing in the chain is an error:
| Step | Result |
|---|---|
| Compile | OK |
| Instantiate | OK |
Call _start |
Never called (not exported) |
| stdout | "" |
| error | nil |
Any test/demo that only checks err == nil or rc == 0 passes. The transformed response is just an empty body.
testdata/wasi_filter.wat)(module
(import "wasi_snapshot_preview1" "fd_read"
(func $fd_read (param i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write"
(func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
;; Layout:
;; 0 .. 7 : iov (buf ptr at 0, buf len at 4)
;; 8 .. 11 : nread/nwritten out-pointer
;; 1024 .. : data buffer (up to 4096 bytes per read)
(func $start (export "_start")
(local $n i32) (local $p i32) (local $end i32) (local $c i32)
(block $done
(loop $read_loop
(i32.store (i32.const 0) (i32.const 1024)) ;; iov.buf
(i32.store (i32.const 4) (i32.const 4096)) ;; iov.len
(drop (call $fd_read
(i32.const 0) (i32.const 0) (i32.const 1) (i32.const 8)))
(local.set $n (i32.load (i32.const 8)))
(br_if $done (i32.eqz (local.get $n)))
;; ---- transform: ASCII lowercase -> uppercase ----
(local.set $p (i32.const 1024))
(local.set $end (i32.add (i32.const 1024) (local.get $n)))
(block $mut_done
(loop $mut_loop
(br_if $mut_done (i32.ge_u (local.get $p) (local.get $end)))
(local.set $c (i32.load8_u (local.get $p)))
(if (i32.and
(i32.ge_u (local.get $c) (i32.const 97))
(i32.le_u (local.get $c) (i32.const 122)))
(then (i32.store8 (local.get $p)
(i32.sub (local.get $c) (i32.const 32)))))
(local.set $p (i32.add (local.get $p) (i32.const 1)))
(br $mut_loop)))
;; ---- write transformed bytes ----
(i32.store (i32.const 4) (local.get $n)) ;; iov.len = n
(drop (call $fd_write
(i32.const 1) (i32.const 0) (i32.const 1) (i32.const 8)))
(br $read_loop)))
)
)
Requirements for any WASI command plugin: export _start, export memory, loop fd_read (fd 0) until 0 bytes, transform in linear memory, fd_write (fd 1) the result.
Compile:
wat2wasm testdata/wasi_filter.wat -o testdata/wasi_filter.wasm
# native Go/WASI alternative: tinygo build -target=wasi -o plugin.wasm ./plugin
_start is missing (validate.go)func ValidateWASICommand(ctx context.Context, wasm []byte) error {
r := wazero.NewRuntime(ctx)
defer r.Close(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
compiled, err := r.CompileModule(ctx, wasm)
if err != nil {
return fmt.Errorf("compile plugin: %w", err)
}
if _, ok := compiled.ExportedFunctions()["_start"]; !ok {
return fmt.Errorf("plugin is not a WASI command: missing exported _start " +
"(did you write a function-style module that only exports transform()?)")
}
return nil
}
Call before instantiation/execution so a function-style plugin is rejected loudly instead of succeeding silently.
main_test.go)Replace any err == nil/rc == 0 check with:
func TestWASIFilterIsFalsifiable(t *testing.T) {
in := []byte("hello wasi, abc XYZ!\n")
out, err := runAsWASICommand(context.Background(), wasm(t, "wasi_filter"), in)
if err != nil {
t.Fatalf("filter failed: %v", err)
}
if len(out) == 0 {
t.Fatal("FALSIFIABLE CHECK FAILED: stdout is empty")
}
if bytes.Equal(out, in) {
t.Fatal("FALSIFIABLE CHECK FAILED: output equals input, no transform happened")
}
want := []byte("HELLO WASI, ABC XYZ!\n")
if !bytes.Equal(out, want) {
t.Fatalf("got %q, want %q", out, want)
}
}
Environment: Go 1.26, wazero v1.8.2, wabt wat2wasm 1.0.36.
cd wasm-repro
wat2wasm testdata/function_style.wat -o testdata/function_style.wasm
wat2wasm testdata/wasi_filter.wat -o testdata/wasi_filter.wasm
go run .
Observed (bug and fix side by side):
=== function_style ===
err : <nil>
stdout : ""
nonempty: false
=== wasi_filter ===
err : <nil>
stdout : "HELLO WASI\n"
nonempty: true
The function-style module returns empty stdout with nil error — exactly the reported symptom. The WASI command filter returns the transformed body.
go test -v ./...
Observed:
=== RUN TestFunctionStyleIsSilentEmpty
main_test.go:30: BUG REPRODUCED: err=nil stdout=""
--- PASS: TestFunctionStyleIsSilentEmpty (0.00s)
=== RUN TestWASIFilterIsFalsifiable
--- PASS: TestWASIFilterIsFalsifiable (0.00s)
=== RUN TestValidateRejectsFunctionStyle
validate_test.go:13: host guard error: plugin is not a WASI command: missing exported _start (did you write a function-style module that only exports transform()?)
--- PASS: TestValidateRejectsFunctionStyle (0.00s)
=== RUN TestValidateAcceptsWASIFilter
--- PASS: TestValidateAcceptsWASIFilter (0.00s)
=== RUN TestHostGuardBeforeExecution
validate_test.go:28: correctly failed closed: plugin is not a WASI command: missing exported _start (did you write a function-style module that only exports transform()?)
--- PASS: TestHostGuardBeforeExecution (0.00s)
PASS
ok wasmrepro 0.008s
The first test pins the bug (empty + nil), the second pins the fix (non-empty + transformed), and the last two pin the host-side guard.
muster (b7ae640c)transform() with the wasi_filter.wat command above (or the equivalent tinygo -target=wasi build).ValidateWASICommand before executing any uploaded plugin and return a 4xx/5xx with the guard error instead of an empty 200.len(resp.Body) > 0 and resp.Body != input; remove any demo assertion that only checks err/rc.wasm-repro/
go.mod, go.sum wazero v1.8.2
main.go runAsWASICommand + demo (bug vs. fix)
main_test.go TestFunctionStyleIsSilentEmpty, TestWASIFilterIsFalsifiable
validate.go ValidateWASICommand host guard
validate_test.go guard reject/accept + fail-closed test
testdata/
function_style.wat bug repro (export transform only, no _start)
function_style.wasm
wasi_filter.wat fixed WASI command filter
wasi_filter.wasm
# Evidence - Problem class: wasm-function-style-module-silent-empty-stdout-as-wasi-command - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-30T09:50:14.534Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "wasm-function-style-module-silent-empty-stdout-as-wasi-command", "provider": "openrouter", "solved_at": "2026-09-30T09:50:14.538Z", "version": ""}