Repository navigation
A finally that returns, breaks or holds a try works on the exceptional path on Windows - #599
Merged
Merged
Conversation
…l path on Windows
Two shapes crashed tslang in Win32ExceptionPass with 0xC0000005 under every memory model:
function f(pre: string) { let s = pre + "1"; try { h(); } finally { return s; } }
function g(pre: string) {
let s = pre + "2";
try { h(); } finally { try { h(); } catch { print("in " + s); } finally { print("inner " + s); } }
return s;
}
On Windows the copy of a finally that runs while an exception unwinds is a cleanup funclet,
and a cleanup funclet can do neither thing these finallys need:
- It cannot return to normal flow, only carry on unwinding. The `return` branched out of the
cleanup region, so the pass found no `resume` to end it, left the region's `end` null and
dereferenced it.
- It cannot hold pads of its own. TryOpLowering tagged the inner try's pads as top-level, so
the pass closed the cleanup region on reaching them, again with no end. Nesting them inside
the cleanup would not have been enough: LLVM gives a pad in a cleanup funclet no EH state
under the MSVC personality, so the inner throw would not be caught there and the process
would terminate.
TryOpLowering now enters the finally's exceptional-path copy through a catch-all pad when that
finally holds a `return`, a break/continue out of the try, or a try. A `return` (or jump) in
that copy ends the catch first, as one in a catch clause does, so it becomes a catchret and the
exception is swallowed. A copy that runs to its end finishes as a cleanup does (end-catch, then
`resume`), and Win32ExceptionPass turns a catch region that ends in `resume` into a rethrow from
inside the funclet, to the handler the region unwinds to or to the caller. The finally's pad
gets an id now, and a try in that copy of the finally (only that copy: the normal-exit, return
and jump copies run outside the funclet) names it as the funclet it is nested in.
Win32ExceptionPass also reports a region it found no end for with report_fatal_error instead
of dereferencing null.
New test 00try_finally_funclet.ts: return in finally with and without a throw, a try in finally
whose exception carries on out, the same inside an enclosing try that catches, a return in a
finally inside a try that catches, and a break in a finally. Compile + JIT, and TSLANG_CORPUS
(gc, rc, none). The try-in-finally case passes the string as a parameter rather than holding an
owned local: an owned local in a function an exception leaves through a try/finally is a known
rc leak (still open from #595), which the ownership-verifier sweep would report.
Not changed here: a finally (or catch) that leaves only on some paths - `finally { if (c)
return s; }` - still breaks, because the pass's scan assumes a region has one exit. The same
holds for `catch { if (c) return s; ... }` before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m path too
00try_finally_funclet.ts failed on Linux with "terminate called after throwing an instance of
'int'" for `finally { return s; }` and `finally { break; }` when no try further out catches.
There the finally's pad is a cleanup, and an exception with only cleanups to run is never
unwound: the search phase finds no handler and the program terminates before the finally runs,
so its return never gets the chance to swallow the exception.
The finally of a try with no enclosing handler in the function is now entered through a
catch-all landing pad when it can leave (the shape the enclosingHandler case already has): a
`return` or jump in that copy ends the catch first, and a run to the end rethrows. The end-catch
marking of those escaping ops now applies on both paths, as the pad is a catch-all on both.
Verified on Linux (WSL): the new test under compile, JIT and the rc/none corpus, and the 261
try/catch/finally/using/throw/ownership tests; on Windows the same subset (267 tests).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 17, 2026
…very path out of it A `break` or `continue` out of a catch clause crashed with 0x80000003 on Windows (JIT and AOT, every memory model, with or without a finally), and so did a `return` out of one whenever the clause could also fall off its end. Two defects: - TryOpLowering only marked a break/continue out of a catch as leaving the catch when the try had a finally, so without one the jump ended no catch at all. A jump out of a catch nested in another catch ended only one of the two. TSContext::catchesLeft now counts the catch clauses a return, break or continue leaves, and their lowering ends that many. The return or jump in the copy of a finally entered through a catch-all pad (#599) counts that pad as one more. - Win32ExceptionPass gave each catch region a single end: its scan follows the layout and closed the region at the first end-of-catch marker. Every other way out of the clause was left as a bare branch out of the catchpad, with no funclet bundle on its calls, which WinEHPrepare turns into unreachable code. findOtherCatchExits now walks each catch clause by its control flow from the pad (a nested catch first, continuing after its ends), collects every end-of-catch marker and the calls on the way to them, and each extra end gets its own catchret. The same goes for the copy of a finally entered through a catch-all pad (#599) that leaves on some paths only (`finally { if (c) return s; ... }`): its run-to-the-end exit, which rethrows, is now always the region's main end and its `return` the other, where the layout could put the `return` first and give the rethrow path a catchret into a `resume` (SimplifyCFG crashed on it). An invoked end-of-catch marker is also recorded among the region's invokes, so that rethrow goes to the enclosing handler rather than to the caller. 00try_jump_from_catch.ts covers break, continue and return out of a catch with and without a finally, a finally reading a local of the block around the try, a typed catch, a catch nested in another, a loop inside a catch, a labelled continue, and a finally that returns or breaks on some paths only, with and without a try around it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 17, 2026
…very path out of it (#601) A `break` or `continue` out of a catch clause crashed with 0x80000003 on Windows (JIT and AOT, every memory model, with or without a finally), and so did a `return` out of one whenever the clause could also fall off its end. Two defects: - TryOpLowering only marked a break/continue out of a catch as leaving the catch when the try had a finally, so without one the jump ended no catch at all. A jump out of a catch nested in another catch ended only one of the two. TSContext::catchesLeft now counts the catch clauses a return, break or continue leaves, and their lowering ends that many. The return or jump in the copy of a finally entered through a catch-all pad (#599) counts that pad as one more. - Win32ExceptionPass gave each catch region a single end: its scan follows the layout and closed the region at the first end-of-catch marker. Every other way out of the clause was left as a bare branch out of the catchpad, with no funclet bundle on its calls, which WinEHPrepare turns into unreachable code. findOtherCatchExits now walks each catch clause by its control flow from the pad (a nested catch first, continuing after its ends), collects every end-of-catch marker and the calls on the way to them, and each extra end gets its own catchret. The same goes for the copy of a finally entered through a catch-all pad (#599) that leaves on some paths only (`finally { if (c) return s; ... }`): its run-to-the-end exit, which rethrows, is now always the region's main end and its `return` the other, where the layout could put the `return` first and give the rethrow path a catchret into a `resume` (SimplifyCFG crashed on it). An invoked end-of-catch marker is also recorded among the region's invokes, so that rethrow goes to the enclosing handler rather than to the caller. 00try_jump_from_catch.ts covers break, continue and return out of a catch with and without a finally, a finally reading a local of the block around the try, a typed catch, a catch nested in another, a loop inside a catch, a labelled continue, and a finally that returns or breaks on some paths only, with and without a try around it. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two shapes that crashed
tslang --emit=jitwith 0xC0000005 infunction(ts::Win32ExceptionPass)under every-mm:Cause
On Windows, the copy of a
finallythat runs on the exceptional path is a cleanup funclet, and a cleanup funclet can't do what these finallys need:returnbranched out of the cleanup region, so the pass never found aresumeto end it. The region'sendstayed null, and the pass dereferenced it.Fix
return, a break/continue out of the try, or a try, its exceptional-path copy is entered through a catch-all pad instead.returnor jump in that copy ends the catch first, which gives a catchret, and the exception is swallowed.resume.resumebecomes a rethrow from inside the funclet. It goes to the handler the region unwinds to, or to the caller.report_fatal_errorinstead of crashing on a null dereference.Tests
New
00try_finally_funclet.ts(compile + JIT + TSLANG_CORPUS gc/rc/none) covers:All of these also pass with
--opt=falseand as AOT executables. Fullctest -C Release -j 8 --timeout 300was green on the branch base, 0b6859c (4138/4138).mainhas since gained #596 and #597. They merge cleanly with this branch, but I haven't run the merged result locally, so CI covers that.The try-in-finally test passes the string as a parameter instead of an owned local. An owned local in a function that an exception leaves through a try/finally was a known rc leak (still open after #595). The ownership-verifier sweep would flag it, and #596 may have closed it.
Not changed
A region that leaves on only some paths, like
finally { if (c) return s; }orcatch { if (c) return s; ... }(the latter is already broken on main), still breaks. The pass's scan assumes each region has a single exit. That needs CFG-based region membership, so it's left for a follow-up.🤖 Generated with Claude Code