Skip to content

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

Merged
kumarak merged 2 commits into
enforced_secrecy_mainfrom
zeroize-inlining
Aug 18, 2026
Merged

[IR] Do not inline functions carrying "zeroize-stack"#4
kumarak merged 2 commits into
enforced_secrecy_mainfrom
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 !nozeroize 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.

@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?

@claude
claude Bot force-pushed the sensitivity-metadata branch from 6099f1a to e92228d Compare August 13, 2026 08:18
@claude
claude Bot force-pushed the zeroize-inlining branch from a36416f to c3a85e0 Compare August 13, 2026 08:20
@kumarak
kumarak marked this pull request as ready for review August 13, 2026 13:44
@claude
claude Bot force-pushed the sensitivity-metadata branch from e92228d to f548a8c Compare August 13, 2026 14:17
@claude
claude Bot force-pushed the zeroize-inlining branch from c3a85e0 to 79babda Compare August 13, 2026 14:18
@kumarak

kumarak commented Aug 13, 2026

Copy link
Copy Markdown
Member

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?

We need to develop an approach to handle alwaysinline. At the moment, delegating the responsibility to the frontend or developers is fine.

@claude
claude Bot force-pushed the zeroize-inlining branch 2 times, most recently from 60ebb07 to c4971d5 Compare August 14, 2026 18:56
Base automatically changed from sensitivity-metadata to enforced_secrecy_main August 14, 2026 19:35
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 zeroize-inlining branch from c4971d5 to 2e4351c Compare August 14, 2026 19:48
@frabert

frabert commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

@claude Apart from leaving the responsibility to the frontend, can you comment on what strategies you think we could apply?

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

The bypass isn't specific to this rule. Every CompatRule has it, and I confirmed a mismatched sanitize_address callee inlines the same way under both -passes=inline and -passes=always-inline. So the real question is what strength this rule should have, and LLVM already has two answers with precedent.

Refuse the inline, even for alwaysinline. There is precedent in this very function, framed the same way our argument would be: InlineCost.cpp:3199-3200 refuses on mismatched target features with the comment "Inlining into a function with less target features is unsound, so enforce this even if alwaysinline is used", pinned by test/Transforms/Inline/X86/always-inline-incompatible-target-features.ll. strictfp does the same from inside the alwaysinline block at :3212. Two natural placements: beside that strictfp check, or in isInlineViable, which takes only the callee and whose seven existing failure reasons are all callee-side properties — the same shape as this rule. isInlineViable also picks up AlwaysInliner.cpp:103's independent call for free.

Correcting something I said earlier in this thread: I described AlwaysInliner as having a path of its own. In this tree it routes through getAttributeBasedInliningDecision at AlwaysInliner.cpp:58, so a single edit there covers the always-inliner, the cost-based inliner, the ML advisor's mandatory path, and Flatten.

Or propagate the obligation to the caller. MergeRule is the mechanism and adjustCallerStackProbeSize (Attributes.cpp:2703) is near-exact precedent: a string-valued function attribute, parsed, compared for strictness, with the stricter value imposed on the caller and "caller has none" handled by adopting the callee's. adjustCallerSSPLevel is the enum analogue of taking the stronger mode. The decisive part is that mergeAttributesForInlining runs on the alwaysinline path — AlwaysInliner.cpp:74 passes MergeAttributes=true, as does every inliner except PartialInlining — so this closes the hole without needing to win the "alwaysinline can be refused" argument at all.

What gives me pause about it: absence of the attribute is not an opt-out, so propagating would quietly make every caller of an alwaysinline protected function clear its whole frame. adjustCallerSSPLevel carves out exactly this case, declining to change a caller with no stack protection because "that could change the program's semantics", and we have nothing equivalent to key a carve-out off.

So refusing is the faithful reading of what the attribute promises, and merging is the option that lets the inline happen at a codegen cost. Choosing between them is really a question about whether widening the clear to the caller's frame is acceptable.

Which is the thing worth saying plainly: none of this protects anything yet. I measured at this branch's head rather than relying on the earlier review. emitZeroizeStack does not exist. supportsZeroizeStack returns false and no target overrides it. emitClearingStep's stack case emits nothing. planClearStack diagnoses "zeroize-stack" is not supported by this target on any real target. And no consumer distinguishes "used" from "sensitive" — the only reader of the mode's value in all of llvm/lib is the Verifier checking it is non-empty. Every strategy above is a rule about a promise nothing currently keeps, which is an argument for settling the cheap parts now and the expensive choice when there is an implementation to measure.

Two things in this PR I would fix either way, since they are wrong now and cost little:

  • The LangRef text this PR adds says a function carrying the attribute "is not inlined into any caller ... This holds however the caller is annotated". That states a guarantee the implementation does not deliver, and it should name the alwaysinline exception.
  • Neither new test exercises alwaysinline, so nothing here would catch a regression in either direction.

Last thing: the frontend cannot reach most of this class of problem. Inlining is one of several places the obligation moves or disappears, and none of the others goes near areInlineCompatible:

  • MachineOutliner builds a fresh function and keeps only target-features, target-cpu and nounwind, so the attribute is dropped while instructions that touched sensitive slots migrate into the outlined frame. Attribute::NoOutline is the natural hook.
  • Coroutine splitting copies all function attributes for the Switch and Async ABIs, but Retcon and RetconOnce replace them wholesale with the prototype's and drop it. A split coroutine's frame is also not the stack frame this attribute talks about.
  • Hot/cold splitting does propagate it, but only because CodeExtractor copies every string attribute except "thunk". Nobody decided that; it rides an else branch.
  • MergeFunctions will not merge two functions that differ in the attribute, but it will merge two identical protected ones into a thunk that inherits it, which is the musttail case [CodeGen] Suppress tail calls in protected functions #11 addresses.

Generated by Claude Code

@kumarak

kumarak commented Aug 18, 2026

Copy link
Copy Markdown
Member

@claude, refusing alwaysinline could cause performance issues and may not be acceptable upstream. Explore propagating attributes to the caller function.

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

Propagation works, and LangRef already fixes the ordering it needs.

"used" is the widest mode. From the entry on this branch: ""sensitive" is a request for precision, not a weaker guarantee: it narrows what a function clears relative to "used"", and any unrecognized value is "used" as well. So the merge is: the caller ends up with the wider of the two modes, and a caller with no attribute adopts the callee's.

/// If the inlined function asked for its frame to be cleared, the caller now
/// owns that obligation, over at least as much of the frame as the callee asked
/// for.
static void adjustCallerZeroizeStack(Function &Caller, const Function &Callee) {
  Attribute CalleeAttr = Callee.getFnAttribute("zeroize-stack");
  if (!CalleeAttr.isValid())
    return;

  // "used" is the widest mode, and LangRef gives any unrecognized value that
  // same meaning, so "sensitive" is the only value that can clear less.
  if (!Caller.hasFnAttribute("zeroize-stack") ||
      CalleeAttr.getValueAsString() != "sensitive")
    Caller.addFnAttr(CalleeAttr);
}

Registered as one line beside the existing merge rules. adjustCallerStackProbeSize is the same shape — string-valued attribute, compare, adopt the callee's when the caller has none — and mergeAttributesForInlining runs on the alwaysinline path, so this closes the hole without needing the inline refused.

Three things come with it, none of them objections.

The refusal has to go rather than sit alongside. Keeping both would refuse on the ordinary path and propagate only where the refusal was already bypassed, which is incoherent. So checkZeroizeStack and its compat rule are deleted, the LangRef paragraph this PR added is replaced, and the two new tests invert from "not inlined" to "inlined, and the caller carries the attribute afterwards".

A caller with no attribute does acquire one, and that is the deliberate half. adjustCallerSSPLevel declines exactly this case, but its reason does not transfer: -fno-stack-protector is an explicit opt-out, and there is no opt-out spelling for this attribute, so absence carries no intent to decline. Declining would drop the obligation, which is the thing being fixed.

The cost moves rather than disappearing. A "used" callee inlined into a "sensitive" caller upgrades the caller to "used", so the caller clears more of its own frame than it asked for, and the effect is viral up the inline chain. In most shapes that is cheaper than a blocked inline, but it is not free, and it is worth knowing that is where the cost went.

One drive-by worth taking while the file is open: the two mode bullets are not literally nested. "used" clears "every stack slot the function used", while "sensitive" clears "every stack slot except" the marked ones, with no such qualifier. The normative sentence settles the ordering, but a careful reader can raise it, and this merge rule's correctness rests on exactly that ordering.

Implementing it rewrites this commit and restacks the ten branches above it, so it needs a go-ahead before I push. If the mode rename is going ahead too, both want the same restack and should travel together.


Generated by Claude Code

@kumarak

kumarak commented Aug 18, 2026

Copy link
Copy Markdown
Member

@frabert, what do you think about going with propagation and seeing how much performance cost it can have?

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

To take the scope question directly: the design above already applies to all inlining, not only the alwaysinline case. Deleting checkZeroizeStack and its compat rule removes the refusal everywhere, and the merge rule then runs on every inline, so nothing is left split — it is propagate-everywhere, not propagate-only-in-the-gap.

On evaluating the cost, no measurement is possible yet. No target emits the clear: supportsZeroizeStack returns false everywhere with no overrides, and the frame-clearing step emits nothing. So today both approaches cost the same, which is nothing, and a performance comparison has to wait for the emission to exist. That is an argument for deciding this on design grounds now rather than deferring to a measurement that cannot be taken. For completeness on what is measured: the only evidence anywhere on this series is correctness, and it is narrow — 713 tests across the verifier and bitcode suites at #5's tree, covering #5's content, with no full-suite run at any current head.

The two costs are also not symmetric, which matters if only one gets built. Blocking costs missed inlining: bounded, and visible in a profile as a call that did not disappear. Propagation costs a wider clear in the caller, and that cost spreads up the inline chain, landing in functions whose authors never asked for it. It is also the novel half — nothing upstream propagates a clearing obligation, and the closest analogue, zero-call-used-regs, simply drops it on inline with no diagnostic. So if we build one now, propagation is the one that will need the measurement, because its cost lands where a reader would not think to look.


Generated by Claude Code

@frabert

frabert commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

What we're proposing here is to propagate the safety tags of a callee to the callers that would inline it, am I understanding this correctly?

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

Yes, that's it, with three precisions worth pinning down.

It's the function attribute that propagates, not the per-object metadata. The !nozeroize marks sit on the allocas themselves, so they travel into the caller as part of the inlined body and need no rule of their own. What needs a rule is the function-level promise that some frame gets cleared, because that promise is attached to a function whose frame is about to stop existing.

It happens at the moment of the inline, and only to the caller that actually inlined. There's no pass over potential callers, and a function that never gets inlined anywhere is unaffected.

The caller ends up with the wider of the two modes, not necessarily the callee's. If the caller already asks for more of its frame to be cleared than the callee did, it keeps what it had.


Generated by Claude Code

@frabert

frabert commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

I feel like that's the wrong direction to be looking at, though: the unsafe path is a caller marked sensitive inlining a nonsensitive callee, or am I wrong?

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

You're right that there's a hazard in that direction, and it's one nothing currently rejects — but it isn't the direction where inlining loses a guarantee, and propagation wouldn't fix it.

Your direction isn't a regression. An unprotected callee's frame was never cleared: once it returns, those bytes sit below the stack pointer and no clear reaches them. Inline it into a protected caller and they become the caller's frame bytes, which the caller does clear. Nothing gets worse, and in the common case it gets better.

What does get worse in your direction is what the caller's mode means. If the unprotected callee's allocas carry !nozeroize, then after inlining into a "sensitive" caller those slots are spared — so the caller's promise of "everything except objects vetted as safe to spare" now excludes an object nobody vetted against the caller's standard. That's a weaker guarantee than the attribute advertises. It doesn't leak anything the un-inlined program didn't already leak, which is why I'd call it a specification problem rather than a miscompile, but it is real.

And it has a cheap fix that is not this PR's merge rule. !nozeroize on an alloca in a function that does not carry "zeroize-stack" should be invalid IR. The Verifier currently checks only that the metadata is attached to an alloca and that the node is empty; it does not check the enclosing function. Your frontend rule already forbids producing that combination, but nothing enforces it, so hand-written or mis-produced IR gets there. Worth adding regardless of what we do about inlining.

The direction that does lose a guarantee is the other one. A protected callee inlined into an unprotected caller: before, its frame was cleared on return; after, nothing clears those bytes at all, and no IR remains that records the obligation. The program is strictly worse than it was. That's the regression the merge rule addresses, and it's why the rule is callee-side.


Generated by Claude Code

@frabert

frabert commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

I see. So, what we would need is a pass that propagates the sensitive tag from callee to caller when inlinining, while also marking all the caller's allocas as !nozeroize, right?

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

I don't think so, and the reason is the hazard you named a moment ago.

Marking the caller's allocas !nozeroize asserts that those objects were vetted as safe to spare. After an inline, the caller's own objects can hold copies of what the callee was protecting — values flow between them freely once the frame is one frame — so that assertion is false exactly where it matters most. It would take the specification problem you spotted and make it the mechanism rather than an edge case.

Two smaller things also get in the way:

The marks only mean anything in "sensitive" mode. "used" clears every slot the function used and does not consult them. So a "used" callee's obligation can't be expressed this way without also downgrading the caller to "sensitive", and if the caller already asked for "used", that downgrade narrows what the caller clears — which is the one thing the design can't do.

The saving would be partial anyway. LangRef already requires that slots with no source-level provenance are cleared regardless of marks: spill slots, the callee-save area, alignment padding, on the grounds that they can hold copies of unmarked data and nothing records where those copies came from. Those are exactly the slots that an inline multiplies.

If bounding the cost is the goal — and I think it should be, because that's the real objection to propagation — the mechanism to reach for is a positive per-slot obligation rather than a negative one. Today the promise lives on the function and the only per-object marker says "spare this", so every option is a choice between clearing more than we need and asserting something untrue. If the obligation lived on the slots, an inline would carry it in with them and there would be nothing to propagate and nothing to re-mark. That's a larger change than this PR, but it's the shape in which the question stops recurring, and it's worth knowing that's where the current design's awkwardness comes from.


Generated by Claude Code

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

2026-08-18 weekly sync — minutes bearing on this PR

Posting the meeting record, not a position. I was not in the room; this is quoted from the transcript of the 2026-08-18 Compiler Enforced Secrecy sync, which ran roughly 14:29–15:05Z.

The room converged on a third option — neither this PR's current flat refusal, nor propagation to the caller: allow inlining only when the caller is also marked, refuse otherwise, plus an opt-in warning rather than an error.

What if only allow sensitive functions to be in line if the color is also sensitive?

a sensitive function can only be in line in a sensitive context... that's what I would call a sensible default

— Francesco Bertolaccini ("color" is the transcript mishearing "caller")

they will only get in line if the caller is having the same attribute otherwise they will be not in line

at the moment I will say just go with the preventing inline.

— Akshay Kumar

an optional... warning that you can enable with dashw, you know, uh unsafe inlining disabled or something like that

— Francesco Bertolaccini, on the diagnostic. The flag name was explicitly a placeholder, and an error was proposed and withdrawn in the meeting once it was pointed out that alwaysinline is best-effort.

Two gaps the room left: which marking the test reads — every speaker used the positive sense, and neither the source-level attribute nor the IR spelling was named — and mode mismatch between a marked caller and a marked callee, which never came up.

@kumarak — your comment above directs propagating the attribute to the caller. The meeting started about an hour later and landed on refusal with an exemption, where your own words were "at the moment I will say just go with the preventing inline." Nobody referenced this comment in the meeting, so the transcript does not tell me whether it is superseded. Which stands?

Nothing has been implemented and no code has changed on the back of this. Recorded here so it is not only in a meeting doc.


Generated by Claude Code

@kumarak

kumarak commented Aug 18, 2026

Copy link
Copy Markdown
Member

@claude, What was discussed in the meeting about handing alwaysinline stands? Update LangRef as discussed.

The attribute's inlining rule refused every caller, including one carrying
the attribute itself. Inlining into a caller that clears at least as much of
its frame keeps the callee's guarantee, so restate the rule to permit it and
to name the mode ordering it depends on.

The mode ordering was not settled in discussion; requiring the caller to
clear at least as much is the only reading that preserves what the callee
promised.

Also state that the refusal is not overridden by a request to inline, and
that declining is a warning rather than an error.
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

LangRef is updated in 3445f6cd, as a commit on top of this one rather than an amendment. That leaves the ten branches above behind by one commit until it is folded in, so say the word whenever you would rather the series read cleanly and I will fold it and restack.

One thing I chose rather than asked, and the commit message says so too: the meeting did not cover both functions being marked in different modes. The text requires the caller's mode to clear at least as much of the frame as the callee's, because "used" is the widest mode and a "sensitive" caller would otherwise spare slots that a "used" callee promised to clear.

Not in this commit, and still to come: the compatibility-rule change that permits a marked caller, and the opt-in warning. Two things worth settling before I write them.

Which marking the caller test reads. Every speaker in the meeting used the positive sense, and neither the source-level attribute nor the IR spelling was named. Since the inversion those are two different rules, so the answer needs to name the spelling rather than the concept.

And a correction to something I said earlier in this thread. I called the stray-mark Verifier rule a cheap fix. It is not. Every !nozeroize attachment in the tree — nine of them across three test files — sits in a function carrying no "zeroize-stack" attribute at all, so the rule would reject all of them, and the Assembler and Bitcode round-trip tests would fail until they are rewritten. Those tests live in two places, inside #5 and in the merged base, so it is a restack plus a separate PR against the base. Still worth doing, and it is a prerequisite rather than a tidy-up: the caller exemption is sound only if a mark cannot arrive from a function where it meant nothing.


Generated by Claude Code

@kumarak
kumarak merged commit 1f24967 into enforced_secrecy_main Aug 18, 2026
8 of 9 checks passed
@kumarak
kumarak deleted the zeroize-inlining branch August 18, 2026 16:40
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.

4 participants