Skip to content

[CodeGen] Order the clearing sequence at every in-scope exit - #9

Draft
claude[bot] wants to merge 1 commit into
zeroize-exit-coveragefrom
zeroize-ordering
Draft

[CodeGen] Order the clearing sequence at every in-scope exit#9
claude[bot] wants to merge 1 commit into
zeroize-exit-coveragefrom
zeroize-ordering

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Requested by Francesco Bertolaccini · Slack thread

Prologue and epilogue insertion emits register clearing at every block whose
last instruction is a return. It is one step with no defined relationship to
anything else that will be emitted there, which has cost nothing so far because
it is the only step there is, and stops being free with the second one.
Clearing the frame cannot be done without registers to hold the address it
stores through and the value it stores, so it ends leaving in registers what it
has just taken out of memory, and a register clear in front of it is undone by
it. Clearing the flags has to be behind everything, because a register is
cleared on x86 with an exclusive-or, which writes them, and a frame clear that
loops writes them too. Those are constraints between steps, and a single
emission has nowhere to record them.

Make the emission point a coordinator. ClearingSequence is the order:
ClearStack, then ClearRegisters, then ClearFlags. The emission walks that array
at each exit and dispatches each step, instead of calling the one step there
is. Two of the three emit nothing yet, and they are here rather than arriving
with their implementations because the order is what is being settled: a step
that arrives without one goes where it is convenient, and the reason it
belonged somewhere else then has to be found again. The comment above the
enumeration is where the reason for each position is written down, because
that, rather than the order itself, is what an implementer of the remaining
steps needs. It also records what the order is over. It is over the emitted
code, not over one insertion point: the steps that exist today all emit in
front of the instruction control leaves through, which is after the epilogue,
while clearing the frame has to happen before the epilogue moves the stack
pointer and the frame stops being addressable as the frame.

What each step does is decided once per function and before anything is
emitted, since a sequence whose steps differed between exits would not be one
sequence. A step is not requested, unsupported and reported, unimplemented, or
emitting. Planning is where the two capability queries are asked and where the
two fail-closed diagnostics are raised, so a function is told what will not
happen for it whether or not it has an exit to emit at. Nothing in the plan or
in the walk consults a value the function computes, so the sequence a protected
function runs is the same on every input.

Where the sequence runs is the exit classification, not a fresh walk over the
blocks that end in a return. The two answers differ in one direction here. A
tail call and a funclet return are both marked as returns in the instruction
description, so blocks ending in TCRETURN, CLEANUPRET or CATCHRET were already
reached by the walk and are reached by the classification too; nothing changes
at them. A landing pad that resumes unwinding ends in a call, which is not
marked as a return, so it was not reached, and a function unwinding out of a
cleanup left every register it had used to the unwinder. That exit is in scope,
and the sequence now runs at it.

Reaching it needs two things the block-scoped emission could not express. The
target used to choose where to emit, the first terminator of the block, and a
landing pad that ends in a call has no terminator, so the clearing would have
been appended after the call that leaves the function. Steps that choose their
own positions cannot be ordered against one another either, so the position is
the coordinator's and is passed to emitZeroCallUsedRegs. At an exit that leaves
through a terminator it is the first terminator, which is where the three
targets implementing the hook already emitted, so nothing they generate moves.
The other is the register set: it is computed once for the function with the
registers its returns read removed, and it does not know that the resume call
reads the exception object in an argument register. Clearing that register
would leave the unwinder nothing to resume with, so the registers an exit names
are dropped at that exit, which at a return exit removes nothing the
function-wide computation had not already removed. Computing the set per exit
instead of narrowing one computed for the function is
trailofbits/vspells-ct-internal-notes#21.

No existing test changes, anywhere in CodeGen/X86 or CodeGen/ARM. Nothing in
the tree combines "zero-call-used-regs" with an unwind path, which is the same
reason the gap lasted this long, so the new site is pinned by a new test rather
than by an old one starting to expect more. The order is pinned separately from
what is emitted, through -pei-print-clearing-sequence, which lists the steps in
sequence order with what each of them does at each in-scope exit; it is a flag
rather than a debug print for the reason -pei-print-exits is, that a sequence
deciding what gets destroyed has to be checkable in the configuration a shipped
compiler is built in. Each new test was confirmed load-bearing by breaking the
implementation once and restoring it: reordering the array, going back to the
walk over return blocks, dropping the out-of-scope filter, letting the target
pick the position again, dropping the per-exit narrowing, and skipping the
funclet exits each failed at least one of them.

The scope is the order and the sites. What the register clear emits is
unchanged, the scratch registers a frame clear will need are
trailofbits/vspells-ct-internal-notes#20, clearing the frame is
trailofbits/vspells-ct-internal-notes#26, and what a tail call needs beyond
being in scope is trailofbits/vspells-ct-internal-notes#22.

This is trailofbits/vspells-ct-internal-notes#19, 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

Prologue and epilogue insertion emits register clearing at every block whose
last instruction is a return. It is one step with no defined relationship to
anything else that will be emitted there, which has cost nothing so far because
it is the only step there is, and stops being free with the second one.
Clearing the frame cannot be done without registers to hold the address it
stores through and the value it stores, so it ends leaving in registers what it
has just taken out of memory, and a register clear in front of it is undone by
it. Clearing the flags has to be behind everything, because a register is
cleared on x86 with an exclusive-or, which writes them, and a frame clear that
loops writes them too. Those are constraints between steps, and a single
emission has nowhere to record them.

Make the emission point a coordinator. ClearingSequence is the order:
ClearStack, then ClearRegisters, then ClearFlags. The emission walks that array
at each exit and dispatches each step, instead of calling the one step there
is. Two of the three emit nothing yet, and they are here rather than arriving
with their implementations because the order is what is being settled: a step
that arrives without one goes where it is convenient, and the reason it
belonged somewhere else then has to be found again. The comment above the
enumeration is where the reason for each position is written down, because
that, rather than the order itself, is what an implementer of the remaining
steps needs. It also records what the order is over. It is over the emitted
code, not over one insertion point: the steps that exist today all emit in
front of the instruction control leaves through, which is after the epilogue,
while clearing the frame has to happen before the epilogue moves the stack
pointer and the frame stops being addressable as the frame.

What each step does is decided once per function and before anything is
emitted, since a sequence whose steps differed between exits would not be one
sequence. A step is not requested, unsupported and reported, unimplemented, or
emitting. Planning is where the two capability queries are asked and where the
two fail-closed diagnostics are raised, so a function is told what will not
happen for it whether or not it has an exit to emit at. Nothing in the plan or
in the walk consults a value the function computes, so the sequence a protected
function runs is the same on every input.

Where the sequence runs is the exit classification, not a fresh walk over the
blocks that end in a return. The two answers differ in one direction here. A
tail call and a funclet return are both marked as returns in the instruction
description, so blocks ending in TCRETURN, CLEANUPRET or CATCHRET were already
reached by the walk and are reached by the classification too; nothing changes
at them. A landing pad that resumes unwinding ends in a call, which is not
marked as a return, so it was not reached, and a function unwinding out of a
cleanup left every register it had used to the unwinder. That exit is in scope,
and the sequence now runs at it.

Reaching it needs two things the block-scoped emission could not express. The
target used to choose where to emit, the first terminator of the block, and a
landing pad that ends in a call has no terminator, so the clearing would have
been appended after the call that leaves the function. Steps that choose their
own positions cannot be ordered against one another either, so the position is
the coordinator's and is passed to emitZeroCallUsedRegs. At an exit that leaves
through a terminator it is the first terminator, which is where the three
targets implementing the hook already emitted, so nothing they generate moves.
The other is the register set: it is computed once for the function with the
registers its returns read removed, and it does not know that the resume call
reads the exception object in an argument register. Clearing that register
would leave the unwinder nothing to resume with, so the registers an exit names
are dropped at that exit, which at a return exit removes nothing the
function-wide computation had not already removed. Computing the set per exit
instead of narrowing one computed for the function is
trailofbits/vspells-ct-internal-notes#21.

No existing test changes, anywhere in CodeGen/X86 or CodeGen/ARM. Nothing in
the tree combines "zero-call-used-regs" with an unwind path, which is the same
reason the gap lasted this long, so the new site is pinned by a new test rather
than by an old one starting to expect more. The order is pinned separately from
what is emitted, through -pei-print-clearing-sequence, which lists the steps in
sequence order with what each of them does at each in-scope exit; it is a flag
rather than a debug print for the reason -pei-print-exits is, that a sequence
deciding what gets destroyed has to be checkable in the configuration a shipped
compiler is built in. Each new test was confirmed load-bearing by breaking the
implementation once and restoring it: reordering the array, going back to the
walk over return blocks, dropping the out-of-scope filter, letting the target
pick the position again, dropping the per-exit narrowing, and skipping the
funclet exits each failed at least one of them.

The scope is the order and the sites. What the register clear emits is
unchanged, the scratch registers a frame clear will need are
trailofbits/vspells-ct-internal-notes#20, clearing the frame is
trailofbits/vspells-ct-internal-notes#26, and what a tail call needs beyond
being in scope is trailofbits/vspells-ct-internal-notes#22.

This is trailofbits/vspells-ct-internal-notes#19, under the umbrella
trailofbits/vspells-ct-internal-notes#17.
@claude
claude Bot force-pushed the zeroize-exit-coverage branch from 864df60 to 3ba9ac7 Compare August 11, 2026 19:58
@claude
claude Bot force-pushed the zeroize-ordering branch 2 times, most recently from 7b680fc to 5631a9c Compare August 11, 2026 20:05
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