[CodeGen] Classify the exits of a protected function - #8
Conversation
|
|
|
Hello @claude[bot] 👋 Thank you for submitting a Pull Request (PR) to the LLVM Project. Since this is your first PR, here are a few useful links covering our main contribution policies and review practices.
Please reply to this message to confirm that you have read these policies, especially the LLVM AI Tool Use Policy, and that any AI tool usage has been noted in the PR description. Frequently asked questionsHow do I add reviewers? This PR will be automatically labeled, and the relevant teams will be notified. For some parts of the project, reviewers may also be added automatically. You can also add reviewers manually using the Reviewers section on this page. If you cannot use that section, it is probably because you do not have write permissions for the repository. In that case, you can request a review by tagging reviewers in a comment using What if there are no comments? If you have not received any comments on your PR after a week, you can request a review by pinging the PR with a comment such as “Ping”. The common courtesy ping rate is once a week. Please remember that you are asking for volunteer time from other developers. Are any special GitHub settings required to contribute to LLVM? We only require contributors to have a public email address associated with their GitHub commits, see this section of LLVM Developer Policy for details. If you have questions, feel free to leave a comment on this PR, or ask on LLVM Discord or LLVM Discourse. Thank you, |
Register clearing finds where to emit by walking the blocks whose last instruction is a return, computing one register set for the whole function and handing it to the target at each of them. Finding return blocks answers a different question from the one a function asked to destroy its registers or its frame needs answered, and the two answers differ in both directions. A tail call is a call marked as a return on the targets that have one, so a tail-call block is in the walk; but the epilogue has already run there, the frame belongs to the callee and the outgoing arguments are live, so being visited is not the same as being protected. In the other direction, a landing pad that runs destructors leaves the function by calling the routine that resumes unwinding, and a funclet leaves through cleanupret or catchret. Neither is a return, so neither is in the walk, and no clearing happens on any unwind path today. Classify the exits rather than collect the return blocks. classifyMachineExit gives a block one of seven kinds, decided by what the exit does with the frame and by nothing else, because that is what decides whether a clearing sequence can be placed at it. Four are in scope: a return that is not a call, a tail call, a return out of an exception scope, and a call to the routine that resumes unwinding. That last one is asked for by libcall, _Unwind_Resume or __cxa_end_cleanup under the ARM EH ABI, rather than matched by name, so it is recognised wherever DwarfEHPrepare would have created it. Three are out of scope, for the same reason in each case: the frame is abandoned rather than released, so there is no position at which a sequence could run and still be the last thing to touch it. A call that does not return here, whether abort, exit, a throw with no cleanup in this function or longjmp reached as an ordinary call, hands the caller's context back through the unwinder or through the jump with nothing of ours in between. A non-local jump does the same by reloading another frame's stack and frame pointers. A trap, or an empty block left behind by an unreachable, does not transfer out of the frame at all. The threat model does not cover abandoned frames, and this is where that exclusion is recorded rather than restated at each emission site. Unwinding past a function that has no cleanup in it is excluded for a stronger reason than policy: at this point it is not expressible. A call that may unwind and is not caught here has no edge to anything in this function, so it is indistinguishable from a call that does not unwind, and there is no instruction a sequence could be attached to. A longjmp that crosses this frame from a callee is the same. Nothing of the function runs, so nothing can be put in it. Unreachable is a kind rather than an absence, so that a block which reaches the end of the function without matching any other shape is recorded as classified instead of being indistinguishable from a block the walk failed to reach. The classification runs in prologue and epilogue insertion, immediately in front of the existing register clearing, which is where registers are already allocated, the frame is laid out and frame indices have not been eliminated yet. Nothing consumes it yet, so it is computed only when -pei-print-exits asks for it, and that flag is also how the tests observe it. It prints rather than counting or tracing because a release build has neither statistics nor -debug-only, and a classification that decides what gets protected has to be checkable in the configuration a shipped compiler is built in. Each test pins a whole function's exit list with CHECK-NEXT between the opening and closing lines, so a kind that changes, an exit that appears and an exit that disappears all fail; each of the five distinctions the classifier draws was removed in turn, and each removal failed at least one test. Nothing is emitted differently. The clearing walk, the register set it computes and the diagnostics around it are untouched, the flag is off by default, and a run without it produces no output at all. Ordering the sequence at the return-shaped exits is trailofbits/vspells-ct-internal-notes#19, tail calls are trailofbits/vspells-ct-internal-notes#22, and emission on the unwind and cleanup exits is trailofbits/vspells-ct-internal-notes#25. This is trailofbits/vspells-ct-internal-notes#18, under the umbrella trailofbits/vspells-ct-internal-notes#17.
413a04b to
8ab6b64
Compare
864df60 to
3ba9ac7
Compare
Requested by Francesco Bertolaccini · Slack thread
Register clearing finds where to emit by walking the blocks whose last
instruction is a return, computing one register set for the whole function and
handing it to the target at each of them. Finding return blocks answers a
different question from the one a function asked to destroy its registers or
its frame needs answered, and the two answers differ in both directions.
A tail call is a call marked as a return on the targets that have one, so a
tail-call block is in the walk; but the epilogue has already run there, the
frame belongs to the callee and the outgoing arguments are live, so being
visited is not the same as being protected. In the other direction, a landing
pad that runs destructors leaves the function by calling the routine that
resumes unwinding, and a funclet leaves through cleanupret or catchret. Neither
is a return, so neither is in the walk, and no clearing happens on any unwind
path today.
Classify the exits rather than collect the return blocks. classifyMachineExit
gives a block one of seven kinds, decided by what the exit does with the frame
and by nothing else, because that is what decides whether a clearing sequence
can be placed at it. Four are in scope: a return that is not a call, a tail
call, a return out of an exception scope, and a call to the routine that
resumes unwinding. That last one is asked for by libcall, _Unwind_Resume or
__cxa_end_cleanup under the ARM EH ABI, rather than matched by name, so it is
recognised wherever DwarfEHPrepare would have created it.
Three are out of scope, for the same reason in each case: the frame is
abandoned rather than released, so there is no position at which a sequence
could run and still be the last thing to touch it. A call that does not return
here, whether abort, exit, a throw with no cleanup in this function or longjmp
reached as an ordinary call, hands the caller's context back through the
unwinder or through the jump with nothing of ours in between. A non-local jump
does the same by reloading another frame's stack and frame pointers. A trap, or
an empty block left behind by an unreachable, does not transfer out of the
frame at all. The threat model does not cover abandoned frames, and this is
where that exclusion is recorded rather than restated at each emission site.
Unwinding past a function that has no cleanup in it is excluded for a stronger
reason than policy: at this point it is not expressible. A call that may unwind
and is not caught here has no edge to anything in this function, so it is
indistinguishable from a call that does not unwind, and there is no instruction
a sequence could be attached to. A longjmp that crosses this frame from a
callee is the same. Nothing of the function runs, so nothing can be put in it.
Unreachable is a kind rather than an absence, so that a block which reaches the
end of the function without matching any other shape is recorded as classified
instead of being indistinguishable from a block the walk failed to reach.
The classification runs in prologue and epilogue insertion, immediately in
front of the existing register clearing, which is where registers are already
allocated, the frame is laid out and frame indices have not been eliminated
yet. Nothing consumes it yet, so it is computed only when -pei-print-exits asks
for it, and that flag is also how the tests observe it. It prints rather than
counting or tracing because a release build has neither statistics nor
-debug-only, and a classification that decides what gets protected has to be
checkable in the configuration a shipped compiler is built in. Each test pins a
whole function's exit list with CHECK-NEXT between the opening and closing
lines, so a kind that changes, an exit that appears and an exit that disappears
all fail; each of the five distinctions the classifier draws was removed in
turn, and each removal failed at least one test.
Nothing is emitted differently. The clearing walk, the register set it computes
and the diagnostics around it are untouched, the flag is off by default, and a
run without it produces no output at all.
Ordering the sequence at the return-shaped exits is
trailofbits/vspells-ct-internal-notes#19, tail calls are
trailofbits/vspells-ct-internal-notes#22, and emission on the unwind and
cleanup exits is trailofbits/vspells-ct-internal-notes#25.
This is trailofbits/vspells-ct-internal-notes#18, under the umbrella
trailofbits/vspells-ct-internal-notes#17.
AI tool use
This pull request contains AI-generated content. It was prepared with the assistance of Claude Code; the contributor has reviewed the generated code and text, is the author of the contribution, and is accountable for it, per the LLVM AI Tool Use Policy.
Generated by Claude Code