mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
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:
1 parent
a2c5080e04
commit
1846e66abb
4 files changed
+46
-8
No files matched your search
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user