Skip to content

[CodeGen] Compute the register clear set at each exit - #10

Draft
claude[bot] wants to merge 1 commit into
zeroize-orderingfrom
zeroize-per-exit-regs
Draft

[CodeGen] Compute the register clear set at each exit#10
claude[bot] wants to merge 1 commit into
zeroize-orderingfrom
zeroize-per-exit-regs

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Requested by Francesco Bertolaccini · Slack thread

Register clearing decides what to destroy once for the function and emits that
decision at every exit. Part of it genuinely belongs to the function: which
registers a mode selects, and the callee-saved registers no exit may touch. The
rest does not. A register can be cleared only where it is dead, and where it is
dead is a property of one exit, so a set computed for the function has to take
the union over the function's returns and deny every exit what every other exit
needs.

A function with two returns is where that shows. Each of them needs its own
live-out registers and nothing else, but the set they are cleared from has both
returns' registers taken out of it, so each exit leaves the other's holding
whatever the function last put in them. That is the value the attribute exists
to destroy, sitting in a register that is dead at the exit actually taken. Taken
far enough the set empties: a function whose two exits are a return and a tail
call needs the return value in %eax at one of them and the outgoing arguments in
%edi and %esi at the other, the union of those is every register the function
used, and the function clears nothing at either exit while reporting that it is
protected.

Split the computation in two. planClearRegisters keeps what the mode decides,
which registers "used", "arg" and "gpr" select over the whole function, because
that is what those words have always meant, and the callee-saved exclusion,
which is the function's too: a callee-saved register has to hold what the
caller left in it wherever the function leaves, so no exit can clear one. What
comes out of it is a candidate set rather than a clear set, and
computeRegsToClearAtExit turns it into one at each exit.

What an exit needs is what runs after the sequence, and the sequence is emitted
at the exit's insertion point, so that is the rest of the block: the return and
the registers it carries the return value in, the jump of a tail call and the
registers it leaves the outgoing arguments in, or the call that resumes
unwinding and the argument register it takes the exception object in. Reading
that off the instructions rather than off a list of exit kinds is what keeps it
right for a kind added later. It also subsumes the narrowing that arrived with
the clearing sequence, which removed the registers named by the exit
instruction from a set computed for the function; there is no function-wide set
left to narrow.

The result never clears less than before. At an exit that leaves through a
terminator the new exclusion is that block's terminator run, which is one of
the terms the old union was taken over, so what is spared at that exit is a
subset of what was spared before and what is cleared is a superset of what was
cleared before. That was checked rather than argued: a temporary build
recomputed the old union-based set at every exit and failed the compilation if
the per-exit set was missing a register the old one had. Every test in
CodeGen/X86 and CodeGen/ARM passed under it, and the check was confirmed to
fire by weakening it on purpose. The scaffolding is not part of this change.

What the existing modes mean is unchanged. Which registers are candidates is
still answered by the same code from the same attribute, and only the exclusion
has moved; a function with one exit has nothing to take a union over, so its
output is identical, which is most of what is in the tree today. Compatibility
with the shipped modes is trailofbits/vspells-ct-internal-notes#60. The
register-unit reset in the exclusion is kept with its FIXME rather than
corrected, for the same reason: it only ever spares registers, so correcting it
would widen what every mode clears, which is a separate change from moving the
exclusion.

No existing test changes, in CodeGen/X86 or in CodeGen/ARM. Two functions in
llvm/test/CodeGen/X86 carry "zero-call-used-regs" and have more than one exit
in the IR, and both lower to a single machine exit, so there is nothing for
them to take a union over either. The zero-call-used-regs tests under
CodeGen/AArch64 and CodeGen/RISCV were not run, since neither target is built
here; every function in them has one exit, which is the case in which the two
computations agree by construction.

The new test is the contrast between the two exits of one function: the tail
call clears the return-value register the other exit needs, the return clears
the argument registers the tail call needs, and neither cleared anything
before. A second function makes the same contrast between a return and an exit
that resumes unwinding, and keeps the exception object the resume call reads,
which is the exit answering for itself rather than being told by another exit.
A third has one exit and emits what it always emitted. On ARM, which refuses to
clear registers at all, what is pinned is that the refusal is still the
function's: one diagnostic for a function with two exits, and the same
disposition reported at both. Each was confirmed load-bearing by breaking the
implementation once and restoring it: putting the union back, dropping the
per-exit exclusion, and planning the sequence at each exit instead of once each
failed one of them.

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

@claude
claude Bot force-pushed the zeroize-ordering branch from c0ecd7d to 7b680fc Compare August 11, 2026 19:58
Register clearing decides what to destroy once for the function and emits that
decision at every exit. Part of it genuinely belongs to the function: which
registers a mode selects, and the callee-saved registers no exit may touch. The
rest does not. A register can be cleared only where it is dead, and where it is
dead is a property of one exit, so a set computed for the function has to take
the union over the function's returns and deny every exit what every other exit
needs.

A function with two returns is where that shows. Each of them needs its own
live-out registers and nothing else, but the set they are cleared from has both
returns' registers taken out of it, so each exit leaves the other's holding
whatever the function last put in them. That is the value the attribute exists
to destroy, sitting in a register that is dead at the exit actually taken. Taken
far enough the set empties: a function whose two exits are a return and a tail
call needs the return value in %eax at one of them and the outgoing arguments in
%edi and %esi at the other, the union of those is every register the function
used, and the function clears nothing at either exit while reporting that it is
protected.

Split the computation in two. planClearRegisters keeps what the mode decides,
which registers "used", "arg" and "gpr" select over the whole function, because
that is what those words have always meant, and the callee-saved exclusion,
which is the function's too: a callee-saved register has to hold what the
caller left in it wherever the function leaves, so no exit can clear one. What
comes out of it is a candidate set rather than a clear set, and
computeRegsToClearAtExit turns it into one at each exit.

What an exit needs is what runs after the sequence, and the sequence is emitted
at the exit's insertion point, so that is the rest of the block: the return and
the registers it carries the return value in, the jump of a tail call and the
registers it leaves the outgoing arguments in, or the call that resumes
unwinding and the argument register it takes the exception object in. Reading
that off the instructions rather than off a list of exit kinds is what keeps it
right for a kind added later. It also subsumes the narrowing that arrived with
the clearing sequence, which removed the registers named by the exit
instruction from a set computed for the function; there is no function-wide set
left to narrow.

The result never clears less than before. At an exit that leaves through a
terminator the new exclusion is that block's terminator run, which is one of
the terms the old union was taken over, so what is spared at that exit is a
subset of what was spared before and what is cleared is a superset of what was
cleared before. That was checked rather than argued: a temporary build
recomputed the old union-based set at every exit and failed the compilation if
the per-exit set was missing a register the old one had. Every test in
CodeGen/X86 and CodeGen/ARM passed under it, and the check was confirmed to
fire by weakening it on purpose. The scaffolding is not part of this change.

What the existing modes mean is unchanged. Which registers are candidates is
still answered by the same code from the same attribute, and only the exclusion
has moved; a function with one exit has nothing to take a union over, so its
output is identical, which is most of what is in the tree today. Compatibility
with the shipped modes is trailofbits/vspells-ct-internal-notes#60. The
register-unit reset in the exclusion is kept with its FIXME rather than
corrected, for the same reason: it only ever spares registers, so correcting it
would widen what every mode clears, which is a separate change from moving the
exclusion.

No existing test changes, in CodeGen/X86 or in CodeGen/ARM. Two functions in
llvm/test/CodeGen/X86 carry "zero-call-used-regs" and have more than one exit
in the IR, and both lower to a single machine exit, so there is nothing for
them to take a union over either. The zero-call-used-regs tests under
CodeGen/AArch64 and CodeGen/RISCV were not run, since neither target is built
here; every function in them has one exit, which is the case in which the two
computations agree by construction.

The new test is the contrast between the two exits of one function: the tail
call clears the return-value register the other exit needs, the return clears
the argument registers the tail call needs, and neither cleared anything
before. A second function makes the same contrast between a return and an exit
that resumes unwinding, and keeps the exception object the resume call reads,
which is the exit answering for itself rather than being told by another exit.
A third has one exit and emits what it always emitted. On ARM, which refuses to
clear registers at all, what is pinned is that the refusal is still the
function's: one diagnostic for a function with two exits, and the same
disposition reported at both. Each was confirmed load-bearing by breaking the
implementation once and restoring it: putting the union back, dropping the
per-exit exclusion, and planning the sequence at each exit instead of once each
failed one of them.

This is trailofbits/vspells-ct-internal-notes#21, under the umbrella
trailofbits/vspells-ct-internal-notes#17.
@claude
claude Bot force-pushed the zeroize-per-exit-regs branch from 0633116 to 6877207 Compare August 11, 2026 20:00
@claude
claude Bot force-pushed the zeroize-ordering branch from 7b680fc to 5631a9c Compare August 11, 2026 20:05
@claude
claude Bot force-pushed the zeroize-per-exit-regs branch from 6877207 to c422de1 Compare August 11, 2026 20:14
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