From af8c737ad573d76dd08cf2927baf2660475c5c01 Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Sat, 12 Sep 2026 18:02:18 -0400 Subject: [PATCH] defer GC work while in `cabi_realloc` Allocations made from `cabi_realloc` may start or finish a GC cycle, and the Go GC calls imports while doing so, which the component model forbids. The monotonic clock is already paused via the adapter, but there are two remaining traps with `cannot leave component instance`: - gcMarkTermination reads the realtime clock (walltime -> wall-clock.now) - gcStart's startTheWorldWithSema polls timers (netpoll -> poll_oneoff) This shows up when a host lowers large results into the guest, e.g. a list of ~40k records (~80k consecutive `cabi_realloc` calls). Wrap the allocation in `runtime.procPin`/`procUnpin`. With the M's lock count raised, the runtime neither starts a GC cycle nor performs assist work, so any GC work is deferred to the next allocation made outside `cabi_realloc`. This also keeps the GC from switching to other goroutines here. The runtime explicitly supports linking to these functions (golang/go#67401). See bytecodealliance/componentize-go#77 and bytecodealliance/wasmtime#14318. Signed-off-by: Bailey Hayes --- wit/runtime/runtime.go | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/wit/runtime/runtime.go b/wit/runtime/runtime.go index 9871799..fc2caea 100644 --- a/wit/runtime/runtime.go +++ b/wit/runtime/runtime.go @@ -109,6 +109,19 @@ func Unpin() { //go:wasmimport wasi_snapshot_preview1 adapter_monotonic_clock_set_paused func adapterMonotonicClockSetPaused(paused bool) +// `procPin` increments the current M's lock count, which prevents the Go +// runtime from starting a GC cycle or performing GC assist work until +// `procUnpin` is called. The runtime explicitly supports linking to these; +// see https://go.dev/issue/67401. + +//nolint:unused +//go:linkname procPin runtime.procPin +func procPin() int + +//nolint:unused +//go:linkname procUnpin runtime.procUnpin +func procUnpin() + //nolint:unused //go:wasmexport cabi_realloc func cabiRealloc(oldPointer unsafe.Pointer, oldSize, align, newSize uintptr) unsafe.Pointer { @@ -117,19 +130,25 @@ func cabiRealloc(oldPointer unsafe.Pointer, oldSize, align, newSize uintptr) uns } if useGCAllocations { - // Here we call `adapter_monotonic_clock_set_paused` before and - // after allocating since the Go garbage collector calls - // `clock_time_get` to measure time spent in various stages of - // GC, but calls to imports from `cabi_realloc` are forbidden by - // the component model, so we must tell the - // `wasi_snapshot_preview1` adapter to use a cached value - // instead of calling `monotonic_clock::now`. + // Calls to imports from `cabi_realloc` are forbidden by the + // component model, so we must not let the Go garbage collector + // run here. A GC cycle may call imports in several ways: it + // reads the monotonic and realtime clocks, polls for I/O when + // restarting the world, and may switch to other goroutines + // (e.g. to park an assist), any of which may then call imports. + // Pinning the M defers any GC work until the next allocation + // made outside of `cabi_realloc`. // - // See https://github.com/bytecodealliance/wasmtime/pull/13563 - // for more details. + // We additionally call `adapter_monotonic_clock_set_paused` + // before and after allocating in case anything else reads the + // monotonic clock while allocating. See + // https://github.com/bytecodealliance/wasmtime/pull/13563 for + // details. + procPin() adapterMonotonicClockSetPaused(true) pointer := Allocate(&pinner, newSize, align) adapterMonotonicClockSetPaused(false) + procUnpin() return pointer } else { alignedSize := newSize + offset(newSize, align)