A string-heap collect performed inside a long-running das loop frees (or relocates) string values that are still reachable from the caller's live locals. A file-path string held by the loop later reaches fopen as a dead pointer.
Observed (Windows x64, Release, interp): ~80% of runs of the lint runner's serial mode crashed within 1–2 collects, in two flavors:
CRASH: EXCEPTION_ACCESS_VIOLATION (0xC0000005)
reading address 0x140000001d
[ 0] das::utf8_file_path_to_wide (src/builtin/module_builtin_fio.cpp:340)
[ 1] das::das_fopen_utf8
[ 2] das::builtin_fopen
[ 3] SimNode_ExtFuncCall<...builtin_fopen>::evalPtr
[ 4] FusionPoint_MiscCopyReference::SimNode_CopyReferenceLocAny::eval
...
or
EXCEPTION: can't fopen NULL name
at daslib/fio.das:17:12
The das-side stack of the fopen-NULL flavor: fread(file) where file is the loop variable of a for (file in files) over a local array<string> — the path strings were built by string interpolation during a directory scan, so they live on the string heap.
Repro — check out PR #3733's branch, restore the serial-loop collect it removed (utils/lint/main.das, the // NO maybe_collect here comment marks the spot — re-add maybe_collect(since_collect) after tally_result), rebuild nothing (das-only), then:
bin/Release/daslang.exe utils/lint/main.das -- daslib -q --silent -j 1 --disable LINT019
daslib is 148 files; the collect fires at file 100. 3–5 runs are enough to see both flavors. The runner declares options gc + options persistent_heap; the collect is unsafe(heap_collect(true, false)) (string_heap=true, validate=false). Running with validate=true did not fail earlier than the crash.
Isolation facts:
| shape |
result |
| serial driver loop, collect every 100 files |
crash ~80% within 1–2 collects |
identical collect in worker mode (--paths-from, same for (f in paths) loop, 305 files = 3 collects) |
9/9 clean |
-j 1 --paranoid-only (single lint pass) |
clean |
full three-pass -j 1 |
crash — the perf/style passes' allocations appear to be part of the trigger |
| under-100-file sets (no collect) |
always clean |
The serial driver differs from the worker by the amount of live state in the enclosing frame across the collect (config struct, tally counters, a growing stale_eligible : array<string>, and lint_file's CMRES LintResult return slot) and by call depth. The crash stack goes through FusionPoint_MiscCopyReference / fused SimNode permutations, so a Debug interpreter (fusion bypassed) may not reproduce — worth checking, since that localizes it to root-walking of fused-frame layouts.
Containment already landed (PR #3733): the serial driver no longer collects mid-loop (grow-only); worker mode keeps the collect. So nothing in-tree currently exercises the broken path — this issue is the runtime bug itself.
🤖 Generated with Claude Code
A string-heap collect performed inside a long-running das loop frees (or relocates) string values that are still reachable from the caller's live locals. A file-path string held by the loop later reaches
fopenas a dead pointer.Observed (Windows x64, Release, interp): ~80% of runs of the lint runner's serial mode crashed within 1–2 collects, in two flavors:
or
The das-side stack of the fopen-NULL flavor:
fread(file)wherefileis the loop variable of afor (file in files)over a localarray<string>— the path strings were built by string interpolation during a directory scan, so they live on the string heap.Repro — check out PR #3733's branch, restore the serial-loop collect it removed (
utils/lint/main.das, the// NO maybe_collect herecomment marks the spot — re-addmaybe_collect(since_collect)aftertally_result), rebuild nothing (das-only), then:daslib is 148 files; the collect fires at file 100. 3–5 runs are enough to see both flavors. The runner declares
options gc+options persistent_heap; the collect isunsafe(heap_collect(true, false))(string_heap=true, validate=false). Running withvalidate=truedid not fail earlier than the crash.Isolation facts:
--paths-from, samefor (f in paths)loop, 305 files = 3 collects)-j 1 --paranoid-only(single lint pass)-j 1The serial driver differs from the worker by the amount of live state in the enclosing frame across the collect (config struct, tally counters, a growing
stale_eligible : array<string>, andlint_file's CMRESLintResultreturn slot) and by call depth. The crash stack goes throughFusionPoint_MiscCopyReference/ fusedSimNodepermutations, so a Debug interpreter (fusion bypassed) may not reproduce — worth checking, since that localizes it to root-walking of fused-frame layouts.Containment already landed (PR #3733): the serial driver no longer collects mid-loop (grow-only); worker mode keeps the collect. So nothing in-tree currently exercises the broken path — this issue is the runtime bug itself.
🤖 Generated with Claude Code