Skip to content

rc: a value stored into a local that owns nothing is not freed under it - #652

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-rc-unowned-local-store
Oct 23, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-rc-unowned-local-store

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Fixes fuzzer bug R9 (#646), plus a broader shape found while reducing it.

let n: N;
if (c) { n = mk(5); } else { n = mk(6); }
churn();
n.v.x;                       // reads a freed block (prints 777, the churn's value)

try {} finally {
    let n10 = mk(5);
    for (..) { p3.v = n10.v; n10 = mk(..); }   // R9: SIGABRT
}

Why

A local declared with no initializer, or inside a catch or finally clause, owns nothing (localTakesOwnership, by design: no retain on store, no release at scope exit). A call's result stored into such a local was still treated as a discarded temporary, so it was released at the end of its own block. When that block is nested inside the local's scope (an if, a loop body), the release came while the local still held the value, and the next read used a freed block. When the store sits in the local's own block, that release happens at the local's scope end and is fine; that's why let n = mk(); n = mk(); in a catch worked.

Fix (rc)

releaseDiscardedTemporaries leaves a value alone when it is stored into an unowned local declared outside the value's block. That value now leaks instead of being freed early, which is the direction this design already takes when it can't tell. Values stored into an unowned local in their own block are still released.

Tests

tests/00unowned_local_store.ts covers:

  • a no-initializer let assigned in an if and in a loop
  • a local of a finally assigned in a loop (the R9 shape)
  • a local of a catch assigned in an if

It fails under rc on Linux without the fix. The ownership verifier reports no errors on it. The test runs compiled, under JIT, and in the rc/none corpus.

Verification

  • ctest -C Release: Windows 4982/4982, WSL Linux 4964/4964.
  • Replaying the fuzzer's repros on Linux: R9-finally-iface-field-store now matches gc.
  • mm_fuzz.py --jobs 10 run --count 500 --seed 1: only the 3 known own failures (R1, O4).

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 23, 2026 05:19
A local declared with no initializer (`let n: N;`), or in a catch or
finally clause, owns nothing (localTakesOwnership): it takes no count
for what it is given. A call's result stored into one was still a
discarded temporary, released at the end of its own block - before the
local's scope ends when the store sits in a nested block:
`let n: N; if (c) { n = mk(); } use(n)`, or a loop in a finally that
assigns a local declared outside it. The local then read a freed block.

releaseDiscardedTemporaries now leaves such a value alone when the
local is declared outside the value's block: a leak, the side this arc
keeps the uncertain case on. Fuzzer (#646) R9; the no-initializer
shape was found while reducing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…store

# Conflicts:
#	tslang/test/tester/CMakeLists.txt
@ASDAlexander77
ASDAlexander77 merged commit f0f1ca6 into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-rc-unowned-local-store branch October 23, 2026 08:38
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