From 00269dd59e6bc752474487a9e44a7282d898638f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 15:51:06 +0000 Subject: [PATCH 1/3] [IR] Add the llvm.zeroize intrinsic Add an overloaded pointer-and-length intrinsic that writes zero over [dest, dest + len) and guarantees the clear is emitted without calling an external function and without establishing a call frame. That codegen guarantee is the reason for the intrinsic. Clearing a buffer by other means goes through memset, and a memset over a dynamic length or a large fixed one lowers to a libcall. Measured on x86-64 with six values live across the clear, the call forces all six into callee-saved registers and pushes them in the prologue, grows the frame from 8 to 56 bytes, and plants a callee frame below the stack pointer. A clear that spills the live state it is trying to erase, into the stack it is trying to erase, defeats the purpose. Expanded as a pseudo after register allocation the same clear is a rep stosb with no libcall at any size. Non-removability is part of the contract too, but it is not what makes the intrinsic necessary and the previous version of this change oversold it. A volatile memset is also not removable: DSEState::isRemovable refuses to remove volatile memory intrinsics unconditionally, so a volatile memset survives dead store elimination and a full pipeline in every shape the test covers. What a volatile memset cannot do is lower without the libcall. llvm.memset.inline does not close the gap either. It survives dead store elimination and lowers without a libcall, but SROA rewrites it back into a plain memset on non-escaping stack allocas, which is exactly the case this targets, and the no-libcall guarantee is gone before code generation. That rewrite is legal because LangRef defines the two as equivalent. llvm.memset.inline is also the precedent for adding an intrinsic whose optimizer semantics duplicate an existing one. Its Intrinsics.td entry carries byte-identical properties to int_memset and LangRef states its behavior is equivalent to llvm.memset, with a codegen guarantee as the whole of its justification. This is the same argument. The survival half of the contract comes out of the declared memory effects rather than out of changes to any pass. Claiming inaccessible memory in addition to argument memory is more pessimistic than the intrinsic really is, but a write that is not confined to argument pointees is not one dead store elimination can attribute to a single location, so it has nothing to remove, while the argument memory half keeps the write to the region itself visible to alias analysis. llvm.prefetch is pessimistic in the same way. IntrNoDuplicate keeps one clear from becoming several. The test covers the three situations dead store elimination handles with four intrinsics each: a plain memset, a volatile memset, a volatile llvm.memset.inline and llvm.zeroize. The plain memset is removed everywhere and llvm.zeroize survives everywhere; the volatile rows are carried so the file does not read as claiming a distinction it does not show, and the memset.inline rows pin the SROA rewrite. The lowering guarantee is not an IR-level property and is tested with the lowering change rather than here. Nothing lowers the intrinsic yet; expanding it for a target is left to a later change. The name is a recommendation still awaiting sign-off on trailofbits/vspells-ct-internal-notes#64. --- llvm/docs/LangRef.md | 76 +++++ llvm/include/llvm/IR/Intrinsics.td | 21 ++ .../DeadStoreElimination/zeroize.ll | 266 ++++++++++++++++++ 3 files changed, 363 insertions(+) create mode 100644 llvm/test/Transforms/DeadStoreElimination/zeroize.ll diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md index 7978b2424d4c0..c11fc28f3329a 100644 --- a/llvm/docs/LangRef.md +++ b/llvm/docs/LangRef.md @@ -16464,6 +16464,82 @@ If `` is not a well-defined value, the behavior is undefined. If `` is not zero, `` should be well-defined, otherwise the behavior is undefined. +(int_zeroize)= + +#### '`llvm.zeroize`' Intrinsic + +##### Syntax: + +This is an overloaded intrinsic. You can use `llvm.zeroize` on any integer +bit width for the length and for different address spaces. + +``` +declare void @llvm.zeroize.p0.i32(ptr , i32 ) +declare void @llvm.zeroize.p0.i64(ptr , i64 ) +``` + +##### Overview: + +The '`llvm.zeroize.*`' intrinsics write zero over a block of memory so that +whatever can read those addresses afterwards cannot recover what they held, and +guarantee that no external functions are called and no call frame is established +in order to do it. + +##### Arguments: + +The first argument is a pointer to the memory to clear, and the second is an +integer specifying the number of bytes to clear. + +The {ref}`align ` parameter attribute can be provided for the +first argument. + +##### Semantics: + +The '`llvm.zeroize.*`' intrinsics set `` bytes of memory starting at the +destination location to zero. + +If `` is 0, it is a no-op modulo the behavior of attributes attached to +the arguments. +If `` is not a well-defined value, the behavior is undefined. +If `` is not zero, `` should be well-defined, otherwise the +behavior is undefined. + +The behavior of '`llvm.zeroize.*`' is equivalent to the behavior of +'`llvm.memset.*`' with a value of zero and `isvolatile` set to `true`, but the +generated code is guaranteed not to call any external functions and not to +establish a call frame in order to perform the clear. That guarantee is the +property the intrinsic exists for: a clear lowered through `memset` spills the +values live across it and enlarges the frame, leaving in the very stack and +registers being cleaned up the residue the clear was meant to remove. +'`llvm.memset.inline.*`' provides the no-external-call half of it, but only +until a transform rewrites it into a plain '`llvm.memset.*`', which is permitted +precisely because those two are equivalent. + +Performing the write is itself the effect the call is there for, rather than a +means of setting up a value some later read is meant to observe, so the usual +grounds for discarding a store do not apply to it. A transform may not delete a +call to '`llvm.zeroize.*`', narrow the region it covers, or replace the bytes it +writes with any other value, and in particular may not do so because a later +store overwrites the same bytes, because the object's lifetime ends, or because +the object is unreachable from the rest of the program: the region being +provably dead afterwards is the expected case here, not a reason to drop the +write. Calls also carry `noduplicate`, so one call may not be turned into +several. + +A volatile '`llvm.memset.*`' is not removable either, so this part of the +contract is not what separates the two; it is stated because a clearing +intrinsic that could be discarded would be useless, not because it is the +reason the intrinsic exists. + +A transform may move a call to '`llvm.zeroize.*`' to a later point on the same +control-flow paths when nothing in between can read the region, and may combine +two calls covering adjacent or overlapping regions into one call covering their +union. + +The clearing sequence a given target emits is specified separately; the +guarantee that it calls no external function and establishes no call frame holds +for every target. + (int_sqrt)= #### '`llvm.sqrt.*`' Intrinsic diff --git a/llvm/include/llvm/IR/Intrinsics.td b/llvm/include/llvm/IR/Intrinsics.td index 37c9c783465d6..feb9f0e85f30a 100644 --- a/llvm/include/llvm/IR/Intrinsics.td +++ b/llvm/include/llvm/IR/Intrinsics.td @@ -1200,6 +1200,27 @@ def int_experimental_memset_pattern NoCapture>, WriteOnly>, ImmArg>]>; +// Clear the region [dest, dest + len). The write itself is the observable +// effect of the call, so it has to survive even where the region is provably +// never read again. A volatile memset survives too; what this intrinsic adds +// on top is the guarantee that the clear lowers without a libcall and without +// a call frame, which no attribute on a memset expresses. See the LangRef +// entry. +// +// Claiming inaccessible memory in addition to argument memory is what carries +// the survival half. It is more pessimistic than the intrinsic really is, but a write that +// is not confined to argument pointees is not a write dead store elimination +// can attribute to a single location, so it has nothing to remove; the +// argument memory half keeps the write to the region itself visible to alias +// analysis. llvm.prefetch above is pessimistic in the same way and for a +// related reason. IntrNoDuplicate keeps a single clear from becoming several. +def int_zeroize + : DefaultAttrsIntrinsic<[], + [llvm_anyptr_ty, // Destination. + llvm_anyint_ty], // Number of bytes to clear. + [IntrWriteMem, IntrInaccessibleMemOrArgMemOnly, IntrNoDuplicate, + NoCapture>, WriteOnly>]>; + // FIXME: Add version of these floating point intrinsics which allow non-default // rounding modes and FP exception handling. diff --git a/llvm/test/Transforms/DeadStoreElimination/zeroize.ll b/llvm/test/Transforms/DeadStoreElimination/zeroize.ll new file mode 100644 index 0000000000000..06bc3f38466f0 --- /dev/null +++ b/llvm/test/Transforms/DeadStoreElimination/zeroize.ll @@ -0,0 +1,266 @@ +; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6 +; Each group below performs the same write over the same dying object with four +; different intrinsics, in the three situations dead store elimination handles: +; dead at the end of a function, fully overwritten by a later write, and dead at +; the end of the object's lifetime. +; +; A plain memset is removed in all three. llvm.zeroize survives all three, both +; under dead store elimination on its own and under a full pipeline. +; +; The volatile rows are the control that keeps this file honest. A volatile +; memset and a volatile llvm.memset.inline also survive all three: +; DSEState::isRemovable refuses to remove a volatile memory intrinsic +; unconditionally, so at the IR level surviving dead store elimination does not +; distinguish llvm.zeroize from a volatile memory intrinsic. That distinction is +; not made here and this file should not be read as making it. +; +; What llvm.zeroize has over the volatile alternatives is a lowering guarantee: +; a volatile memset over a dynamic or large length lowers to a memset libcall, +; which spills the values live across the clear and grows the frame, while +; llvm.zeroize expands in place. That is not an IR-level property, so it is +; pinned by the X86 lowering test rather than by this one. +; +; Two things this file does show bear on that. llvm.zeroize needs no volatile +; marker to survive, so it stays a plain analyzable write to its region rather +; than an opaque one. And the memset_inline rows record that SROA rewrites +; llvm.memset.inline into a plain llvm.memset under the full pipeline, on +; exactly the kind of non-escaping stack buffer this feature targets, which is +; how the no-libcall guarantee is lost before code generation sees it. + +; RUN: opt < %s -aa-pipeline=basic-aa -passes=dse -S | FileCheck %s +; RUN: opt < %s -passes='default' -S | FileCheck %s --check-prefix=O2 + +declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1) +declare void @llvm.memset.inline.p0.i64(ptr nocapture writeonly, i8, i64, i1) +declare void @llvm.zeroize.p0.i64(ptr nocapture writeonly, i64) + +;; The object dies at the return and never escaped, so nothing can read any of +;; these writes. + +define void @memset_at_end_of_function() { +; CHECK-LABEL: define void @memset_at_end_of_function() { +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @memset_at_end_of_function( +; O2-SAME: ) local_unnamed_addr #[[ATTR2:[0-9]+]] { +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.memset.p0.i64(ptr %buf, i8 0, i64 16, i1 false) + ret void +} + +define void @volatile_memset_at_end_of_function() { +; CHECK-LABEL: define void @volatile_memset_at_end_of_function() { +; CHECK-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[BUF]], i8 0, i64 16, i1 true) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @volatile_memset_at_end_of_function( +; O2-SAME: ) local_unnamed_addr #[[ATTR3:[0-9]+]] { +; O2-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; O2-NEXT: call void @llvm.memset.p0.i64(ptr align 1 [[BUF]], i8 0, i64 16, i1 true) +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.memset.p0.i64(ptr %buf, i8 0, i64 16, i1 true) + ret void +} + +define void @memset_inline_at_end_of_function() { +; CHECK-LABEL: define void @memset_inline_at_end_of_function() { +; CHECK-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; CHECK-NEXT: call void @llvm.memset.inline.p0.i64(ptr [[BUF]], i8 0, i64 16, i1 true) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @memset_inline_at_end_of_function( +; O2-SAME: ) local_unnamed_addr #[[ATTR3]] { +; O2-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; O2-NEXT: call void @llvm.memset.p0.i64(ptr align 1 [[BUF]], i8 0, i64 16, i1 true) +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.memset.inline.p0.i64(ptr %buf, i8 0, i64 16, i1 true) + ret void +} + +define void @zeroize_at_end_of_function() { +; CHECK-LABEL: define void @zeroize_at_end_of_function() { +; CHECK-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; CHECK-NEXT: call void @llvm.zeroize.p0.i64(ptr [[BUF]], i64 16) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @zeroize_at_end_of_function( +; O2-SAME: ) local_unnamed_addr #[[ATTR4:[0-9]+]] { +; O2-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; O2-NEXT: call void @llvm.zeroize.p0.i64(ptr nonnull [[BUF]], i64 16) +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.zeroize.p0.i64(ptr %buf, i64 16) + ret void +} + +;; A later write covers exactly the same bytes. + +define void @memset_overwritten(ptr %p) { +; CHECK-LABEL: define void @memset_overwritten( +; CHECK-SAME: ptr [[P:%.*]]) { +; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[P]], i8 1, i64 16, i1 false) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @memset_overwritten( +; O2-SAME: ptr nofree writeonly captures(none) initializes((0, 16)) [[P:%.*]]) local_unnamed_addr #[[ATTR5:[0-9]+]] { +; O2-NEXT: tail call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(16) [[P]], i8 1, i64 16, i1 false) +; O2-NEXT: ret void +; + call void @llvm.memset.p0.i64(ptr %p, i8 0, i64 16, i1 false) + call void @llvm.memset.p0.i64(ptr %p, i8 1, i64 16, i1 false) + ret void +} + +define void @volatile_memset_overwritten(ptr %p) { +; CHECK-LABEL: define void @volatile_memset_overwritten( +; CHECK-SAME: ptr [[P:%.*]]) { +; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[P]], i8 0, i64 16, i1 true) +; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[P]], i8 1, i64 16, i1 false) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @volatile_memset_overwritten( +; O2-SAME: ptr nofree writeonly captures(address) [[P:%.*]]) local_unnamed_addr #[[ATTR6:[0-9]+]] { +; O2-NEXT: tail call void @llvm.memset.p0.i64(ptr [[P]], i8 0, i64 16, i1 true) +; O2-NEXT: tail call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(16) [[P]], i8 1, i64 16, i1 false) +; O2-NEXT: ret void +; + call void @llvm.memset.p0.i64(ptr %p, i8 0, i64 16, i1 true) + call void @llvm.memset.p0.i64(ptr %p, i8 1, i64 16, i1 false) + ret void +} + +define void @memset_inline_overwritten(ptr %p) { +; CHECK-LABEL: define void @memset_inline_overwritten( +; CHECK-SAME: ptr [[P:%.*]]) { +; CHECK-NEXT: call void @llvm.memset.inline.p0.i64(ptr [[P]], i8 0, i64 16, i1 true) +; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[P]], i8 1, i64 16, i1 false) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @memset_inline_overwritten( +; O2-SAME: ptr nofree writeonly captures(address) [[P:%.*]]) local_unnamed_addr #[[ATTR6]] { +; O2-NEXT: tail call void @llvm.memset.inline.p0.i64(ptr [[P]], i8 0, i64 16, i1 true) +; O2-NEXT: tail call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(16) [[P]], i8 1, i64 16, i1 false) +; O2-NEXT: ret void +; + call void @llvm.memset.inline.p0.i64(ptr %p, i8 0, i64 16, i1 true) + call void @llvm.memset.p0.i64(ptr %p, i8 1, i64 16, i1 false) + ret void +} + +define void @zeroize_overwritten(ptr %p) { +; CHECK-LABEL: define void @zeroize_overwritten( +; CHECK-SAME: ptr [[P:%.*]]) { +; CHECK-NEXT: call void @llvm.zeroize.p0.i64(ptr [[P]], i64 16) +; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[P]], i8 1, i64 16, i1 false) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @zeroize_overwritten( +; O2-SAME: ptr nofree writeonly captures(none) [[P:%.*]]) local_unnamed_addr #[[ATTR7:[0-9]+]] { +; O2-NEXT: tail call void @llvm.zeroize.p0.i64(ptr [[P]], i64 16) +; O2-NEXT: tail call void @llvm.memset.p0.i64(ptr noundef nonnull align 1 dereferenceable(16) [[P]], i8 1, i64 16, i1 false) +; O2-NEXT: ret void +; + call void @llvm.zeroize.p0.i64(ptr %p, i64 16) + call void @llvm.memset.p0.i64(ptr %p, i8 1, i64 16, i1 false) + ret void +} + +;; The lifetime of the object ends immediately after the write. + +define void @memset_before_lifetime_end() { +; CHECK-LABEL: define void @memset_before_lifetime_end() { +; CHECK-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; CHECK-NEXT: call void @llvm.lifetime.start.p0(ptr [[BUF]]) +; CHECK-NEXT: call void @llvm.lifetime.end.p0(ptr [[BUF]]) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @memset_before_lifetime_end( +; O2-SAME: ) local_unnamed_addr #[[ATTR2]] { +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.lifetime.start.p0(ptr %buf) + call void @llvm.memset.p0.i64(ptr %buf, i8 0, i64 16, i1 false) + call void @llvm.lifetime.end.p0(ptr %buf) + ret void +} + +define void @volatile_memset_before_lifetime_end() { +; CHECK-LABEL: define void @volatile_memset_before_lifetime_end() { +; CHECK-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; CHECK-NEXT: call void @llvm.lifetime.start.p0(ptr [[BUF]]) +; CHECK-NEXT: call void @llvm.memset.p0.i64(ptr [[BUF]], i8 0, i64 16, i1 true) +; CHECK-NEXT: call void @llvm.lifetime.end.p0(ptr [[BUF]]) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @volatile_memset_before_lifetime_end( +; O2-SAME: ) local_unnamed_addr #[[ATTR3]] { +; O2-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; O2-NEXT: call void @llvm.lifetime.start.p0(ptr nonnull [[BUF]]) +; O2-NEXT: call void @llvm.memset.p0.i64(ptr align 1 [[BUF]], i8 0, i64 16, i1 true) +; O2-NEXT: call void @llvm.lifetime.end.p0(ptr nonnull [[BUF]]) +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.lifetime.start.p0(ptr %buf) + call void @llvm.memset.p0.i64(ptr %buf, i8 0, i64 16, i1 true) + call void @llvm.lifetime.end.p0(ptr %buf) + ret void +} + +define void @memset_inline_before_lifetime_end() { +; CHECK-LABEL: define void @memset_inline_before_lifetime_end() { +; CHECK-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; CHECK-NEXT: call void @llvm.lifetime.start.p0(ptr [[BUF]]) +; CHECK-NEXT: call void @llvm.memset.inline.p0.i64(ptr [[BUF]], i8 0, i64 16, i1 true) +; CHECK-NEXT: call void @llvm.lifetime.end.p0(ptr [[BUF]]) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @memset_inline_before_lifetime_end( +; O2-SAME: ) local_unnamed_addr #[[ATTR3]] { +; O2-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; O2-NEXT: call void @llvm.lifetime.start.p0(ptr nonnull [[BUF]]) +; O2-NEXT: call void @llvm.memset.p0.i64(ptr align 1 [[BUF]], i8 0, i64 16, i1 true) +; O2-NEXT: call void @llvm.lifetime.end.p0(ptr nonnull [[BUF]]) +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.lifetime.start.p0(ptr %buf) + call void @llvm.memset.inline.p0.i64(ptr %buf, i8 0, i64 16, i1 true) + call void @llvm.lifetime.end.p0(ptr %buf) + ret void +} + +define void @zeroize_before_lifetime_end() { +; CHECK-LABEL: define void @zeroize_before_lifetime_end() { +; CHECK-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; CHECK-NEXT: call void @llvm.lifetime.start.p0(ptr [[BUF]]) +; CHECK-NEXT: call void @llvm.zeroize.p0.i64(ptr [[BUF]], i64 16) +; CHECK-NEXT: call void @llvm.lifetime.end.p0(ptr [[BUF]]) +; CHECK-NEXT: ret void +; +; O2-LABEL: define void @zeroize_before_lifetime_end( +; O2-SAME: ) local_unnamed_addr #[[ATTR4]] { +; O2-NEXT: [[BUF:%.*]] = alloca [16 x i8], align 1 +; O2-NEXT: call void @llvm.lifetime.start.p0(ptr nonnull [[BUF]]) +; O2-NEXT: call void @llvm.zeroize.p0.i64(ptr nonnull [[BUF]], i64 16) +; O2-NEXT: call void @llvm.lifetime.end.p0(ptr nonnull [[BUF]]) +; O2-NEXT: ret void +; + %buf = alloca [16 x i8], align 1 + call void @llvm.lifetime.start.p0(ptr %buf) + call void @llvm.zeroize.p0.i64(ptr %buf, i64 16) + call void @llvm.lifetime.end.p0(ptr %buf) + ret void +} + +declare void @llvm.lifetime.start.p0(ptr nocapture) +declare void @llvm.lifetime.end.p0(ptr nocapture) From 528641de771db2e789addf097a6bb098b2590c28 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 15:55:03 +0000 Subject: [PATCH 2/3] [IR] Drop IntrNoDuplicate from llvm.zeroize The property defeats the intrinsic it was meant to protect. InlineCost refuses a call site whose callee contains a noduplicate call unless that site is the sole call to a local function, where inlining deletes the original and duplicates nothing. Every other site fails outright, with cost=never and noduplicate as the reason. So a small internal helper that wraps a clear and is called from more than one place, which is the natural way to reach for this intrinsic, is refused at every one of its call sites, and the clear ends up pinning open any caller it appears in. An intrinsic whose purpose is to be dropped wherever a buffer needs erasing cannot also be a barrier to inlining the code that erases it. The justification did not hold either. Clearing a region is idempotent: performing it twice leaves the region in the state one pass would have left it, so there is nothing a duplicated clear can undo and no reason to keep the call sites distinct. The half of the contract that does matter, that the write survives even where the region is provably never read again, does not rest on this property and is unchanged. It comes from the declared memory effects, and the dead store elimination test continues to pin it. --- llvm/docs/LangRef.md | 3 +-- llvm/include/llvm/IR/Intrinsics.td | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md index c11fc28f3329a..40482808bd35f 100644 --- a/llvm/docs/LangRef.md +++ b/llvm/docs/LangRef.md @@ -16523,8 +16523,7 @@ writes with any other value, and in particular may not do so because a later store overwrites the same bytes, because the object's lifetime ends, or because the object is unreachable from the rest of the program: the region being provably dead afterwards is the expected case here, not a reason to drop the -write. Calls also carry `noduplicate`, so one call may not be turned into -several. +write. A volatile '`llvm.memset.*`' is not removable either, so this part of the contract is not what separates the two; it is stated because a clearing diff --git a/llvm/include/llvm/IR/Intrinsics.td b/llvm/include/llvm/IR/Intrinsics.td index feb9f0e85f30a..c3f573618dfd2 100644 --- a/llvm/include/llvm/IR/Intrinsics.td +++ b/llvm/include/llvm/IR/Intrinsics.td @@ -1213,12 +1213,12 @@ def int_experimental_memset_pattern // can attribute to a single location, so it has nothing to remove; the // argument memory half keeps the write to the region itself visible to alias // analysis. llvm.prefetch above is pessimistic in the same way and for a -// related reason. IntrNoDuplicate keeps a single clear from becoming several. +// related reason. def int_zeroize : DefaultAttrsIntrinsic<[], [llvm_anyptr_ty, // Destination. llvm_anyint_ty], // Number of bytes to clear. - [IntrWriteMem, IntrInaccessibleMemOrArgMemOnly, IntrNoDuplicate, + [IntrWriteMem, IntrInaccessibleMemOrArgMemOnly, NoCapture>, WriteOnly>]>; // FIXME: Add version of these floating point intrinsics which allow non-default From 2d8751e1ab0c42394d316c70ddab9833cc5530a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 07:57:55 +0000 Subject: [PATCH 3/3] [IR] Trim the llvm.zeroize LangRef entry The entry argued for the intrinsic instead of specifying it. Drop the rationale for why a clear must not go through memset, the comparison with llvm.memset.inline, the explanation of why the usual grounds for discarding a store do not apply, and the aside noting that a volatile llvm.memset.* is not removable either. None of it constrains an implementation or a transform; the normative statements it surrounded are kept verbatim. Also drop the claim that the no-external-call, no-call-frame guarantee "holds for every target". No target lowers llvm.zeroize yet, so nothing backs the claim, and it stays wrong for the targets the stacked X86 expansion does not cover. Documentation only; Intrinsics.td and the tests are unchanged. --- llvm/docs/LangRef.md | 41 ++++++++++++----------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md index 40482808bd35f..4f9f7372c0e89 100644 --- a/llvm/docs/LangRef.md +++ b/llvm/docs/LangRef.md @@ -16480,10 +16480,10 @@ declare void @llvm.zeroize.p0.i64(ptr , i64 ) ##### Overview: -The '`llvm.zeroize.*`' intrinsics write zero over a block of memory so that -whatever can read those addresses afterwards cannot recover what they held, and -guarantee that no external functions are called and no call frame is established -in order to do it. +The '`llvm.zeroize.*`' intrinsics write zero over a block of memory so that its +former contents cannot be recovered from those addresses, and guarantee that no +external functions are called and no call frame is established in order to do +it. ##### Arguments: @@ -16507,37 +16507,20 @@ behavior is undefined. The behavior of '`llvm.zeroize.*`' is equivalent to the behavior of '`llvm.memset.*`' with a value of zero and `isvolatile` set to `true`, but the generated code is guaranteed not to call any external functions and not to -establish a call frame in order to perform the clear. That guarantee is the -property the intrinsic exists for: a clear lowered through `memset` spills the -values live across it and enlarges the frame, leaving in the very stack and -registers being cleaned up the residue the clear was meant to remove. -'`llvm.memset.inline.*`' provides the no-external-call half of it, but only -until a transform rewrites it into a plain '`llvm.memset.*`', which is permitted -precisely because those two are equivalent. - -Performing the write is itself the effect the call is there for, rather than a -means of setting up a value some later read is meant to observe, so the usual -grounds for discarding a store do not apply to it. A transform may not delete a -call to '`llvm.zeroize.*`', narrow the region it covers, or replace the bytes it -writes with any other value, and in particular may not do so because a later -store overwrites the same bytes, because the object's lifetime ends, or because -the object is unreachable from the rest of the program: the region being -provably dead afterwards is the expected case here, not a reason to drop the -write. - -A volatile '`llvm.memset.*`' is not removable either, so this part of the -contract is not what separates the two; it is stated because a clearing -intrinsic that could be discarded would be useless, not because it is the -reason the intrinsic exists. +establish a call frame in order to perform the clear. + +A transform may not delete a call to '`llvm.zeroize.*`', narrow the region it +covers, or replace the bytes it writes with any other value, and in particular +may not do so because a later store overwrites the same bytes, because the +object's lifetime ends, or because the object is unreachable from the rest of +the program. A transform may move a call to '`llvm.zeroize.*`' to a later point on the same control-flow paths when nothing in between can read the region, and may combine two calls covering adjacent or overlapping regions into one call covering their union. -The clearing sequence a given target emits is specified separately; the -guarantee that it calls no external function and establishes no call frame holds -for every target. +The clearing sequence a given target emits is specified separately. (int_sqrt)=