unity-dotnet provider: a call's result survives the runtime's GC transition back to the guest

The worker-thread race in the embed test ("worker thread: N bad of 640", Make or mono_string_to_utf8
returning NULL on the worker, about 1 run in 20; every run under EMBED_WORKER_STRESS) was the
provider ABI, not the JIT or the GC: an adapter puts its result in the guest context's rax, then
gexp_return runs gexp_to_guest, whose runtime hook (Mono's GC-safe transition) can wait out another
thread's collection on a futex. musl-wasm64's syscalls go through the same context and leave their
result in rax, so the guest read 0. The string object was intact and a second conversion of it
succeeded; the provider never saw a NULL.

- gexp_return keeps rax and rdx across gexp_to_guest.
- gexp_call_guest runs gexp_hook_call_guest before it loads the argument registers (al), and reads
  the callee's results before gexp_hook_call_return.
- gexp_call_guest_catching restores the guest's registers after gexp_hook_unwind.
- embed.c checks each Make result at once (counted as bad) and takes EMBED_WORKER_STRESS=N;
  run.sh adds a leg with EMBED_WORKER_STRESS=10 at JIT threshold 2.

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:02:42 -04:00
1 parent a2c5080e04
commit 1846e66abb
4 files changed
+46 -8

No files matched your search

+19 -3
View File
@@ -63,7 +63,9 @@ worker (void *arg)
{
mono_thread_attach (mono_get_root_domain ());
int bad = 0;
for (int round = 0; round < 20; round++) {
/* EMBED_WORKER_STRESS=N: N times the rounds here and of Churn on the main thread. */
int rounds = getenv ("EMBED_WORKER_STRESS") ? 20 * atoi (getenv ("EMBED_WORKER_STRESS")) : 20;
for (int round = 0; round < rounds; round++) {
P held [32];
P exc = NULL;
for (int i = 0; i < 32; i++) {
@@ -75,6 +77,19 @@ worker (void *arg)
P s = mono_object_to_string (exc, NULL);
fprintf (stderr, "worker: Make(%d) threw: %.300s\n", v, s ? mono_string_to_utf8 (s) : "?");
}
/* Checked at once too: a provider call's result lost to a syscall in its GC transition (the
* mono_string_to_utf8 and Make results of this thread read 0 while Churn collected) showed here. */
{
char want [128];
snprintf (want, sizeof want, "object %d %.*s", v, v % 50, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
const char *got = held [i] ? mono_string_to_utf8 (held [i]) : NULL;
if (!got || strcmp (got, want)) {
bad++;
unsigned long long *w = (unsigned long long *) held [i];
fprintf (stderr, "worker: Make(%d) returned %p = \"%.60s\" at once; words %llx %llx %llx %llx\n", v, held [i], got ? got : "(null)",
w ? w [0] : 0, w ? w [1] : 0, w ? w [2] : 0, w ? w [3] : 0);
}
}
}
volatile unsigned long spin = 0;
for (unsigned long k = 0; k < 2000000; k++)
@@ -243,12 +258,13 @@ main (int argc, char **argv)
g_make = make;
pthread_t t;
pthread_create (&t, NULL, worker, NULL);
for (int r = 0; r < 4; r++) {
int crounds = getenv ("EMBED_WORKER_STRESS") ? 4 * atoi (getenv ("EMBED_WORKER_STRESS")) : 4;
for (int r = 0; r < crounds; r++) {
mono_runtime_invoke (churn, NULL, cargs, &exc);
check ("Churn", exc);
}
pthread_join (t, NULL);
printf ("worker thread: %d bad of 640\n", worker_bad);
printf ("worker thread: %d bad of 640\n", getenv ("EMBED_WORKER_STRESS") ? (worker_bad ? 1 : 0) : worker_bad);
P clears = mono_class_get_method_from_name (k, "Clears", 1);
int cn = 300;
void *clargs [1] = { &cn };
+8
View File
@@ -49,6 +49,14 @@ GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 600 "$GEXC"
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > wasmjit10.txt 2>&1 || true
for p in "${PATTERNS[@]}"; do grep -a -q -x -F "$p" wasmjit10.txt || { echo "FAIL (wasm JIT, threshold 10): no '$p'"; exit 1; }; done
if grep -a -q '^WASM_JIT_INVALID' wasmjit10.txt; then echo "FAIL (wasm JIT, threshold 10): a module did not validate"; grep -a '^WASM_JIT_INVALID' wasmjit10.txt | head -3 | cut -c1-200; exit 1; fi
# The worker thread under load (EMBED_WORKER_STRESS: 10 times its rounds and the main thread's
# collections): a provider call's result in rax was overwritten by a syscall in the runtime's GC-safe
# transition on the way back to the guest (a futex while another thread collected), so the worker
# saw NULL from Make and from mono_string_to_utf8 in about 1 run in 20 (every run under this load).
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 600 "$GEXC" run --mem64 \
--env GEXMONO_WASM_JIT=1 --env MONO_WASM_JIT_THRESHOLD=2 --env EMBED_WORKER_STRESS=10 \
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > stress.txt 2>&1 || true
grep -a -q -x -F "worker thread: 0 bad of 640" stress.txt || { echo "FAIL (worker under load)"; grep -a '^worker' stress.txt | head -3; exit 1; }
# The liveness walk as it was before the class shapes (field metadata per object): the same set.
GEX_WASM_DLLS=libmonobdwgc-2.0.so="$OUT/provider/mono.wasm" timeout 300 "$GEXC" run --mem64 --env GEXMONO_LV_NOSHAPE=1 \
--env GEXMONO_ASSEMBLIES="$T/lib" ./embed ./libmonobdwgc-2.0.so "$T/asm/t.dll" > noshape.txt 2>&1 || true
+11 -5
View File
@@ -114,6 +114,11 @@ void gexp_call_guest(uint64_t fn, gexp_call *c)
int ctx = gexp_ctx;
int32_t depth = gexp_depth;
static const unsigned char sysv[6] = { 7, 6, 2, 1, 8, 9 }, win[4] = { 1, 2, 8, 9 };
/* The runtime's hook first: it may make a syscall (a GC-safe transition
* that waits out a collection), which goes through this context and
* leaves its result in rax, so it must not run once the arguments
* (al, the vector count) are in place. */
void *cookie = gexp_hook_call_guest();
uint64_t rsp0 = GEXP_X(ctx, 4);
/* Below the caller's frame (and any red zone), 16-aligned after the
* stack arguments, so RSP + 8 is 16-aligned at the callee's entry. */
@@ -140,7 +145,6 @@ void gexp_call_guest(uint64_t fn, gexp_call *c)
for (unsigned i = 0; i < c->nxmm && i < 8; i++)
*(volatile uint64_t *)((uintptr_t)(uint32_t)ctx + GEXP_CTX_V + 16 * i) = c->xmm[i];
GEXP_X(ctx, 4) = sp;
void *cookie = gexp_hook_call_guest();
gexp_host_guest_call(1);
in_guest(1);
/* Run the guest until it returns to `ra`. A function body returns 1 when
@@ -164,12 +168,13 @@ void gexp_call_guest(uint64_t fn, gexp_call *c)
__gex_set_ctx((uint32_t)ctx);
in_guest(0);
gexp_host_guest_call(-1);
gexp_hook_call_return(cookie);
GEXP_X(ctx, 4) = rsp0;
/* The results before the runtime's hook, whose syscalls write rax. */
c->rax = GEXP_X(ctx, 0);
c->rdx = GEXP_X(ctx, 2);
c->xmm0 = *(volatile uint64_t *)((uintptr_t)(uint32_t)ctx + GEXP_CTX_V);
c->xmm1 = *(volatile uint64_t *)((uintptr_t)(uint32_t)ctx + GEXP_CTX_V + 16);
gexp_hook_call_return(cookie);
GEXP_X(ctx, 4) = rsp0;
}
/* ---- raising through guest frames ---- */
@@ -212,13 +217,14 @@ uint64_t gexp_call_guest_catching(uint64_t fn, gexp_call *c)
return 0;
/* Unwound: the guest's registers as they were before the call (its
* callee-saved ones were mid-function), this thread's state again. */
for (int i = 0; i < 16; i++)
GEXP_X(k.ctx, i) = k.saved[i];
gexp_ctx = k.ctx;
gexp_depth = k.depth;
__gex_set_ctx((uint32_t)k.ctx);
in_guest(0);
gexp_hook_unwind(mark);
/* After the hook, whose syscalls would write rax. */
for (int i = 0; i < 16; i++)
GEXP_X(k.ctx, i) = k.saved[i];
if (code != GEXP_RAISE_CODE)
gexp_host_throw(code); /* someone else's: keep going */
return k.payload ? k.payload : 1;
+8
View File
@@ -94,7 +94,15 @@ static inline uint64_t gexp_arg_win64(int ctx, unsigned i)
* one the caller predicted (`ret`), as every F_body reports. */
static inline int gexp_return(int ctx, uint64_t ret)
{
/* The adapter has put the result in rax (and rdx); the runtime's hook
* 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);
gexp_to_guest();
GEXP_X(ctx, 0) = rax;
GEXP_X(ctx, 2) = rdx;
uint64_t sp = GEXP_X(ctx, 4);
uint64_t ra = GEXP_MEM64(sp);
GEXP_X(ctx, 4) = sp + 8;