Skip to content

[CodeGen] Classify the exits of a protected function - #8

Draft
claude[bot] wants to merge 1 commit into
zeroize-capabilitiesfrom
zeroize-exit-coverage
Draft

[CodeGen] Classify the exits of a protected function#8
claude[bot] wants to merge 1 commit into
zeroize-capabilitiesfrom
zeroize-exit-coverage

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

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

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions

Copy link
Copy Markdown

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.

  • All contributions to LLVM must follow our LLVM AI Tool Use Policy. In particular, if you used AI while working on this PR, remember to add a note to the PR description.
  • The LLVM Code-Review Policy and Practices document contains practical information about the PR process, including how patches are reviewed and accepted, and who can review a PR.
  • Our LLVM Developer Policy describes our expectations for code quality, commit summaries and contains notes on our CI system.

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 questions

How 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 @ followed by their GitHub username.

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,
The LLVM Community

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.
@claude
claude Bot force-pushed the zeroize-capabilities branch from 413a04b to 8ab6b64 Compare August 11, 2026 19:58
@claude
claude Bot force-pushed the zeroize-exit-coverage branch from 864df60 to 3ba9ac7 Compare August 11, 2026 19:58
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.

2 participants