Symptom
Access violation (c0000005) with empty stderr — the compiled game dies silently or freezes (last presented frame stays on screen, input appears dead). Faulting reads land just past the end of a heap page (0x...FFF8-style addresses). The crash is layout-sensitive: a relink can hide or resurface it, which makes it look unreproducible.
Trigger
Running a string through split() + parseFloat() on a hot path (every frame). Perry's runtime string scanners appear to read one word past the end of their own exact-sized slice allocations; with enough fresh allocations per second, one eventually lands flush against an unmapped page and faults.
We hit this parsing a label|1.23|4.56\n...-style blob returned from a native FFI every frame: 6/6 crashes within 7-29 seconds, reproduced across two different link layouts. One-shot parses carry the same risk per call, just with much lower odds.
Minimal repro sketch
- Any FFI (or plain TS source) producing a multi-line
1.23|4.56\n blob.
- Per frame:
for (const line of blob.split('\n')) { const parts = line.split('|'); parseFloat(parts[0]); ... }
- On Windows x64 this AVs within a minute on an affected build.
Workaround we shipped
Replaced the delimited-text ABI with a numeric one (one FFI call per scalar, f64 across the boundary; strings cross whole and are only drawn, never parsed). Also tail-padded engine-allocated FFI strings by 16 zero bytes — note this cannot protect Perry-internal slice allocations, which is why the ABI change was required.
Environment
- perry 0.5.1208 (also reproduced on the 0.5.11xx line)
- Windows 11 Pro 26200, x64, lld-link
- Crash addresses consistently at end-of-heap-page boundaries; minidumps available on request
Symptom
Access violation (c0000005) with empty stderr — the compiled game dies silently or freezes (last presented frame stays on screen, input appears dead). Faulting reads land just past the end of a heap page (
0x...FFF8-style addresses). The crash is layout-sensitive: a relink can hide or resurface it, which makes it look unreproducible.Trigger
Running a string through
split()+parseFloat()on a hot path (every frame). Perry's runtime string scanners appear to read one word past the end of their own exact-sized slice allocations; with enough fresh allocations per second, one eventually lands flush against an unmapped page and faults.We hit this parsing a
label|1.23|4.56\n...-style blob returned from a native FFI every frame: 6/6 crashes within 7-29 seconds, reproduced across two different link layouts. One-shot parses carry the same risk per call, just with much lower odds.Minimal repro sketch
1.23|4.56\nblob.for (const line of blob.split('\n')) { const parts = line.split('|'); parseFloat(parts[0]); ... }Workaround we shipped
Replaced the delimited-text ABI with a numeric one (one FFI call per scalar, f64 across the boundary; strings cross whole and are only drawn, never parsed). Also tail-padded engine-allocated FFI strings by 16 zero bytes — note this cannot protect Perry-internal slice allocations, which is why the ABI change was required.
Environment