unity-dotnet wasm JIT: a negative 32-bit constant is sign-extended as a native int (patch 0004; Hollow Knight's fault in SpanHelpers.ClearWithReferences); embed checks Array.Clear/Span.Clear under the JIT

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_d2453757b8a84fdbb4ee7c9e093141df
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-07 18:18:29 -04:00
1 parent 5c16a1400b
commit a1b2f22f95
4 files changed
+64 -1

No files matched your search

+6
View File
@@ -224,6 +224,12 @@ main (int argc, char **argv)
}
pthread_join (t, NULL);
printf ("worker thread: %d bad of 640\n", worker_bad);
P clears = mono_class_get_method_from_name (k, "Clears", 1);
int cn = 300;
void *clargs [1] = { &cn };
P clr = mono_runtime_invoke (clears, NULL, clargs, &exc);
check ("Clears", exc);
printf ("%s\n", mono_string_to_utf8 (clr));
/* The liveness walk Unity's UnloadUnusedAssets runs: every Leaf reachable from the root,
* each reported once, in batches of at most 16. */
P lvg = mono_class_get_method_from_name (k, "LvGraph", 1);
+1 -1
View File
@@ -26,7 +26,7 @@ PATTERNS=("Compute(50) = 50002" "Hello = hello gex from t ptr=8"
"Guest = twice 42; concat guest strings; mix 323.25; scale 2,4,6; big 11,25,52,3; caught from the guest"
"Back = callback 806.50; unmanaged 1015" "Work(200) = 24983091"
"held across collections: 64 of 64 intact" "worker thread: 0 bad of 640"
"liveness: 301 leaves, 0 reported twice, batched" "gc ok")
"clears 643296" "liveness: 301 leaves, 0 reported twice, batched" "gc ok")
for p in "${PATTERNS[@]}"; do grep -a -q -x -F "$p" out.txt || { echo "FAIL: no '$p'"; exit 1; }; done
# The same under the function JIT, which must leave the replaced library's own code alone: it once
# followed the PLT into the stub ("x86 Mono stub ... called"), as it followed Muck's into Boehm's GC.
+22
View File
@@ -16,6 +16,28 @@ public class LvRoot { public object[] arr; public Pair p; public Pair[] pairs; p
public static class T {
// 3n+1 distinct Leafs: n in arr (half SubLeaf, each .next the following one, a cycle), one in
// p.b, n in pairs[i].b (pairs[i].a repeats arr[i]), n in list.
// Array.Clear and Span.Clear over reference, value and mixed element types, many offsets
// (Hollow Knight's first wasm-JIT fault was in SpanHelpers.ClearWithReferences under Array.Clear).
public struct Mixed { public object o; public long v; }
public static string Clears(int n) {
long sum = 0;
for (int len = 1; len <= n; len += 7) {
var a = new object[len]; for (int i = 0; i < len; i++) a[i] = i;
int from = len / 3, cnt = len - from - len / 4;
Array.Clear(a, from, cnt);
for (int i = 0; i < len; i++) if (a[i] == null) sum += i + 1;
var b = new int[len]; for (int i = 0; i < len; i++) b[i] = i + 1;
Array.Clear(b, from, cnt);
for (int i = 0; i < len; i++) sum += b[i];
var c = new Mixed[len]; for (int i = 0; i < len; i++) c[i] = new Mixed { o = "s", v = i };
Array.Clear(c, from, cnt);
for (int i = 0; i < len; i++) if (c[i].o == null) sum += 3 * c[i].v + 1;
var l = new List<string>(); for (int i = 0; i < len; i++) l.Add("x"); l.Clear(); sum += l.Count;
new Span<object>(a).Slice(len / 2).Clear();
for (int i = 0; i < len; i++) if (a[i] == null) sum += 1;
}
return "clears " + sum;
}
public static object LvGraph(int n) {
var r = new LvRoot { arr = new object[n], pairs = new Pair[n], list = new List<Leaf>(), s = "not a leaf" };
for (int i = 0; i < n; i++) r.arr[i] = i % 2 == 0 ? new SubLeaf { id = i } : new Leaf { id = i };
@@ -0,0 +1,35 @@
diff --git a/src/mono/mono/mini/mini-wasm-emitter.inc b/src/mono/mono/mini/mini-wasm-emitter.inc
index 9919357ca..11bfda8b6 100644
--- a/src/mono/mono/mini/mini-wasm-emitter.inc
+++ b/src/mono/mono/mini/mini-wasm-emitter.inc
@@ -635,6 +635,15 @@ mono_wasm_emit_method (MonoCompile *cfg)
if (wj_ins_is_move (ins->opcode))
continue;
rt = wasm_valtype_of_opcode (ins->opcode);
+#if WJ_W64
+ /* A negative 32-bit constant is sign-extended in its 64-bit register (inst_c0 is the 64-bit value,
+ * and mini-amd64 moves it as one), and mono relies on it: conv.i of a constant is folded away, so
+ * `Unsafe.Add (ref p, -1)` reads the ICONST as a native int. As an i32 local, read by an i64 op,
+ * it would be zero-extended (0xffffffff): SpanHelpers.ClearWithReferences wrote 32 GiB past its
+ * span (Hollow Knight). Such a constant lives in an i64 local; 32-bit readers wrap it. */
+ if (ins->opcode == OP_ICONST && (gint64) ins->inst_c0 < 0 && !(ins->type == STACK_OBJ && ins->inst_p0))
+ rt = WASM_I64;
+#endif
if (!rt && (ins->opcode == OP_CALL || ins->opcode == OP_FCALL || ins->opcode == OP_LCALL || ins->opcode == OP_RCALL
|| ins->opcode == OP_CALL_REG || ins->opcode == OP_FCALL_REG || ins->opcode == OP_LCALL_REG || ins->opcode == OP_RCALL_REG
|| ins->opcode == OP_CALL_MEMBASE || ins->opcode == OP_FCALL_MEMBASE || ins->opcode == OP_LCALL_MEMBASE || ins->opcode == OP_RCALL_MEMBASE)) {
@@ -3704,6 +3713,14 @@ mono_wasm_emit_method (MonoCompile *cfg)
if (!wasm_st_from (&body, &lc, ins->dreg, WASM_PTR)) { fail = "iconst"; goto done; }
break;
} else {
+#if WJ_W64
+ /* the 64-bit register's value (see "negative 32-bit constant" in the valtype pass) */
+ if (ins->dreg >= 0 && ins->dreg < lc.nvreg && lc.vt [ins->dreg] == WASM_I64) {
+ wasm_i64_const (&body, (gint64) ins->inst_c0);
+ if (!wasm_st (&body, &lc, ins->dreg)) { fail = "iconst"; goto done; }
+ break;
+ }
+#endif
wasm_i32_const (&body, (gint32) ins->inst_c0);
}
if (!wasm_st_from (&body, &lc, ins->dreg, WASM_I32)) { fail = "iconst"; goto done; }