unity-dotnet provider: the guest's registers stay visible to the collector across a GC-safe wait

1846e66a kept a call's result across the runtime's transition hook in a wasm local. While the hook
waits out another thread's collection (a futex syscall through the guest context, which holds the
syscall's number and arguments meanwhile), that local was the result's only reference, and the
collector cannot see wasm locals. Allocation in the provider goes straight to the major heap, so
the object was freed and its slot handed to the next allocation: the embed worker's held strings
became other strings or "" (1 in 300 repeat runs; 1 in 8 under EMBED_WORKER_STRESS=20; 4 in 16 with
the sweep in the pause).

gexp_return keeps rax/rdx, and gexp_enter keeps rax and the argument registers, in a volatile
array on the C stack, which the collector scans, and puts them back after the hook.

With p33: 24 of 24 stress runs clean with no-lazy-sweep,no-concurrent-sweep.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_781bad2ec05143db9067c86b4b2678e2
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-10 21:52:36 -04:00
1 parent d1539ced0d
commit 352a273777
1 file changed
+14 -4
+14 -4
View File
@@ -61,7 +61,14 @@ static inline void gexp_enter_(int ctx, int32_t depth)
}
gexp_ctx = ctx;
gexp_depth = depth;
/* The runtime's hook may wait out a collection in a syscall, which puts
* its own number and arguments in rax, rdi, rsi, rdx, r10, r8 and r9 and
* keeps the guest's values in wasm locals, where a collector scanning the
* context cannot see them. These copies on the C stack it does see. */
volatile uint64_t keep[7] = { GEXP_X(ctx, 0), GEXP_X(ctx, 7), GEXP_X(ctx, 6), GEXP_X(ctx, 2), GEXP_X(ctx, 1), GEXP_X(ctx, 8), GEXP_X(ctx, 9) };
gexp_from_guest();
__asm__ volatile("" : : "r"(keep) : "memory");
GEXP_X(ctx, 0) = keep[0];
}
#define GEXP_SYSV(name) int name(int ctx, int32_t depth, uint32_t need, uint64_t ret)
@@ -98,11 +105,14 @@ static inline int gexp_return(int ctx, uint64_t ret)
* may make a syscall through this context (a GC-safe transition that
* waits on a futex while a collection runs), and a syscall's result
* lands in rax. Without this, a worker returning while another thread
* collected saw 0 for its call's result. */
uint64_t rax = GEXP_X(ctx, 0), rdx = GEXP_X(ctx, 2);
* collected saw 0 for its call's result. The copy is in memory (the C
* stack, which the collector scans), not a wasm local: while the hook
* waits, it is the result's only reference. */
volatile uint64_t keep[2] = { GEXP_X(ctx, 0), GEXP_X(ctx, 2) };
gexp_to_guest();
GEXP_X(ctx, 0) = rax;
GEXP_X(ctx, 2) = rdx;
__asm__ volatile("" : : "r"(keep) : "memory");
GEXP_X(ctx, 0) = keep[0];
GEXP_X(ctx, 2) = keep[1];
uint64_t sp = GEXP_X(ctx, 4);
uint64_t ra = GEXP_MEM64(sp);
GEXP_X(ctx, 4) = sp + 8;