Skip to content

[X86] Lower llvm.zeroize to a late clearing sequence - #6

Draft
claude[bot] wants to merge 1 commit into
zeroize-verifierfrom
zeroize-lowering
Draft

[X86] Lower llvm.zeroize to a late clearing sequence#6
claude[bot] wants to merge 1 commit into
zeroize-verifierfrom
zeroize-lowering

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Requested by Francesco Bertolaccini · Slack thread

Sixth patch in the stack, on top of #5. Closes trailofbits/vspells-ct-internal-notes#11.

llvm.zeroize had no expansion, so a function using it did not build. This routes it through both instruction selectors to a clearing sequence on x86-64.

What keeps the emitted sequence from being optimized away

Expanding the intrinsic into stores at selection time would make the guarantee depend on every later pass choosing not to remove them, and the region a clear covers is normally dead afterwards, which is exactly the shape a store-removing pass looks for.

So neither selector emits a store. Both emit ZEROIZE64, an X86 pseudo with no pattern and no memory operand, carrying hasSideEffects and mayStore. While it exists there is no store for anything to find: no analysis can attribute a write to it, so no pass can prove that write redundant, and the side-effect flag keeps it off the dead-code lists. X86ExpandPseudo turns it into movb $0, %al; rep;stosb in addPreSched2, after register allocation.

That is the whole argument, and it is structural rather than a matter of pass ordering luck: before the expansion there is no store to remove, and after it nothing remains in the pipeline that removes stores.

Entry points

Both are the hook each selector already has for an intrinsic that needs custom handling rather than a generic call:

  • SelectionDAG: LowerINTRINSIC_W_CHAIN, reached from LowerOperation for ISD::INTRINSIC_VOID, which X86 already marks Custom. SelectionDAGBuilder builds that node for any intrinsic it does not handle itself.
  • GlobalISel: X86LegalizerInfo::legalizeIntrinsic, which until now accepted every intrinsic unchanged.

Each copies the destination and the count into %rdi and %rcx and emits the pseudo, so the two selectors produce the same machine code.

Tests

llvm/test/CodeGen/X86/zeroize.ll runs both selectors (-global-isel -global-isel-abort=1 for the second) over dynamic, constant and narrow lengths, a clear followed by a store covering the same bytes, two identical clears, a clear of an alloca that never escapes and dies at the return, and a clear immediately before lifetime.end. Each of the first two is paired with the equivalent plain store, which the backend does remove, so the test pins the difference rather than only that some instructions came out.

llvm/test/CodeGen/X86/zeroize-expand-late.ll pins the lateness directly, for both selectors: what each handed over is the pseudo, it is still the pseudo after register allocation (-stop-before=x86-expand-pseudo), and the write appears only in the expansion.

Confirmed load-bearing by breaking each of the three pieces separately (SelectionDAG lowering, GlobalISel lowering, pseudo expansion) and checking the tests fail each time.

Scope

x86-64 (LP64) only. 32-bit x86 still reports the intrinsic as unlowered, as it did before this patch, and sequences for other targets are separate work.

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.

Nothing expanded the intrinsic, so a function that used it did not build.
What makes expanding it different from expanding a memset is that the write
has to still be there at the end: the region is normally dead by the time the
clear runs, which is exactly the shape a store-removing pass looks for.

Emitting the stores at selection time and hoping they survive would rest on
every later pass declining to remove them. Emit an opaque pseudo instead.
ZEROIZE64 has no pattern and no memory operand, so no analysis can attribute a
store to it and no pass can find a store to prove redundant, and it carries
hasSideEffects and mayStore, so it is not dead either. X86ExpandPseudo turns it
into "movb $0, %al; rep;stosb" in addPreSched2, after register allocation and
so after every pass that deletes stores. Between selection and that point there
is no store to remove, and past that point nothing is left that would remove
one.

Both selectors reach the pseudo through the hook each already has for an
intrinsic needing custom handling rather than a generic call:
LowerINTRINSIC_W_CHAIN for ISD::INTRINSIC_VOID, which X86 already marks Custom,
and X86LegalizerInfo::legalizeIntrinsic, which until now accepted every
intrinsic unchanged. Each copies the destination and the count into %rdi and
%rcx and emits the pseudo, so the two produce the same machine code.

The tests pair each clear with an ordinary store the backend does remove, one
covered by a later store and one repeated, so what they pin is the difference
rather than only that some instructions came out. A second test walks the three
points that carry the guarantee: what each selector handed over, that it is
still the pseudo once registers are allocated, and that the write exists only
after the expansion.

The clearing sequence is LP64 only. 32-bit x86 still reports the intrinsic as
unlowered, and the sequences for other targets are separate work, on
trailofbits/vspells-ct-internal-notes#11.
@claude
claude Bot force-pushed the zeroize-verifier branch from 2cc8417 to 53f69dc Compare August 11, 2026 19:57
@claude
claude Bot force-pushed the zeroize-lowering branch from cf198e8 to 2812359 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