Back in 2020, I proposed a #[sensitive] attribute for functions which handle transient secrets. I'm also the author of the zeroize crate.
I'd like to revisit that proposal, particularly as we continue to struggle with what to do for stack zeroization in RustCrypto, where we have effectively punted on this problem and currently don't do any sort of stack zeroization by policy. However, that policy isn't helping us in the age of LLM-assisted vulnerability reporting where we are now being spammed with reports about how we don't wipe every last secret from the stack.
This is an area where I believe some compiler assistance would be extremely beneficial.
Frank Denis a.k.a. jedisct1 wrote an excellent series of blog posts which cover various aspects of this problem. Part 1, “Wiping can make things worse”, shows C code where adding a wipe causes an additional spill which survives the wipe. Part 2, “Clearing the stack and registers”, discusses compiler assistance, cleanup at operation boundaries, and dedicated stacks. Part 3, “Copies you can’t wipe”, also makes the case for APIs which avoid materializing unnecessary secret copies.
There have been some interesting developments in both LLVM and GCC since my previous proposal. On the LLVM side, the recent Function-Boundary Zeroization RFC proposed a per-function model for this sort of zeroization. LLVM already has register-zeroing support exposed through -fzero-call-used-regs. The new RFC proposes extending that infrastructure with stack zeroization, performed after register allocation and final stack layout. That would put the clearing logic where the compiler can account for spill slots and other storage introduced during code generation.
GCC’s STRUB contexts are another possible implementation route for a GCC-based Rust backend. They use cooperation between a function and its caller: the caller initializes a stack watermark, the function updates it, and the caller scrubs the recorded region after return or exception propagation. Together with GCC’s register-zeroing support, I think this sort of first-class zeroization at the compiler level can be supported.
The LLVM RFC proposes the following Clang interface:
[[clang::zeroize_on_return]], which requests function-boundary zeroization.[[clang::sensitive]], which optionally identifies sensitive local storage for more precise stack zeroization.__builtin_clear_sensitive(), which provides guaranteed explicit clearing of a named memory region.
You could imagine a corresponding Rust API like:
#[zeroize_on_return]
pub fn secret_key_op(key: &Key, input: &[u8]) -> Output {
// Perform an operation using transient secret material.
// ...
}
The intended behavior would be to clear the function’s own stack storage, including compiler-generated spills, and the covered residual register state at supported exits. Return values and state required by the calling convention would be preserved.
Each function would be responsible for its own cleanup. Annotating secret_key_op would not automatically protect an unannotated helper’s stack frame. Helpers which handle secrets would need to opt in themselves. This revises the scope of my original proposal to match LLVM’s design.
The LLVM discussion also places this behavior in the compiled callee, without changing the calling convention. That seems like a useful model for Rust: a protected function would retain its cleanup when called through a function pointer or FFI, but annotating a declaration could not retrofit protection onto somebody else’s compiled code.
For LLVM codegen, #[zeroize_on_return] would map to the existing zero-call-used-regs attribute together with the proposed zeroize-stack attribute. The corresponding Clang feature is [[clang::zeroize_on_return]]; its separate [[clang::sensitive]] annotation is for local storage. As in the LLVM proposal, “on return” would be shorthand for the supported function exits, with unwind behavior specified explicitly.
The mapping to GCC STRUB contexts would need care. GCC’s strub("at-calls") mode changes the function interface, while strub("internal") preserves the external interface using a wrapper whose own frame is not scrubbed. We’d need to establish whether that machinery can meet the same Rust contract, including wrapper temporaries and register cleanup. I’d treat STRUB as a candidate implementation mechanism, with LLVM’s proposed per-function semantics defining the starting point.
For targets or codegen backends which don’t support the requested zeroization, my preference would be to accept the attribute without effect, rather than fail compilation. I see this as an opt-in hardening feature with backend-dependent support.
All that said, I thought I'd raise awareness about these features and get the ball rolling on thinking about what a Rust API for them might look like. Curious to hear any feedback on this idea. I realize the proposed LLVM stack zeroization support is still under development, and hypothetical changes there might affect a more concrete proposal, hence why this is just a "Pre-Pre-RFC".