Empty TinyGo project crashes under component adapter
A Fastly Compute project using the TinyGo SDK and having an empty main() crashes when run under viceroy --adapt:
% RUST_BACKTRACE=1 target/debug/viceroy --adapt /Users/me/Desktop/main\ -scheduler=none.wasm
unreachable executed at adapter line 1555: assertion failed
2025-06-05T17:56:20.354549Z ERROR WebAssembly trapped: error while executing at wasm backtrace:
0: 0x173da - wit-component:adapter:wasi_snapshot_preview1!random_get
1: 0x18797 - wit-component:shim!adapt-wasi_snapshot_preview1-random_get
2: 0x459 - __wasi_random_get
at /Users/runner/work/tinygo/tinygo/lib/wasi-libc/libc-bottom-half/sources/__wasilibc_real.c:598:19
3: 0x47a - __getentropy
at /Users/runner/work/tinygo/tinygo/lib/wasi-libc/libc-bottom-half/sources/getentropy.c:11:13
4: 0xa01 - arc4random_buf
at /Users/runner/work/tinygo/tinygo/lib/wasi-libc/libc-top-half/sources/arc4random.c:101:13
5: 0xfa3 - arc4random
at /Users/runner/work/tinygo/tinygo/lib/wasi-libc/libc-top-half/sources/arc4random.c:138:5
6: 0x184d - runtime.hardwareRand
at /opt/homebrew/Cellar/tinygo/0.35.0/src/runtime/runtime_tinygowasm.go:106:29
- runtime.init#1
at /opt/homebrew/Cellar/tinygo/0.35.0/src/runtime/algorithm.go:27:22
- runtime.run
at /opt/homebrew/Cellar/tinygo/0.35.0/src/runtime/scheduler_none.go:16:9
- _start
at /opt/homebrew/Cellar/tinygo/0.35.0/src/runtime/runtime_wasmentry.go:20:5
7: 0x17bb6 - wit-component:adapter:wasi_snapshot_preview1!fastly:api/reactor#serve
Caused by:
wasm trap: wasm `unreachable` instruction executed
Stack backtrace:
0: std::backtrace_rs::backtrace::libunwind::trace
at /rustc/90b35a6239c3d8bdabc530a6a0816f7ff89a0aaf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:116:5
1: std::backtrace_rs::backtrace::trace_unsynchronized
at /rustc/90b35a6239c3d8bdabc530a6a0816f7ff89a0aaf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
2: std::backtrace::Backtrace::create
at /rustc/90b35a6239c3d8bdabc530a6a0816f7ff89a0aaf/library/std/src/backtrace.rs:331:13
3: anyhow::error::<impl core::convert::From<E> for anyhow::Error>::from
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/anyhow-1.0.98/src/backtrace.rs:27:14
4: <T as core::convert::Into<U>>::into
at /Users/me/.rustup/toolchains/1.83-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/convert/mod.rs:759:9
5: <T as wasmtime_types::prelude::IntoAnyhow>::into_anyhow
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-types-25.0.3/src/prelude.rs:74:9
6: wasmtime::runtime::trap::from_runtime_box
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/trap.rs:118:34
7: wasmtime::runtime::func::invoke_wasm_and_catch_traps::{{closure}}
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/func.rs:1610:28
8: core::result::Result<T,E>::map_err
at /Users/me/.rustup/toolchains/1.83-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/result.rs:856:27
9: wasmtime::runtime::func::invoke_wasm_and_catch_traps
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/func.rs:1610:9
10: wasmtime::runtime::func::Func::call_unchecked_raw
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/func.rs:1072:9
11: wasmtime::runtime::component::func::Func::call_raw
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/component/func.rs:468:13
12: wasmtime::runtime::component::func::typed::TypedFunc<Params,Return>::call_impl
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/component/func/typed.rs:207:17
13: wasmtime::runtime::component::func::typed::TypedFunc<Params,Return>::call_async::{{closure}}::{{closure}}
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/component/func/typed.rs:189:31
14: wasmtime::runtime::store::<impl wasmtime::runtime::store::context::StoreContextMut<T>>::on_fiber::{{closure}}::{{closure}}
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-25.0.3/src/runtime/store.rs:2107:34
15: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
at /Users/me/.rustup/toolchains/1.83-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/boxed.rs:2454:9
16: wasmtime_fiber::Suspend<Resume,Yield,Return>::execute::{{closure}}
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-fiber-25.0.3/src/lib.rs:201:62
17: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
at /Users/me/.rustup/toolchains/1.83-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:272:9
18: std::panicking::try::do_call
at /Users/me/.rustup/toolchains/1.83-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:557:40
19: std::panicking::try
at /Users/me/.rustup/toolchains/1.83-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:520:19
20: std::panic::catch_unwind
at /Users/me/.rustup/toolchains/1.83-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:358:14
21: wasmtime_fiber::Suspend<Resume,Yield,Return>::execute
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-fiber-25.0.3/src/lib.rs:201:22
22: wasmtime_fiber::unix::fiber_start
at /Users/me/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-fiber-25.0.3/src/unix.rs:207:9
23: _wasmtime_fiber_start_25_0_3
2025-06-05T17:56:20.376644Z ERROR There was an error handling the request error while executing at wasm backtrace:
0: 0x173da - wit-component:adapter:wasi_snapshot_preview1!random_get
...same backtrace as up top.
Note that you need to pull in https://github.com/fastly/Viceroy/pull/489 to get code to successfully run under the adapter at all.
Truths
- The assertion refers to the corruption of the
magic2magic number in theStatestruct. - It can't be blamed on the scheduler, as it crashes even when the build is done with
-scheduler=none. It is triggered by the presence of GC: a build using-scheduler=none -gc=leakingdoes not crash. (These can both be adjusted in fastly.toml's[scripts]section.) - Building the same project with Big Go does not crash. (
GOARCH=wasm GOOS=wasip1 go build -o bin/main.wasm ./)
There's a decent chance that this is caused by this line in tinygo.
The component adapter is a Wasm module that doesn't define its own memory; it imports the memory of the main module. To claim some of the memory for its own use, the adapting process increases the size of the main module's linear memory, making it look as if something had done a memory.grow. In theory, language runtimes should allow user code to do memory.grow at any time, but it's a little tricky in the adapter's situation because there, the grow happens before the program starts executing. That means calling memory.size at the very beginning of the program and assuming that all memory up to that size is unused doesn't work.
One way to fix this would be to have tinygo do something like the __heap_end symbol trick that wasi-libc uses.
Ah, the issue appears to be more involved. If I understand this code correctly, TinyGo doesn't support users calling memory.grow, as it assumes its heap is contiguous.
wasm-tools' adapter process supports calling cabi_realloc in the main module instead of doing a raw memory.grow, if cabi_realloc exists, however it doesn't appear that tinygo current exports a cabi_realloc. So wasm-tools is probably falling back to its polyfill which uses memory.grow.
wasm-reduce yields this shorter version which still crashes. I've hand-stripped the producers and target_features sections off.
(module
(type (;0;) (func))
(type (;1;) (func (param i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "random_get" (func (;0;) (type 1)))
(memory (;0;) 2)
(export "memory" (memory 0))
(export "_start" (func 2))
(func (;1;) (type 0)
i32.const 0
i32.const 0
call 0
drop
)
(func (;2;) (type 0)
(local i32 i32)
i32.const 66364
memory.size
i32.const 16
i32.shl
i32.store
i32.const 66496
i32.const 66364
i32.load
i32.const 1
i32.sub
i32.store
i32.const 66496
i32.load
i32.const 0
i32.const 1
memory.fill
call 1
)
)
wasm-reduce is a bit aggressive and randomly mutates instructions. In our case, where the State lives in wasm linear memory, it can quite possibly land on an otherwise unrelated program that simply stores a value overtop of it. "Reduce" is a misnomer.
Here's a more proper but larger reduction from wasm-tools shrink.
The adapter will happily use cabi_realloc(), if it can find it, at adaption time to carve out a chunk of linear memory for itself. As a test, Dan and I added…
//export cabi_realloc
func cabi_realloc(ptr, oldsize, align, newsize unsafe.Pointer) unsafe.Pointer {
var space = make([]byte, int(100_000))
return unsafe.Pointer(&space[0])
}
…to the crashing empty project, and the crash went away. Now we just need to put that over in the SDK, actually use newsize, and keep Go from GCing that RAM.
Solutions I considered:
- Have the adapter add a
cabi_realloc()which callsmalloc(), which allocates a region of memory and hangs onto it from the GC's point of view. Pro: nice and clean, with no trouble calling across language boundaries due to the explicit contract of themallocfunction. Con: TinyGo is trying to remove the export ofmalloc. However, the issue has been open for years, with no comments in the past year. - Do the same as above, but call
runtime.alloc, the lower-level routine which allocates memory but doesn't hang onto it. In order to not have it paved over by future allocations, we'd have to retain a reference to it in a stack frame that outlives the GC. We could smuggle a pointer to it into that frame by stowing it in a wasm global that we splice into the existing module. Pro:runtime.allocmight be more likely to exist in future TinyGo programs. Con: smooshing 2 independently compiled Go programs together (perhaps with different GCs) might not work reliably.
Pursuing number 1. Reasoning:
- Every TinyGo program today includes
malloc. Thus, we should be able to adapt all our existing services reliably. - If
mallocis removed in a future release of TinyGo, we can update our contract to require that authors opt into its inclusion before pushing a new release. Failure to do so will be eagerly caught at service validation time. - Ultimately, we will have our SDKs churn out only components, and no adaptation will be needed.
We'll invoke this functionality only if the module appears to have been generated by TinyGo (according to the producers section, most likely) and doesn't already provide a cabi_realloc().