diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md index 7978b2424d4c0..4f9f7372c0e89 100644 --- a/llvm/docs/LangRef.md +++ b/llvm/docs/LangRef.md @@ -16464,6 +16464,64 @@ 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 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: + +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. + +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. + (int_sqrt)= #### '`llvm.sqrt.*`' Intrinsic diff --git a/llvm/include/llvm/IR/Intrinsics.td b/llvm/include/llvm/IR/Intrinsics.td index 37c9c783465d6..c3f573618dfd2 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. +def int_zeroize + : DefaultAttrsIntrinsic<[], + [llvm_anyptr_ty, // Destination. + llvm_anyint_ty], // Number of bytes to clear. + [IntrWriteMem, IntrInaccessibleMemOrArgMemOnly, + 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)