[llvm] [IR][ASan] Add sanitize.unpadded.size global variable metadata (PR #217257)
via llvm-commits
llvm-commits at lists.llvm.org
Wed Aug 19 03:03:46 PDT 2026
https://github.com/skc7 created https://github.com/llvm/llvm-project/pull/217257
AddressSanitizer replaces each instrumented global with a larger struct that appends a redzone, so an i32 becomes { i32, [28 x i8] }. After that rewrite the declared size of the original object is unrecoverable from the IR, and st_size in the symbol table describes the padded struct rather than the object the user declared.
AMD language runtimes provide queries for the size of device global symbols and functions to copy data to and from device global variables. Runtime gets the needed information form the ELF symbol table. So, when it querires the size of device global variable, it gets the padded size rather than actual size.
Fix is to record the pre-padding size in the IR instead.
This PR adds a fixed metadata kind, !`sanitize.unpadded.size`, holding a single i64 constant operand, attached to the replacement global in `ModuleAddressSanitizer::instrumentGlobals()`. Absence of the attachment means the allocated size is the declared size.
>From 957c144a076219b3c43d27c35f411ad78d43bea3 Mon Sep 17 00:00:00 2001
From: skc7 <Krishna.Sankisa at amd.com>
Date: Wed, 19 Aug 2026 10:42:55 +0530
Subject: [PATCH] [IR][ASan] Add sanitize.unpadded.size global variable
metadata
---
llvm/docs/LangRef.md | 32 +++++++++++++++++++
llvm/include/llvm/IR/FixedMetadataKinds.def | 1 +
llvm/lib/IR/Verifier.cpp | 19 +++++++++++
.../Instrumentation/AddressSanitizer.cpp | 6 ++++
.../AMDGPU/global-unpadded-size.ll | 22 +++++++++++++
.../AddressSanitizer/debug-info-global-var.ll | 2 +-
.../global-unpadded-size-absent.ll | 18 +++++++++++
.../AddressSanitizer/global-unpadded-size.ll | 21 ++++++++++++
.../AddressSanitizer/global_with_comdat.ll | 8 ++---
.../sanitize-unpadded-size-metadata.ll | 26 +++++++++++++++
10 files changed, 150 insertions(+), 5 deletions(-)
create mode 100644 llvm/test/Instrumentation/AddressSanitizer/AMDGPU/global-unpadded-size.ll
create mode 100644 llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size-absent.ll
create mode 100644 llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size.ll
create mode 100644 llvm/test/Verifier/sanitize-unpadded-size-metadata.ll
diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 3a616e8a29fcf..410e98f8e6761 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -9177,6 +9177,38 @@ Example:
This defines a global with type `SHT_LLVM_CFI_JUMP_TABLE` and entry
size 8.
+#### '`sanitize.unpadded.size`' Metadata
+
+The '`sanitize.unpadded.size`' metadata is attached to a global variable
+whose storage a sanitizer has enlarged, and records the size in bytes of
+the object as it was declared, before any padding was added. It has a
+single operand, an `i64` constant.
+
+Sanitizers such as AddressSanitizer replace an instrumented global with a
+larger object that appends a redzone, and transfer the original name to the
+replacement. The declared size is not otherwise recoverable afterwards,
+because `getTypeAllocSize` and the emitted object file size both describe
+the enlarged object.
+
+Example:
+
+```llvm
+ at g = global { i32, [28 x i8] } zeroinitializer, align 32, !sanitize.unpadded.size !0
+
+!0 = !{i64 4}
+```
+
+The global occupies 32 bytes, of which the first 4 belong to the declared
+`i32`.
+
+Absence of this metadata means the global was not padded, so its allocated
+size is also its declared size. Consumers must therefore treat the metadata
+as optional and fall back to the allocated size when it is missing; a
+sanitizer does not pad every global, and passes are permitted to drop
+metadata.
+
+The recorded size must not exceed the size of the global it is attached to.
+
## Module Flags Metadata
diff --git a/llvm/include/llvm/IR/FixedMetadataKinds.def b/llvm/include/llvm/IR/FixedMetadataKinds.def
index 350adfc27bef4..28fc295fbeefe 100644
--- a/llvm/include/llvm/IR/FixedMetadataKinds.def
+++ b/llvm/include/llvm/IR/FixedMetadataKinds.def
@@ -67,3 +67,4 @@ LLVM_FIXED_MD_KIND(MD_mem_cache_hint, "mem.cache_hint", 52)
LLVM_FIXED_MD_KIND(MD_block_uniformity_profile, "block.uniformity.profile", 53)
LLVM_FIXED_MD_KIND(MD_callgraph, "callgraph", 54)
LLVM_FIXED_MD_KIND(MD_metadata_section_kind, "metadata_section_kind", 55)
+LLVM_FIXED_MD_KIND(MD_sanitize_unpadded_size, "sanitize.unpadded.size", 56)
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 09429024e3ae8..3281a68afefaa 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -643,6 +643,25 @@ void Verifier::visitGlobalValue(const GlobalValue &GV) {
Check(EntsizeInt, "entsize field must be ConstantInt", GO, Props);
}
}
+
+ if (const MDNode *UnpaddedSize =
+ GO->getMetadata(LLVMContext::MD_sanitize_unpadded_size)) {
+ Check(UnpaddedSize->getNumOperands() == 1,
+ "sanitize.unpadded.size metadata must have one operand", GO,
+ UnpaddedSize);
+ if (UnpaddedSize->getNumOperands() == 1) {
+ const auto *Size = mdconst::dyn_extract_or_null<ConstantInt>(
+ UnpaddedSize->getOperand(0));
+ Check(Size,
+ "sanitize.unpadded.size operand must be an integer constant", GO,
+ UnpaddedSize);
+ const auto *GVar = dyn_cast<GlobalVariable>(GO);
+ if (Size && GVar && GVar->getValueType()->isSized())
+ Check(Size->getZExtValue() <= GVar->getGlobalSize(DL),
+ "sanitize.unpadded.size must not exceed the size of the global",
+ GO, UnpaddedSize);
+ }
+ }
}
Check(!GV.hasAppendingLinkage() || isa<GlobalVariable>(GV),
diff --git a/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
index d358318195265..6500c6ce44776 100644
--- a/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
@@ -2721,6 +2721,12 @@ void ModuleAddressSanitizer::instrumentGlobals(IRBuilder<> &IRB,
G->replaceAllUsesWith(NewGlobal);
NewGlobal->takeName(G);
+ // The redzone makes getTypeAllocSize() disagree with the declared size;
+ // record the latter for consumers that must report it back to the user.
+ NewGlobal->setMetadata(
+ LLVMContext::MD_sanitize_unpadded_size,
+ MDNode::get(*C, ConstantAsMetadata::get(ConstantInt::get(
+ Type::getInt64Ty(*C), SizeInBytes))));
G->eraseFromParent();
NewGlobals[i] = NewGlobal;
diff --git a/llvm/test/Instrumentation/AddressSanitizer/AMDGPU/global-unpadded-size.ll b/llvm/test/Instrumentation/AddressSanitizer/AMDGPU/global-unpadded-size.ll
new file mode 100644
index 0000000000000..620e310a58bb5
--- /dev/null
+++ b/llvm/test/Instrumentation/AddressSanitizer/AMDGPU/global-unpadded-size.ll
@@ -0,0 +1,22 @@
+; Device globals are the motivating consumer: the host runtime resolves them
+; through ELF symbols, so it needs the declared size rather than the padded one.
+; Redzones grow with the object, so the two sizes can differ by a lot.
+
+; RUN: opt < %s -passes=asan -S | FileCheck %s
+
+target triple = "amdgpu7.00-amd-amdhsa"
+
+ at scalar = addrspace(1) global i32 7, align 4
+ at array = addrspace(1) global [64 x float] zeroinitializer, align 4
+ at huge = addrspace(1) global [1000000 x i8] zeroinitializer, align 1
+ at ro = addrspace(4) global i64 7, align 8
+
+; CHECK: @scalar = addrspace(1) global { i32, [28 x i8] } {{.*}}!sanitize.unpadded.size ![[SCALAR:[0-9]+]]
+; CHECK: @array = addrspace(1) global { [64 x float], [64 x i8] } {{.*}}!sanitize.unpadded.size ![[ARRAY:[0-9]+]]
+; CHECK: @huge = addrspace(1) global { [1000000 x i8], [249984 x i8] } {{.*}}!sanitize.unpadded.size ![[HUGE:[0-9]+]]
+; CHECK: @ro = addrspace(4) global { i64, [24 x i8] } {{.*}}!sanitize.unpadded.size ![[RO:[0-9]+]]
+
+; CHECK-DAG: ![[SCALAR]] = !{i64 4}
+; CHECK-DAG: ![[ARRAY]] = !{i64 256}
+; CHECK-DAG: ![[HUGE]] = !{i64 1000000}
+; CHECK-DAG: ![[RO]] = !{i64 8}
diff --git a/llvm/test/Instrumentation/AddressSanitizer/debug-info-global-var.ll b/llvm/test/Instrumentation/AddressSanitizer/debug-info-global-var.ll
index 0b516e0174d6d..aa139c4d3c1d3 100644
--- a/llvm/test/Instrumentation/AddressSanitizer/debug-info-global-var.ll
+++ b/llvm/test/Instrumentation/AddressSanitizer/debug-info-global-var.ll
@@ -2,7 +2,7 @@
source_filename = "version.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"
-; CHECK: @version = constant { [5 x i8], [27 x i8] } {{.*}}, !dbg ![[GV:.*]]
+; CHECK: @version = constant { [5 x i8], [27 x i8] } {{.*}}, !dbg ![[GV:[0-9]+]]
@version = constant [5 x i8] c"4.00\00", align 1, !dbg !0
diff --git a/llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size-absent.ll b/llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size-absent.ll
new file mode 100644
index 0000000000000..d5d600e3175d0
--- /dev/null
+++ b/llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size-absent.ll
@@ -0,0 +1,18 @@
+; Globals the pass declines to instrument keep no size metadata, so consumers
+; can treat its absence as "the allocated size is the declared size".
+
+; RUN: opt < %s -passes=asan -S | FileCheck %s
+
+target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+; Alignment exceeds the minimum redzone size.
+ at overaligned = global i32 0, align 64
+
+ at tls = thread_local global i32 0, align 4
+
+ at declared = external global i32, align 4
+
+ at opted_out = global i32 0, align 4, no_sanitize_address
+
+; CHECK-NOT: sanitize.unpadded.size
diff --git a/llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size.ll b/llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size.ll
new file mode 100644
index 0000000000000..392743bfd8df0
--- /dev/null
+++ b/llvm/test/Instrumentation/AddressSanitizer/global-unpadded-size.ll
@@ -0,0 +1,21 @@
+; Check that instrumented globals record their pre-padding size, and that the
+; attachment survives a bitcode round trip: the sanitizer pass and the consumer
+; can be in different processes when LTO is in use.
+
+; RUN: opt < %s -passes=asan -S | FileCheck %s
+; RUN: opt < %s -passes=asan | llvm-dis | FileCheck %s
+
+target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+ at small = global i32 7, align 4
+ at exact = global [32 x i8] zeroinitializer, align 1
+ at big = global [256 x i8] zeroinitializer, align 1
+
+; CHECK: @small = global { i32, [28 x i8] } {{.*}}!sanitize.unpadded.size ![[SMALL:[0-9]+]]
+; CHECK: @exact = global { [32 x i8], [32 x i8] } {{.*}}!sanitize.unpadded.size ![[EXACT:[0-9]+]]
+; CHECK: @big = global { [256 x i8], [64 x i8] } {{.*}}!sanitize.unpadded.size ![[BIG:[0-9]+]]
+
+; CHECK-DAG: ![[SMALL]] = !{i64 4}
+; CHECK-DAG: ![[EXACT]] = !{i64 32}
+; CHECK-DAG: ![[BIG]] = !{i64 256}
diff --git a/llvm/test/Instrumentation/AddressSanitizer/global_with_comdat.ll b/llvm/test/Instrumentation/AddressSanitizer/global_with_comdat.ll
index b2578292406bc..455087eb861a8 100644
--- a/llvm/test/Instrumentation/AddressSanitizer/global_with_comdat.ll
+++ b/llvm/test/Instrumentation/AddressSanitizer/global_with_comdat.ll
@@ -102,8 +102,8 @@ target triple = "x86_64-unknown-linux-gnu"
;; Don't place the instrumented globals in a comdat when the unique module ID is empty.
; NOMODULEID: @.str = internal constant { [4 x i8], [28 x i8] } { [4 x i8] c"str\00", [28 x i8] zeroinitializer }, align 32
; NOMODULEID: @_ZL3buf = internal global { [4 x i8], [28 x i8] } zeroinitializer, align 32
-; NOMODULEID: @__asan_global_.str = private global {{.*}}, section "asan_globals"{{.*}}, !associated !0
-; NOMODULEID: @__asan_global__ZL3buf = private global {{.*}}, section "asan_globals"{{.*}}, !associated !1
+; NOMODULEID: @__asan_global_.str = private global {{.*}}, section "asan_globals"{{.*}}, !associated ![[STR_ASSOC:[0-9]+]]
+; NOMODULEID: @__asan_global__ZL3buf = private global {{.*}}, section "asan_globals"{{.*}}, !associated ![[BUF_ASSOC:[0-9]+]]
; NOMODULEID: @llvm.compiler.used = appending global [4 x ptr] [ptr @.str, ptr @_ZL3buf, ptr @__asan_global_.str, ptr @__asan_global__ZL3buf]
; NOMODULEID: define internal void @asan.module_ctor() #[[#]] comdat {
@@ -113,8 +113,8 @@ target triple = "x86_64-unknown-linux-gnu"
; NOMODULEID-NEXT: ret void
; NOMODULEID-NEXT: }
-; NOMODULEID: !0 = !{ptr @.str}
-; NOMODULEID: !1 = !{ptr @_ZL3buf}
+; NOMODULEID: ![[STR_ASSOC]] = !{ptr @.str}
+; NOMODULEID: ![[BUF_ASSOC]] = !{ptr @_ZL3buf}
@.str = private unnamed_addr constant [4 x i8] c"str\00", align 1
@_ZL3buf = internal unnamed_addr global [4 x i8] zeroinitializer, align 1
diff --git a/llvm/test/Verifier/sanitize-unpadded-size-metadata.ll b/llvm/test/Verifier/sanitize-unpadded-size-metadata.ll
new file mode 100644
index 0000000000000..dcbf6644a0222
--- /dev/null
+++ b/llvm/test/Verifier/sanitize-unpadded-size-metadata.ll
@@ -0,0 +1,26 @@
+; RUN: not llvm-as -disable-output < %s -o /dev/null 2>&1 | FileCheck %s
+
+; CHECK: sanitize.unpadded.size metadata must have one operand
+; CHECK-NEXT: ptr @too.many.ops
+; CHECK-NEXT: !0 = !{i64 4, i64 8}
+ at too.many.ops = global { i32, [28 x i8] } zeroinitializer, align 32, !sanitize.unpadded.size !0
+
+; CHECK: sanitize.unpadded.size metadata must have one operand
+; CHECK-NEXT: ptr @empty
+; CHECK-NEXT: !1 = !{}
+ at empty = global { i32, [28 x i8] } zeroinitializer, align 32, !sanitize.unpadded.size !1
+
+; CHECK: sanitize.unpadded.size operand must be an integer constant
+; CHECK-NEXT: ptr @not.an.integer
+; CHECK-NEXT: !2 = !{!"4"}
+ at not.an.integer = global { i32, [28 x i8] } zeroinitializer, align 32, !sanitize.unpadded.size !2
+
+; CHECK: sanitize.unpadded.size must not exceed the size of the global
+; CHECK-NEXT: ptr @too.large
+; CHECK-NEXT: !3 = !{i64 64}
+ at too.large = global { i32, [28 x i8] } zeroinitializer, align 32, !sanitize.unpadded.size !3
+
+!0 = !{i64 4, i64 8}
+!1 = !{}
+!2 = !{!"4"}
+!3 = !{i64 64}
More information about the llvm-commits
mailing list