Skip to content

[IR] Do not inline functions carrying "zeroize-stack" - #4

Draft
claude[bot] wants to merge 1 commit into
sensitivity-metadatafrom
zeroize-inlining
Draft

[IR] Do not inline functions carrying "zeroize-stack"#4
claude[bot] wants to merge 1 commit into
sensitivity-metadatafrom
zeroize-inlining

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Requested by Francesco Bertolaccini · Slack thread

A function carrying the "zeroize-stack" attribute promises to clear its own stack frame before returning. Inlining it dissolves that frame into the caller's: the bytes it promised to clear become bytes of a frame that outlives the point where the clear was due, and nothing is left in the IR to record the obligation. Until now the inliner would fold such a function into a caller that clears nothing and drop the guarantee silently.

Refuse it where mismatched sanitizer instrumentation is already refused, as a compatibility rule keyed on the IR attribute and consulted through AttributeFuncs::areInlineCompatible. Placing the check there rather than in a pass of its own is what makes it hold wherever inline compatibility is decided, LTO and ThinLTO included; the test covers both, since the callee reaching the inliner through the link rather than through its own module is the case worth pinning down.

The rule is callee-side and has no same-attribute exemption. A caller that carries the attribute clears its own frame at its own returns, which is neither the clear the callee owed at the point it would have returned nor necessarily as much of the frame, as the two functions may ask for different amounts of it. Inlining a callee that does not carry the attribute into one that does stays allowed and is worth having: the callee's frame lies below the stack pointer once it returns and no clear reaches it, whereas inlining turns those bytes into frame bytes of the caller, which are cleared.

The LangRef entry for the attribute said the inlining rule was specified separately; it now states the rule.

A reviewer coming from trailofbits/vspells-ct-internal-notes#14 should note that what is implemented here is broader than what that issue describes, deliberately and per the merged design decision the issue predates. Three differences. The rule is keyed on the IR function attribute rather than on a noinline the frontend attaches, so it also holds for IR that was not produced by Clang and cannot be undone by a pass that drops noinline. The qualifier about the callee being inlined across a trust boundary is gone: there is no condition on the caller at all, because no property of the caller reinstates the callee's clear. And protected-into-protected is refused too, for the reason given above; the issue's framing would have exempted it.

Rejecting always_inline combined with the attribute is left to the frontend, which is where the combination can be diagnosed as an error on the declaration; alwaysinline call sites bypass the attribute compatibility check in getAttributeBasedInliningDecision, so this half of the decision is not enforceable in the middle end and is deferred to the Clang change.

Testing: llvm/test/Transforms/Inline/zeroize-stack.ll covers the three single-module cases under both -passes=inline and default<O2>, and llvm/test/Transforms/Inline/zeroize-stack-lto.ll covers regular LTO and ThinLTO through llvm-lto2, which is the completion criterion the issue names. Both LTO configurations also check that an unannotated callee from the other module is still inlined into an annotated caller, so a missing inline is evidence of the rule rather than of nothing having been inlined. llvm/test/Transforms/Inline passes with no new failures.

Stacked on #3 (sensitivity-metadata), which is where the attribute's LangRef entry and the !sensitive metadata come from. Draft until the branches below it land.

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.

A function carrying the attribute promises to clear its own stack frame
before returning. Inlining it dissolves that frame into the caller's: the
bytes it promised to clear become bytes of a frame that outlives the
point where the clear was due, and nothing is left in the IR to record
the obligation. Until now the inliner would fold such a function into a
caller that clears nothing and drop the guarantee silently.

Refuse it where mismatched sanitizer instrumentation is already refused,
as a compatibility rule keyed on the IR attribute and consulted through
AttributeFuncs::areInlineCompatible. Placing the check there rather than
in a pass of its own is what makes it hold wherever inline compatibility
is decided, LTO and ThinLTO included; the test covers both, since the
callee reaching the inliner through the link rather than through its own
module is the case worth pinning down.

The rule is callee-side and has no same-attribute exemption. A caller
that carries the attribute clears its own frame at its own returns, which
is neither the clear the callee owed at the point it would have returned
nor necessarily as much of the frame, as the two functions may ask for
different amounts of it. Inlining a callee that does not carry the
attribute into one that does stays allowed and is worth having: the
callee's frame lies below the stack pointer once it returns and no clear
reaches it, whereas inlining turns those bytes into frame bytes of the
caller, which are cleared.

Rejecting alwaysinline combined with the attribute is left to the
frontend, which is where the combination can be diagnosed; alwaysinline
call sites bypass the attribute compatibility check in
getAttributeBasedInliningDecision.

The rule is the one decided on
trailofbits/vspells-ct-internal-notes#14.
@claude
claude Bot force-pushed the sensitivity-metadata branch from ce6cbab to 6099f1a Compare August 11, 2026 19:57
@claude
claude Bot force-pushed the zeroize-inlining branch from 055238d to a36416f Compare August 11, 2026 19:57

@frabert frabert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does the inlining rejection actually happen? I can see an attribute definition but no logic that acts on its presence or absence

@claude claude Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rejection happens in the attribute compatibility table rather than in the inliner: checkZeroizeStack in llvm/lib/IR/Attributes.cpp:2606 returns !Callee.hasFnAttribute("zeroize-stack") (line 2623), and def : CompatRule<"checkZeroizeStack">; in llvm/include/llvm/IR/Attributes.td:472 is what makes TableGen emit the call to it inside hasCompatibleFnAttrs, which is all that AttributeFuncs::areInlineCompatible returns (llvm/lib/IR/Attributes.cpp:2803).

A false from the predicate becomes a refusal in getAttributeBasedInliningDecision: functionsHaveCompatibleAttributes (llvm/lib/Analysis/InlineCost.cpp:3082, consulting areInlineCompatible at :3092) fails, and the call site is rejected with InlineResult::failure("conflicting attributes") at llvm/lib/Analysis/InlineCost.cpp:3224.

There is deliberately no inliner-side code — this is where mismatched sanitizer and strictfp instrumentation are already refused, and stating the rule as a property of the caller/callee attribute pair rather than of one pass is what makes it hold wherever inline compatibility is decided, LTO and ThinLTO included; alwaysinline call sites are the one exception, as they skip the compatibility check entirely, and diagnosing that combination is left to the frontend.

llvm/test/Transforms/Inline/zeroize-stack.ll covers it under both -passes=inline and default<O2>, for annotated and unannotated callers, and zeroize-stack-lto.ll pins the same behaviour down across regular LTO and ThinLTO — each with an unannotated callee that must still be inlined, so a missing inline is evidence of the rule rather than of nothing having been inlined at all.


Generated by Claude Code

@frabert

frabert commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

alwaysinline call sites are the one exception, as they skip the compatibility check entirely, and diagnosing that combination is left to the frontend.

@kumarak @wizardengineer Do we like this?

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.

3 participants