Skip to content

rc: a char or number cast to string reports its allocation; own: string length borrows (DefaultLib rc failures 20 -> 3) - #414

Merged
ASDAlexander77 merged 2 commits into
mainfrom
rc-overruns
Sep 30, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
rc-overruns

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR follows #413 and holds two commits:

  • an rc use-after-free that was behind every remaining regexp and replace failure in DefaultLib;
  • a small -mm=own fix.

With them and the companion DefaultLib PR (ASDAlexander77/TypeScriptCompilerDefaultLib#15), DefaultLib under rc goes from 20 failing runs to 3. The 3 left are the known sleep/weakref_basic link errors.

1. A number or char cast to string reports its allocation

switch (c) on a char with string cases casts c to a string once per case, and each of those casts allocates a new string. CastOp::getEffects reported an allocation for only two shapes: a const array cast to an array, and a cast to any. CSE therefore merged the per-case casts into one string.

Under -mm=rc each case retains and then releases its string. With one string shared by all cases:

  1. the first case's release freed it;
  2. every later case retained, compared and released freed memory.

That is a use-after-free plus a double free for every character that misses the first case.

DefaultLib's RegExp constructor reads its flags exactly this way, so the bug was behind these tests: regexp_indices, regexp_test2/3, string_match, string_matchAll (which spun), string_replace* and string_search. On the process heap a freed block usually still holds the right text, so they failed only some runs, looped, or corrupted the heap.

The fix makes getEffects report an allocation for any cast to string that allocates. It uses castToStringAllocates, the same predicate MLIRGen uses to mark the result as owned. That predicate moves next to the op so both places share it.

Test: 00switch_char_string_cases.ts. Without the fix it fails under rc with 0xC0000409 (heap corruption) on both the exe and the JIT build, and passes under gc and none.

2. -mm=own: reading a string's length borrows it

return box.text.length was rejected with "borrows a field and cannot be stored, returned or captured". ts.StringLength was missing from the uses that own's inference counts as borrows, and anything missing from that list is treated as a take. ts.LengthOf was already listed for arrays.

Test: own/own_string_length_borrow.ts. Without the fix it does not compile.

How these were found

The flaky failures that #413 left were invisible to the quarantine heap, which never frees memory. So I temporarily replaced ProcessHeap.h with a guard-page heap. It is a diagnostic only and not part of this PR:

  • each block ends at a page boundary, with an inaccessible page after it;
  • a freed block's pages are decommitted and never reused.

Any overrun or use-after-free then faults at the instruction that does it, and a dump shows where. Two tests hit that heap on their first run:

  • the switch cast above;
  • a write past the end of an array in DefaultLib's fill, which is in the DefaultLib PR.

With both fixed, the whole DefaultLib rc suite runs clean on that heap apart from the sleep/weakref_basic link errors.

Results

before after
Release ctest 3283/3283 (#413) 3292/3292
DefaultLib rc, real heap (compile + JIT) 20 of 316 runs fail 3 of 316 fail (sleep, weakref_basic)
DefaultLib rc, guard-page heap 313/316 (only sleep and weakref_basic)
DefaultLib gc (compile + JIT) 158/158 158/158

Not in this PR

  • sleep.ts and weakref_basic.ts under rc: they reference GC_* symbols, and an rc program links no collector. This is a configuration question, not a memory bug.
  • --di together with --opt: optimized DefaultLib tests fail to compile with "inlinable function call in a function with a DISubprogram location must have a debug location". I haven't looked into it yet.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits September 30, 2026 14:44
…the casts apart

`switch (c)` on a char with string cases casts `c` to a string once per
case. That cast prints into a new block, but CastOp::getEffects reported an
allocation only for a const array made an array and for a cast to `any`, so
CSE merged the per-case casts into one. Under -mm=rc each case retains and
releases its string, and with one string for all of them the first case's
release freed it: every later case retained, compared and released freed
memory, a use after free and a double free for each character that missed
the first case.

This is RegExp's constructor reading its flags in DefaultLib, and it was
behind the regexp and replace failures under rc (regexp_indices,
regexp_test2/3, string_match, string_matchAll, string_replace*,
string_search): on the process heap the freed block usually still held the
right text, so they failed some runs, spun, or corrupted the heap.

getEffects now also reports an allocation for a cast to `string` that
allocates, asking the same castToStringAllocates MLIRGen uses to mark the
result owned; the predicate moves next to the op so both share it.

Test 00switch_char_string_cases.ts (a flags-style switch in a loop). Without
the fix it fails under rc with 0xC0000409 (heap corruption), compile and
JIT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`return box.text.length` was rejected under -mm=own ("'this value' borrows
a field and cannot be stored, returned or captured"): ts.StringLength was
not among the uses the ownership inference counts as borrows, so the
field's string was treated as taken. It reads the string and keeps
nothing, like ts.LengthOf already does for arrays.

Test own/own_string_length_borrow.ts (a field's string length returned
from a function, and a local's). Without the fix it does not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit b6e1958 into main Sep 30, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the rc-overruns branch September 30, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant