[IR] Do not inline functions carrying "zeroize-stack" - #4
Conversation
|
|
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.
ce6cbab to
6099f1a
Compare
055238d to
a36416f
Compare
frabert
left a comment
There was a problem hiding this comment.
Where does the inlining rejection actually happen? I can see an attribute definition but no logic that acts on its presence or absence
There was a problem hiding this comment.
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
@kumarak @wizardengineer Do we like this? |
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#14should 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 anoinlinethe frontend attaches, so it also holds for IR that was not produced by Clang and cannot be undone by a pass that dropsnoinline. 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_inlinecombined with the attribute is left to the frontend, which is where the combination can be diagnosed as an error on the declaration;alwaysinlinecall sites bypass the attribute compatibility check ingetAttributeBasedInliningDecision, 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.llcovers the three single-module cases under both-passes=inlineanddefault<O2>, andllvm/test/Transforms/Inline/zeroize-stack-lto.llcovers regular LTO and ThinLTO throughllvm-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/Inlinepasses with no new failures.Stacked on #3 (
sensitivity-metadata), which is where the attribute's LangRef entry and the!sensitivemetadata 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