From 2033a91fe8e30c0182e809d88cbe7bb3a2cf4f9d Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Mon, 10 Aug 2026 17:31:59 -0700 Subject: [PATCH] test canonical ABI conformance --- NOTICE | 7 + internal/engine/engine.go | 8 + internal/instance/instance.go | 45 ++++- testdata/wasmtime/post_return_scalars.wasm | Bin 0 -> 524 bytes testdata/wasmtime/post_return_scalars.wat | 32 +++ testdata/wasmtime/post_return_string.wasm | Bin 0 -> 257 bytes testdata/wasmtime/post_return_string.wat | 22 ++ testdata/wasmtime/post_return_trap.wasm | Bin 0 -> 170 bytes testdata/wasmtime/post_return_trap.wat | 13 ++ testdata/wasmtime/string_length_overflow.wasm | Bin 0 -> 746 bytes testdata/wasmtime/string_length_overflow.wat | 48 +++++ testdata/wasmtime/string_ptr_oob.wasm | Bin 0 -> 674 bytes testdata/wasmtime/string_ptr_oob.wat | 46 +++++ testdata/wasmtime/string_realloc_oob.wasm | Bin 0 -> 758 bytes testdata/wasmtime/string_realloc_oob.wat | 48 +++++ wasmtime_conformance_test.go | 188 ++++++++++++++++++ 16 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 testdata/wasmtime/post_return_scalars.wasm create mode 100644 testdata/wasmtime/post_return_scalars.wat create mode 100644 testdata/wasmtime/post_return_string.wasm create mode 100644 testdata/wasmtime/post_return_string.wat create mode 100644 testdata/wasmtime/post_return_trap.wasm create mode 100644 testdata/wasmtime/post_return_trap.wat create mode 100644 testdata/wasmtime/string_length_overflow.wasm create mode 100644 testdata/wasmtime/string_length_overflow.wat create mode 100644 testdata/wasmtime/string_ptr_oob.wasm create mode 100644 testdata/wasmtime/string_ptr_oob.wat create mode 100644 testdata/wasmtime/string_realloc_oob.wasm create mode 100644 testdata/wasmtime/string_realloc_oob.wat create mode 100644 wasmtime_conformance_test.go diff --git a/NOTICE b/NOTICE index 4060149..d5aa5b4 100644 --- a/NOTICE +++ b/NOTICE @@ -4,3 +4,10 @@ cd2607360a17a63bd83a6c99e33c4545f467b57e. Copyright 2026 the Wazy authors. Licensed under the Apache License, Version 2.0. See the repository root LICENSE. + +Selected Component Model conformance fixtures and test scenarios are adapted +from github.com/bytecodealliance/wasmtime at commit +899e66bef961f63a795a371a19a1db019ef9e015. + +Copyright the Wasmtime authors. +Licensed under Apache-2.0 WITH LLVM-exception. diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 5c31ecb..5a922b6 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -63,6 +63,14 @@ type Function interface { CallWithStack(context.Context, []uint64) error } +// IsHostFunction reports whether fn is a synthetic function implemented by +// the host. Such functions can only be entered through a core wasm import and +// must not be called directly by component lifecycle code. +func IsHostFunction(fn Function) bool { + _, ok := fn.(*hostFunction) + return ok +} + type Memory interface { Size() uint32 Read(uint32, uint32) ([]byte, bool) diff --git a/internal/instance/instance.go b/internal/instance/instance.go index e5fd72e..fd53cfe 100644 --- a/internal/instance/instance.go +++ b/internal/instance/instance.go @@ -1149,6 +1149,16 @@ func Instantiate(ctx context.Context, r wazy.Runtime, componentBytes []byte, opt // table (not just funcs), and a core func index space where canon-produced // funcs (lower, resource.*) and core-level func aliases interleave. func needsGraphPath(comp *binary.Component) bool { + // The trivial path intentionally binds only the lifted core function and + // does not resolve canonical options. Routing a lift with post-return (or + // any other option carrying runtime behavior) through it would silently + // discard that behavior. The graph path resolves and validates the full + // option set. + for _, canon := range comp.Canons { + if len(canon.Opts) != 0 { + return true + } + } for _, ci := range comp.CoreInstances { if ci.Kind != 0x01 { continue @@ -1725,7 +1735,7 @@ func (in *Instance) invokeEntered(ctx context.Context, be *boundExport, exportNa } putCoreValueSlice(coreArgsPtr) // coreArgs' bits are now copied into stack; done with it - if err := be.coreFn.CallWithStack(ctx, stack); err != nil { + if err := callCoreWithStack(ctx, be.coreFn, stack); err != nil { putUint64Slice(stackPtr) in.poisoned = true // guest code actually ran and trapped -- see this func's doc err = wrapUnreachableTrap(err) @@ -1757,7 +1767,7 @@ func (in *Instance) invokeEntered(ctx context.Context, be *boundExport, exportNa } // post-return takes the same flat results as params; CallWithStack lets // it reuse rawResults' own buffer (the guest reads params, writes none). - if err := be.postReturnFn.CallWithStack(ctx, rawResults); err != nil { + if err := callCoreWithStack(ctx, be.postReturnFn, rawResults); err != nil { putUint64Slice(stackPtr) in.poisoned = true // guest code actually ran and trapped -- see this func's doc return nil, fmt.Errorf("component/instance: export %q: post-return %q: %w", exportName, be.postReturnFuncName, err) @@ -1768,6 +1778,34 @@ func (in *Instance) invokeEntered(ctx context.Context, be *boundExport, exportNa return results, nil } +// callCoreWithStack converts an error panic raised by a Component Model host +// adapter into the trap error returned by the public component call. The core +// engine already returns its own traps as errors, but deliberately re-panics +// unknown host panic values. Canonical ABI adapters use error panics to abort a +// guest call when lifting or lowering detects invalid guest-controlled memory. +// Non-error panics remain programmer bugs and are not hidden. +func callCoreWithStack(ctx context.Context, fn api.Function, stack []uint64) (err error) { + defer func() { + if recovered := recover(); recovered != nil { + if recoveredErr, ok := recovered.(error); ok { + err = recoveredErr + return + } + panic(recovered) + } + }() + // Some core engines use a zero-length stack as the sentinel for "nothing + // to execute" in their optimized path. A real () -> () function must still + // run: post-return functions commonly have that signature and may trap or + // perform mandatory cleanup. Use the ordinary entry point for this one + // shape so it cannot be skipped. + if len(stack) == 0 { + _, err = fn.Call(ctx) + return err + } + return fn.CallWithStack(ctx, stack) +} + // lowerParams lowers each component-level argument into its flattened core // values, in parameter order, using be's precomputed per-param type/ // usesMemory/error (see finalizeBoundExport) instead of recomputing them. @@ -2002,6 +2040,9 @@ func (in *Instance) DropResource(ctx context.Context, iface, resourceName string dtorName := iface + "#[dtor]" + resourceName for _, mod := range in.closers { if fn := safeExportedFunction(mod, dtorName); fn != nil { + if api.IsHostFunction(fn) { + continue + } if _, err := fn.Call(ctx, uint64(rep)); err != nil { return fmt.Errorf("component/instance: DropResource %s/%s: destructor: %w", iface, resourceName, err) } diff --git a/testdata/wasmtime/post_return_scalars.wasm b/testdata/wasmtime/post_return_scalars.wasm new file mode 100644 index 0000000000000000000000000000000000000000..6e9b8bab30799f71ad39441cabfde916b1818b49 GIT binary patch literal 524 zcmYLG%TB{E5S;ZRafzs)UqH{Om$vjA3KbVl+>kh=pjPT3Nu?GLQXe17k1%$E6KS!CYl29)LX0GCa}`zblV#s6Uw;S-^Ak z5^$gOaQZFK0cpJS9!lt$YFVzJq*TTanA{m~)s@jkIka9~y-DDE?uHxF;XYmaZT=V5i<^l8O55Edb6aWAK literal 0 HcmV?d00001 diff --git a/testdata/wasmtime/post_return_scalars.wat b/testdata/wasmtime/post_return_scalars.wat new file mode 100644 index 0000000..75e68e6 --- /dev/null +++ b/testdata/wasmtime/post_return_scalars.wat @@ -0,0 +1,32 @@ +;; Adapted from Wasmtime tests/all/component_model/post_return.rs at +;; 899e66bef961f63a795a371a19a1db019ef9e015. +;; Licensed under Apache-2.0 WITH LLVM-exception. +(component + (core module $m + (func (export "i32") (result i32) i32.const 1) + (func (export "i64") (result i64) i64.const 2) + (func (export "f32") (result f32) f32.const 3) + (func (export "f64") (result f64) f64.const 4) + (func (export "post-i32") (param i32) + local.get 0 i32.const 1 i32.ne if unreachable end) + (func (export "post-i64") (param i64) + local.get 0 i64.const 2 i64.ne if unreachable end) + (func (export "post-f32") (param f32) + local.get 0 f32.const 3 f32.ne if unreachable end) + (func (export "post-f64") (param f64) + local.get 0 f64.const 4 f64.ne if unreachable end) + ) + (core instance $i (instantiate $m)) + (alias core export $i "post-i32" (core func $post-i32)) + (alias core export $i "post-i64" (core func $post-i64)) + (alias core export $i "post-f32" (core func $post-f32)) + (alias core export $i "post-f64" (core func $post-f64)) + (func (export "i32") (result u32) + (canon lift (core func $i "i32") (post-return $post-i32))) + (func (export "i64") (result u64) + (canon lift (core func $i "i64") (post-return $post-i64))) + (func (export "f32") (result float32) + (canon lift (core func $i "f32") (post-return $post-f32))) + (func (export "f64") (result float64) + (canon lift (core func $i "f64") (post-return $post-f64))) +) diff --git a/testdata/wasmtime/post_return_string.wasm b/testdata/wasmtime/post_return_string.wasm new file mode 100644 index 0000000000000000000000000000000000000000..96f4aa667ed2c90b5d999a2069f38ec6b7bd5176 GIT binary patch literal 257 zcmYjM(GI~t5S-n+R%=6oh*v(qYrX1|-ymF&^m?LA@YJ96A@&gQv@<)KnOW=_KLCO5 zP(%PI=Pjt)QU_xglo(ZeGn0Df!}Y;1N7n-O5}Fp&U+@HFRz6X1oKmnYGZD;{ZR{E% zGA5`la&lD_=C^RI4#;i2^A1d&ZA?w4tOVZ>ZSe`{CA0>bG}jw?CujW#Ie`I0*Rf4B fSPl2VUqbEbb{VZH|Cx~dSyPIUKIC<3FFL;f7w{@Q literal 0 HcmV?d00001 diff --git a/testdata/wasmtime/post_return_string.wat b/testdata/wasmtime/post_return_string.wat new file mode 100644 index 0000000..3541e39 --- /dev/null +++ b/testdata/wasmtime/post_return_string.wat @@ -0,0 +1,22 @@ +;; Adapted from Wasmtime tests/all/component_model/post_return.rs at +;; 899e66bef961f63a795a371a19a1db019ef9e015. +;; Licensed under Apache-2.0 WITH LLVM-exception. +(component + (core module $m + (memory (export "memory") 1) + (func (export "get") (result i32) + (i32.store offset=0 (i32.const 8) (i32.const 100)) + (i32.store offset=4 (i32.const 8) (i32.const 11)) + i32.const 8) + (func (export "post") (param i32) + local.get 0 i32.const 8 i32.ne if unreachable end) + (data (i32.const 100) "hello world") + ) + (core instance $i (instantiate $m)) + (alias core export $i "memory" (core memory $memory)) + (alias core export $i "post" (core func $post)) + (func (export "get") (result string) + (canon lift (core func $i "get") + (post-return $post) + (memory $memory))) +) diff --git a/testdata/wasmtime/post_return_trap.wasm b/testdata/wasmtime/post_return_trap.wasm new file mode 100644 index 0000000000000000000000000000000000000000..df0819728b3723965ced6f3bbbb8169ee00ebfe4 GIT binary patch literal 170 zcmX|*Q3}F93`A$L+jiTcqQ?+CfPWsvrSwB+b_?zOF;Vm`D$#50V3Xfq*YIKfw{k>Eer97;3x25Hx*n&h WYb|rxZ_Z=?hp@%j{!-J6>FW(wFc)V+45QXPWoPUB!ghZF!cEJg{DT^MUi|&b1t3^!$3KWS|uG>8q=orUIL5(8W znz84-_nC@)oe@Z#LH!o5W(Oj|l-JyKv{s#b=sb@Jz6V0M@T4EMx~QscNeF6PZ`%`7 z5;Ie(XhkW-swrwsjGC1h7u>VV9B(nGzZXvM4F6bE-+C{_Ku*garDpXM8Lf08jiM~| z?qFuUOv`P(+cvs6EH5f?)ap`ij=IIM@|0kA>E>nIvU5XUSj@a6^~>B*ua9<_)(2Equc6tD!b5!31?e4snwxv*OP zrHOcO$hlnV-3l5}%MoTmG^lj*arivR$;6=K%gxB*MJhErN_t&t!*y^YF7E)@kxKbw1;WEg&PW%n}g97lL2nqg^2YU`czLCfJLK2ToB$fN_L?=~cx0X|Gk&bTwdNwCVbH zf+8mZDPRW@a?vWIfzXh!?UHNap7Sk2@b}UM-eY`H&8r@`nDA--GHK^XfMv-AZ>YM~ z$Ia3_J@*Zy{`4)q3E i7~vbl&a<>GLutwB20dK9yHJ7~K`=j9khO{bseS|1zlM1L literal 0 HcmV?d00001 diff --git a/testdata/wasmtime/string_ptr_oob.wat b/testdata/wasmtime/string_ptr_oob.wat new file mode 100644 index 0000000..ff4ac74 --- /dev/null +++ b/testdata/wasmtime/string_ptr_oob.wat @@ -0,0 +1,46 @@ +;; Adapted from Wasmtime tests/all/component_model/strings.rs at +;; 899e66bef961f63a795a371a19a1db019ef9e015. +;; Licensed under Apache-2.0 WITH LLVM-exception. +(component + (component $receiver + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + i32.const 0) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (alias core export $m "realloc" (core func $realloc)) + (alias core export $m "memory" (core memory $memory)) + (func (export "accept") (param "value" string) + (canon lift (core func $m "") + (realloc $realloc) + (memory $memory) + string-encoding=utf8) + ) + ) + + (component $sender + (import "accept" (func $accept (param "value" string))) + (core module $memory + (memory (export "memory") 1) + ) + (core instance $memory (instantiate $memory)) + (alias core export $memory "memory" (core memory $linear-memory)) + (core func $accept (canon lower (func $accept) + string-encoding=utf8 + (memory $linear-memory))) + (core module $start + (import "" "accept" (func $accept (param i32 i32))) + (func $start + (call $accept (i32.const 0x80000000) (i32.const 1))) + (start $start) + ) + (core instance (instantiate $start + (with "" (instance (export "accept" (func $accept)))))) + ) + + (instance $receiver (instantiate $receiver)) + (instance $sender (instantiate $sender + (with "accept" (func $receiver "accept")))) +) diff --git a/testdata/wasmtime/string_realloc_oob.wasm b/testdata/wasmtime/string_realloc_oob.wasm new file mode 100644 index 0000000000000000000000000000000000000000..c91b9c9b39f569b5678e91f25283122f2045d81c GIT binary patch literal 758 zcmZuvOHRWu5S^JgaT2!`i7J&#RO0^;EZPMpfCDrNUQ{G$m6l4Zasuwa@wfm>B*t}| zKt*K9){N)<9LP515D>%{LVLtVb3y>X5iO``U?Cd*u<<+s^gRTW6Oa3{)>%=ka=@U} z<*I%oh!qj20OlX>fr3a>S*d}rBw+x9V`9{Pju6^OIlvkEyR3NC8!iTXn^&mJ?g=n# zYQT*w&-Hq1upUPFs$8!sU2Ug(mME-su9rJqz(EII>Hdy1hFxEa@ZKPadV?* z2k;0{hA46l8<2%XhH)Mrfr5#p^7l=QI?a^wP^+KAVzLyak26j+h)t-pp9pQxsJLb! zxc73HtPD1~dgfe}$