Skip to content

A temporary a finally reads is released after the finally - #650

Merged
ASDAlexander77 merged 4 commits into
mainfrom
fix-rc-temporary-read-in-finally
Oct 23, 2026
Merged

ASDAlexander77 merged 4 commits into
mainfrom
fix-rc-temporary-read-in-finally

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Fixes fuzzer bug R6 (#646): a break, continue or return out of a try, when the finally reads a const of the enclosing block.

for (..) {
    const v = mv(6);
    try { if (c) continue; } finally { s += v.get(); }   // SIGSEGV / 0xC0000374
}

Why

const v = mv(6) is a folded const: the call's result is named directly, with no slot. releaseDiscardedTemporaries releases it where its block ends, and before each break, continue or return that leaves the block early. That release ran before the finally the jump goes through, so the finally read a freed block. #600 fixed this for locals that live in slots (finallyGuardScope + mlirGenScopeExitAfterFinally), but folded consts are released by this pass instead, which knew nothing about finallys.

Fix

When a jump passes through a finally between it and the temporary's block, the release moves to the end of the outermost such finally (the last one the jump runs). It goes inside a ts.If true tagged for that jump (JUMP_ONLY_ATTR_NAME with break:/continue: + label, or RETURN_ONLY_ATTR_NAME). TryOpLowering already keeps such a group only in that jump's copy of the finally, so this reuses MLIRGen's scheme for slot locals. Jumps with the same key share one copy, so each gets one release. A jump that passes through no finally is unchanged.

The release moves even when no finally reads the temporary (follow-up commit 4d07298). A finally that leaves by a jump of its own (try { continue; } finally { break; }) never reaches the end of the finally, and the jump's own release is then the only one. Before, the temporary was released in front of the continue and again in front of the break. The fuzzer found this past its first 1000 programs, and jumpInFinally in the test covers it.

The pass runs in every memory model, so this also fixes the own forms of R6 (R6-minimal, R6-own, R6-call-in-finally crashed under own as well).

Tests

tests/00temporary_read_in_finally.ts covers:

  • continue and break through a finally
  • two continues through one finally
  • a labelled continue through two nested finallys
  • a return through a finally
  • a call in the finally

It runs compiled, under JIT, and in the rc/none corpus, and it crashes under rc on Linux without the fix. The ownership verifier (--verify-ownership) reports no errors on it.

Verification

🤖 Generated with Claude Code

ASDAlexander77 and others added 4 commits October 23, 2026 05:06
`const v = mv(); try { if (c) continue; } finally { s += v.get(); }`: a
folded const (a call's result named by a const) is released where its
block ends, and in front of each break, continue or return that leaves
the block early. That release ran before the finally the jump goes
through, and the finally then read a freed block. #600 fixed the same
for locals in slots; a folded const is released by
releaseDiscardedTemporaries instead.

When a finally between the jump and the temporary's block reads it, the
release now goes at the end of the outermost such finally, in a ts.If
tagged with the jump (JUMP_ONLY_ATTR_NAME, RETURN_ONLY_ATTR_NAME), so
TryOpLowering keeps it in that jump's copy of the finally alone - the
scheme MLIRGen uses for the scopes the jump leaves. Jumps with one key
share one copy and get one release. Fuzzer (#646) R6, rc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`for (..) { const v = mv(); try { try { continue; } finally { break; } }
finally {} }`: the temporary was released in front of the `continue`,
and again in front of the `break` its finally then took. The release
for a jump that passes through a finally now always goes at the end of
the outermost such finally, tagged with the jump, not only when the
finally reads the temporary: a finally that leaves by a jump of its own
never reaches it, and the jump's own release is then the only one.
Fuzzer (#646), found past the first 1000 programs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 4c18e7f into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-rc-temporary-read-in-finally branch October 23, 2026 09:15
ASDAlexander77 added a commit that referenced this pull request Oct 23, 2026
…nt-slot-assigned

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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