Repository navigation
JIT: keep the block a catch returns to non-empty when optimizations are disabled - #135638
Open
pcshrosbree wants to merge 1 commit into
Open
pcshrosbree wants to merge 1 commit into
pcshrosbree wants to merge 1 commit into
Conversation
…re disabled When a catch ends with a thread abort pending, the runtime raises the abort again from the address the catch returns to and looks the handlers up for that address. fgOptimizeEmptyBlock keeps the block a catch returns to from being empty (it adds a GT_NO_OP) when the block is in a different EH region than its successor, so that the address stays inside the block's regions. That only runs when optimizing. In MinOpts and debuggable code the block falls into its successor without generating code, the address is the end of its regions, and the abort is dispatched as if it had been raised in the code that follows: a finally the catch's leave passes through does not run, and the handlers that see the abort differ from optimized code. Move the check into a helper and run it from fgPostImportationCleanup when optimizations are disabled. Optimized code is unchanged. Fixes dotnet#135637
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
Member
|
@EgorBo PTAL. |
This branch has not been deployed
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 #135637.
When a catch block ends with a thread abort pending, the runtime raises the abort again from the address the catch returns to and looks the handlers up for that address (
EECodeManager::ResumeAfterCatch→COMPlusCheckForAbort→ThrowControlForThread, which dispatches from aFaultingExceptionFrameat the exact resume address). The JIT already accounts for that:impImportLeaveplaces the block a catch returns to inside the EH regions that enclose the catch (see its comment onThreadAbortException), andfgOptimizeEmptyBlockkeeps that block from being empty when it is in a different EH region than its successor, by adding aGT_NO_OP, "for re-raise of thread abort exceptions to work". The second part only runs when optimizing: it is part offgUpdateFlowGraph, which assertsOptimizationEnabled().So in MinOpts and debuggable code that block falls into its successor without generating code, the resume address is the end of the block's EH regions, and the abort is dispatched as if it had been raised in the code that follows. In the shapes of #135637 a
finallythe catch'sleavepasses through does not run, an enclosing catch does not see the abort, and atrythat starts right after the catch does.This change moves the check into a helper,
fgKeepCatchRetTargetNonEmpty, and runs it fromfgPostImportationCleanupwhen optimizations are disabled.fgOptimizeEmptyBlockcalls the same helper, so optimized code is unchanged. The change inserts aGT_NO_OPinto each empty block a catch returns to that is in a different EH region than its successor, when optimizations are disabled; that is onenopinstruction per such block.What changes
Checked builds on linux-x64, the JIT from this branch against a JIT built at main 523a36f on the same runtime; three runs per configuration and shape, identical each time. The traces are the handlers that ran; "finally ran" is a call-free
volatilestore.JITMinOpts=1beforeJITMinOpts=1afterTieredCompilation=0(before = after)CatchThenFinallycatch outer, finally not runcatch finally outer, finally rancatch finally outer, finally ranLeaveOutOfOuterTryinnerinner outerinner outerConsecutiveTryinner new catchinnerinnerIn the tier-0 JitDump of
CatchThenFinallythe catch-return blockBB03is now a one-bytenopat0x1Band the finally's try range covers it (EH#1: try [0014..001C); before:[0014..001B)and no code forBB03); the no-op is added in the Post-import phase. TheTieredCompilation=0layouts of the three methods (block list, instruction group offsets, EH table, code size) are identical before and after.Tests
Regressions/coreclr/GitHub_135637/test135637has the three shapes. With the current JIT it fails under tier 0 andJITMinOpts=1and passes underTieredCompilation=0; with this change it passes in all three. That pattern reproduces on linux-x64, on osx-arm64, and on windows-x64 with #135531 applied: on a machine with CET shadow stacks theCatchThenFinallyshape hits the Checked-only assert of #135530 inCallCatchFuncletwith and without this change, because the re-raised abort is caught in the same method, so this test depends on #135531 and should merge after it.The test runs on jitted code only: in the recorded interpreter runs (
DOTNET_InterpMode=1and3) the finally runs, but the outer catch ofLeaveOutOfOuterTrydoes not; I will raise that with the interpreter owners separately.JIT/Methodical(Methodical_d2,_r2,_do,_ro,_others),JIT/jit64(jit64_5) andbaseservices/exceptionson linux-x64 under the default configuration,TieredCompilation=0andJITMinOpts=1, with the JIT before and after the change: no result differs. 74 of the 79 runner scripts pass with both JITs, and the same five (baseservices/exceptions/inproccrashreport/*) fail with both on that machine.Tier-0 code size, jitting with
ReadyToRun=0(CoreLib and the repro program, three runs per JIT, linux-x64): of 2467 tier-0 methods whose size is stable across the runs, 4 grow by one byte each (the three shapes of the repro andSystem.Diagnostics.StackFrameHelper:InitializeSourceInfo); the compared FullOpts and Tier1 methods are unchanged. On arm64 thenopis four bytes.Note
This PR was prepared with AI assistance (Anthropic Claude, checked by OpenAI Codex) at my request. I reviewed the change, the test and the measurements before opening it.