Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions llvm/docs/LangRef.md
Original file line number Diff line number Diff line change
Expand Up @@ -16464,6 +16464,64 @@ If `<count>` is not a well-defined value, the behavior is undefined.
If `<count>` is not zero, `<dest>` 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 <dest>, i32 <len>)
Comment thread
kumarak marked this conversation as resolved.
declare void @llvm.zeroize.p0.i64(ptr <dest>, i64 <len>)
```

##### 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 <attr_align>` parameter attribute can be provided for the
first argument.

##### Semantics:

The '`llvm.zeroize.*`' intrinsics set `<len>` bytes of memory starting at the
destination location to zero.

If `<len>` is 0, it is a no-op modulo the behavior of attributes attached to
the arguments.
If `<len>` is not a well-defined value, the behavior is undefined.
If `<len>` is not zero, `<dest>` 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
Expand Down
21 changes: 21 additions & 0 deletions llvm/include/llvm/IR/Intrinsics.td
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,27 @@ def int_experimental_memset_pattern
NoCapture<ArgIndex<0>>, WriteOnly<ArgIndex<0>>,
ImmArg<ArgIndex<3>>]>;

// 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<ArgIndex<0>>, WriteOnly<ArgIndex<0>>]>;

// FIXME: Add version of these floating point intrinsics which allow non-default
// rounding modes and FP exception handling.

Expand Down
266 changes: 266 additions & 0 deletions llvm/test/Transforms/DeadStoreElimination/zeroize.ll
Original file line number Diff line number Diff line change
@@ -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<O2>' -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)