[llvm] [ASan] Introduce bidirectional pivoting shadow mapping for baremetal targets (PR #213787)
Schrodinger ZHU Yifan via llvm-commits
llvm-commits at lists.llvm.org
Mon Aug 3 15:48:02 PDT 2026
https://github.com/SchrodingerZhu created https://github.com/llvm/llvm-project/pull/213787
[ASan] Introduce bidirectional pivoting shadow mapping for baremetal targets
This change builds upon #213083 and is intended to be used in conjunction with #212890 and #212882 for baremetal ASan support.
In baremetal environments such as the Raspberry Pi RP2350 (Pico SDK), the address space is partitioned into dedicated memory zones (e.g., Flash ROM at 0x1000_0000 and SRAM at 0x2000_0000). Standard shift-and-add shadow translation cannot bridge these disparate regions without excessive memory overhead or complex static shadow byte calculations via modified linker scripts and LLD extensions.
This PR introduces Bidirectional Pivoting Address Sanitizer (BPASan), a zero-branch shadow mapping strategy that enables ASan on RP2350 and similar baremetal architectures without requiring statically precomputed shadow tables or specialized linker modifications.
Instead of translating addresses via a constant offset across a single virtual memory space, BPASan anchors shadow memory around a midpoint pivot within SRAM (configured via `-asan-mapping-offset` as the shadow pivot and `-asan-memory-pivot` as the application RAM boundary). By stripping top region bits (via #213083's `-asan-preserve-topbits`), the shadow memory cleanly divides at the pivot:
- The upward-growing shadow area maps application SRAM (positive memory deltas).
- The downward-extending shadow area covers Flash/ROM (negative sign-extended memory deltas).
The shadow address is derived according to the following arithmetic expression:
Shadow = ShadowBase + ((Addr & OffsetMask) - (MemoryPivot & OffsetMask)) >> Scale
Notice an interesting architectural property of this design: enlarging the instrumented Flash region increases demand for Flash shadow memory, which simply lifts both the memory pivot and shadow pivot upward in SRAM while keeping the logical mapping invariant. This allows cleanly distinguishing Flash and SRAM shadow regions purely through the sign bit of the masked delta, achieving high instruction density and zero branch overhead on microcontrollers without MMUs.
Assisted-by: Gemini based automation tools (human-in-the-loop)
TAG=agy
CONV=9a499613-2ae2-4aae-ace8-9354cb5caa4f
>From 5276287fd4be0890b9698a24f8a303a1c5f4d1e7 Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Thu, 30 Jul 2026 10:32:59 -0700
Subject: [PATCH 1/2] Allow address sanitizer to preserve topbits of sanitized
pointer.
Some baremetal environments use top bits to represent the zone of memory mapping; asan should preserve the top bits and maps the shadow to corresponding region. This may also apply to architectures that allow user to apply arbitrary pointer tagging at topbits; which should be preserved or at least ruled out when computing shadow address.
Assisted-by: Gemini based automation tools (human-in-the-loop)
TAG=agy
CONV=65695035-39d8-4cf6-b381-c8fd7e1bcce3
---
.../Instrumentation/AddressSanitizer.cpp | 42 +++++++++++++++----
.../address-preserve-topbits.ll | 30 +++++++++++++
2 files changed, 65 insertions(+), 7 deletions(-)
create mode 100644 llvm/test/Instrumentation/AddressSanitizer/address-preserve-topbits.ll
diff --git a/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
index 2fae7d8c15ff3..73218da8398ad 100644
--- a/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
@@ -475,6 +475,10 @@ STATISTIC(NumOptimizedAccessesToGlobalVar,
"Number of optimized accesses to global vars");
STATISTIC(NumOptimizedAccessesToStackVar,
"Number of optimized accesses to stack vars");
+static cl::opt<unsigned> ClAsanPreserveTopBits(
+ "asan-preserve-topbits",
+ cl::desc("Number of address top bits to preserve during shadow mapping"),
+ cl::Hidden, cl::init(0));
namespace {
@@ -488,6 +492,7 @@ struct ShadowMapping {
uint64_t Offset;
bool OrShadowOffset;
bool InGlobal;
+ unsigned PreserveTopBits;
};
} // end anonymous namespace
@@ -623,6 +628,7 @@ static ShadowMapping getShadowMapping(const Triple &TargetTriple, int LongSize,
!(Mapping.Offset & (Mapping.Offset - 1)) &&
Mapping.Offset != kDynamicShadowSentinel;
Mapping.InGlobal = ClWithIfunc && IsAndroid && IsArmOrThumb;
+ Mapping.PreserveTopBits = ClAsanPreserveTopBits;
return Mapping;
}
@@ -1431,19 +1437,41 @@ Value *AddressSanitizer::memToShadow(Value *Shadow, IRBuilder<> &IRB) {
Shadow = IRB.CreateAnd(Shadow,
ConstantInt::get(IntptrTy, ~(uint64_t(0x0f) << 56)));
}
- // Shadow >> scale
- Shadow = IRB.CreateLShr(Shadow, Mapping.Scale);
- if (Mapping.Offset == 0) return Shadow;
- // (Shadow >> scale) | offset
+ // PtrMask to ensure bit operations stay in platform address range
+ uint64_t PtrMask = IntptrTy->getIntegerBitWidth() == 64
+ ? ~0ULL
+ : ((1ULL << IntptrTy->getIntegerBitWidth()) - 1);
+
Value *ShadowBase;
if (LocalDynamicShadow)
ShadowBase = LocalDynamicShadow;
else
- ShadowBase = ConstantInt::get(IntptrTy, Mapping.Offset);
+ ShadowBase = ConstantInt::get(IntptrTy, Mapping.Offset & PtrMask);
+
+ if (Mapping.PreserveTopBits > 0) {
+ unsigned BitWidth = IntptrTy->getIntegerBitWidth();
+ uint64_t PreservedMask = 0;
+ if (Mapping.PreserveTopBits < BitWidth)
+ PreservedMask = (~0ULL << (BitWidth - Mapping.PreserveTopBits)) & PtrMask;
+ uint64_t OffsetMask = ~PreservedMask & PtrMask;
+
+ Value *TopBits =
+ IRB.CreateAnd(Shadow, ConstantInt::get(IntptrTy, PreservedMask));
+ Value *BottomBits =
+ IRB.CreateAnd(Shadow, ConstantInt::get(IntptrTy, OffsetMask));
+ BottomBits = IRB.CreateLShr(BottomBits, Mapping.Scale);
+ Shadow = IRB.CreateOr(TopBits, BottomBits);
+ return IRB.CreateAdd(Shadow, ShadowBase);
+ }
+
+ // Shadow >> scale
+ Shadow = IRB.CreateLShr(Shadow, Mapping.Scale);
+ if (Mapping.Offset == 0)
+ return Shadow;
+ // (Shadow >> scale) | offset
if (Mapping.OrShadowOffset)
return IRB.CreateOr(Shadow, ShadowBase);
- else
- return IRB.CreateAdd(Shadow, ShadowBase);
+ return IRB.CreateAdd(Shadow, ShadowBase);
}
// Instrument memset/memmove/memcpy
diff --git a/llvm/test/Instrumentation/AddressSanitizer/address-preserve-topbits.ll b/llvm/test/Instrumentation/AddressSanitizer/address-preserve-topbits.ll
new file mode 100644
index 0000000000000..8ab208664a647
--- /dev/null
+++ b/llvm/test/Instrumentation/AddressSanitizer/address-preserve-topbits.ll
@@ -0,0 +1,30 @@
+; RUN: opt < %s -passes=asan -asan-preserve-topbits=4 -S -mtriple=x86_64-unknown-linux-gnu | FileCheck %s --check-prefixes=CHECK,CHECK-64
+; RUN: opt < %s -passes=asan -asan-preserve-topbits=4 -S -mtriple=arm-none-eabi | FileCheck %s --check-prefixes=CHECK,CHECK-32
+
+define void @test(ptr %p) sanitize_address {
+; CHECK-LABEL: define void @test(
+; CHECK-SAME: ptr [[P:%.*]])
+entry:
+; CHECK-NEXT: entry:
+; CHECK-64-NEXT: [[ADDR:%[0-9]+]] = ptrtoint ptr [[P]] to i64
+; CHECK-64-NEXT: [[TOP:%[0-9]+]] = and i64 [[ADDR]], -1152921504606846976
+; CHECK-64-NEXT: [[BOTTOM:%[0-9]+]] = and i64 [[ADDR]], 1152921504606846975
+; CHECK-64-NEXT: [[BOTTOM_SHR:%[0-9]+]] = lshr i64 [[BOTTOM]], 3
+; CHECK-64-NEXT: [[SHADOW:%[0-9]+]] = or i64 [[TOP]], [[BOTTOM_SHR]]
+; CHECK-64-NEXT: [[SHADOW_ADDR:%[0-9]+]] = add i64 [[SHADOW]], 2147450880
+
+; CHECK-32-NEXT: [[ADDR:%[0-9]+]] = ptrtoint ptr [[P]] to i32
+; CHECK-32-NEXT: [[TOP:%[0-9]+]] = and i32 [[ADDR]], -268435456
+; CHECK-32-NEXT: [[BOTTOM:%[0-9]+]] = and i32 [[ADDR]], 268435455
+; CHECK-32-NEXT: [[BOTTOM_SHR:%[0-9]+]] = lshr i32 [[BOTTOM]], 3
+; CHECK-32-NEXT: [[SHADOW:%[0-9]+]] = or i32 [[TOP]], [[BOTTOM_SHR]]
+; CHECK-32-NEXT: [[SHADOW_ADDR:%[0-9]+]] = add i32 [[SHADOW]], 536870912
+
+; CHECK-NEXT: [[SHADOW_PTR:%[0-9]+]] = inttoptr {{i(64|32)}} [[SHADOW_ADDR]] to ptr
+; CHECK-NEXT: [[SHADOW_BYTE:%[0-9]+]] = load i8, ptr [[SHADOW_PTR]], align 1
+; CHECK-NEXT: [[IS_POISONED:%[0-9]+]] = icmp ne i8 [[SHADOW_BYTE]], 0
+; CHECK-NEXT: br i1 [[IS_POISONED]], label %[[L_REPORT:[0-9]+]], label %[[L_STORE:[0-9]+]]
+
+ store i32 0, ptr %p, align 4
+ ret void
+}
>From d4903d41233b8d33e0e91fa1e02f43018f2841e2 Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Mon, 3 Aug 2026 15:47:46 -0700
Subject: [PATCH 2/2] [ASan] Introduce bidirectional pivoting shadow mapping
for baremetal targets
This change builds upon #213083 and is intended to be used in conjunction with #212890 and #212882 for baremetal ASan support.
In baremetal environments such as the Raspberry Pi RP2350 (Pico SDK), the address space is partitioned into dedicated memory zones (e.g., Flash ROM at 0x1000_0000 and SRAM at 0x2000_0000). Standard shift-and-add shadow translation cannot bridge these disparate regions without excessive memory overhead or complex static shadow byte calculations via modified linker scripts and LLD extensions.
This PR introduces Bidirectional Pivoting Address Sanitizer (BPASan), a zero-branch shadow mapping strategy that enables ASan on RP2350 and similar baremetal architectures without requiring statically precomputed shadow tables or specialized linker modifications.
Instead of translating addresses via a constant offset across a single virtual memory space, BPASan anchors shadow memory around a midpoint pivot within SRAM (configured via `-asan-mapping-offset` as the shadow pivot and `-asan-memory-pivot` as the application RAM boundary). By stripping top region bits (via #213083's `-asan-preserve-topbits`), the shadow memory cleanly divides at the pivot:
- The upward-growing shadow area maps application SRAM (positive memory deltas).
- The downward-extending shadow area covers Flash/ROM (negative sign-extended memory deltas).
The shadow address is derived according to the following arithmetic expression:
Shadow = ShadowBase + ((Addr & OffsetMask) - (MemoryPivot & OffsetMask)) >> Scale
Notice an interesting architectural property of this design: enlarging the instrumented Flash region increases demand for Flash shadow memory, which simply lifts both the memory pivot and shadow pivot upward in SRAM while keeping the logical mapping invariant. This allows cleanly distinguishing Flash and SRAM shadow regions purely through the sign bit of the masked delta, achieving high instruction density and zero branch overhead on microcontrollers without MMUs.
Assisted-by: Gemini based automation tools (human-in-the-loop)
TAG=agy
CONV=9a499613-2ae2-4aae-ace8-9354cb5caa4f
---
.../Instrumentation/AddressSanitizer.cpp | 42 ++++++++++++++++
.../AddressSanitizer/baremetal-pivoting.ll | 48 +++++++++++++++++++
2 files changed, 90 insertions(+)
create mode 100644 llvm/test/Instrumentation/AddressSanitizer/baremetal-pivoting.ll
diff --git a/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
index 73218da8398ad..3c0d5934f9119 100644
--- a/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
@@ -479,6 +479,11 @@ static cl::opt<unsigned> ClAsanPreserveTopBits(
"asan-preserve-topbits",
cl::desc("Number of address top bits to preserve during shadow mapping"),
cl::Hidden, cl::init(0));
+static cl::opt<uint64_t> ClAsanMemoryPivot(
+ "asan-memory-pivot",
+ cl::desc("Memory pivot boundary between lower (negative delta) and upper "
+ "positive delta memory regions"),
+ cl::Hidden, cl::init(0));
namespace {
@@ -493,6 +498,7 @@ struct ShadowMapping {
bool OrShadowOffset;
bool InGlobal;
unsigned PreserveTopBits;
+ uint64_t MemoryPivot;
};
} // end anonymous namespace
@@ -629,6 +635,23 @@ static ShadowMapping getShadowMapping(const Triple &TargetTriple, int LongSize,
Mapping.Offset != kDynamicShadowSentinel;
Mapping.InGlobal = ClWithIfunc && IsAndroid && IsArmOrThumb;
Mapping.PreserveTopBits = ClAsanPreserveTopBits;
+ if (Mapping.PreserveTopBits >= (unsigned)LongSize) {
+ report_fatal_error(
+ "-asan-preserve-topbits must be strictly smaller than the target "
+ "pointer bit width.");
+ }
+
+ Mapping.MemoryPivot = ClAsanMemoryPivot;
+ if (Mapping.MemoryPivot > 0) {
+ if (ClForceDynamicShadow || Mapping.Offset == kDynamicShadowSentinel ||
+ ClMappingOffset.getNumOccurrences() == 0 || Mapping.Offset == 0) {
+ report_fatal_error(
+ "ASan memory pivoting (-asan-memory-pivot) requires an explicit "
+ "static shadow base (-asan-mapping-offset) and is incompatible with "
+ "zero or dynamic shadow mapping.");
+ }
+ Mapping.OrShadowOffset = false;
+ }
return Mapping;
}
@@ -1448,6 +1471,25 @@ Value *AddressSanitizer::memToShadow(Value *Shadow, IRBuilder<> &IRB) {
else
ShadowBase = ConstantInt::get(IntptrTy, Mapping.Offset & PtrMask);
+ if (Mapping.MemoryPivot > 0) {
+ unsigned BitWidth = IntptrTy->getIntegerBitWidth();
+ uint64_t OffsetMask = PtrMask;
+ if (Mapping.PreserveTopBits > 0 && Mapping.PreserveTopBits < BitWidth) {
+ uint64_t PreservedMask =
+ (~0ULL << (BitWidth - Mapping.PreserveTopBits)) & PtrMask;
+ OffsetMask = ~PreservedMask & PtrMask;
+ }
+ Value *MaskedAddr = Shadow;
+ if (Mapping.PreserveTopBits > 0)
+ MaskedAddr =
+ IRB.CreateAnd(Shadow, ConstantInt::get(IntptrTy, OffsetMask));
+ Value *MaskedPivot =
+ ConstantInt::get(IntptrTy, Mapping.MemoryPivot & OffsetMask);
+ Value *Delta = IRB.CreateSub(MaskedAddr, MaskedPivot);
+ Value *ShiftedDelta = IRB.CreateAShr(Delta, Mapping.Scale);
+ return IRB.CreateAdd(ShadowBase, ShiftedDelta);
+ }
+
if (Mapping.PreserveTopBits > 0) {
unsigned BitWidth = IntptrTy->getIntegerBitWidth();
uint64_t PreservedMask = 0;
diff --git a/llvm/test/Instrumentation/AddressSanitizer/baremetal-pivoting.ll b/llvm/test/Instrumentation/AddressSanitizer/baremetal-pivoting.ll
new file mode 100644
index 0000000000000..c8905c7733cb4
--- /dev/null
+++ b/llvm/test/Instrumentation/AddressSanitizer/baremetal-pivoting.ll
@@ -0,0 +1,48 @@
+; RUN: opt < %s -passes=asan -asan-memory-pivot=0x20010000 -asan-mapping-offset=0x20004000 -asan-preserve-topbits=4 -S -mtriple=arm-none-eabi | FileCheck %s --check-prefixes=CHECK,CHECK-32-MASK
+; RUN: opt < %s -passes=asan -asan-memory-pivot=0x20010000 -asan-mapping-offset=0x20004000 -S -mtriple=arm-none-eabi | FileCheck %s --check-prefixes=CHECK,CHECK-32-NOMASK
+; RUN: opt < %s -passes=asan -asan-memory-pivot=0x1000000000010000 -asan-mapping-offset=0x2000000000004000 -asan-preserve-topbits=16 -S -mtriple=aarch64-none-elf | FileCheck %s --check-prefixes=CHECK,CHECK-64-MASK
+
+; RUN: not --crash opt < %s -passes=asan -asan-memory-pivot=0x20010000 -S -mtriple=arm-none-eabi 2>&1 | FileCheck %s --check-prefix=ERR-MISSING-BASE
+; RUN: not --crash opt < %s -passes=asan -asan-preserve-topbits=32 -S -mtriple=arm-none-eabi 2>&1 | FileCheck %s --check-prefix=ERR-INVALID-TOPBITS
+
+; ERR-MISSING-BASE: LLVM ERROR: ASan memory pivoting (-asan-memory-pivot) requires an explicit static shadow base (-asan-mapping-offset) and is incompatible with zero or dynamic shadow mapping.
+; ERR-INVALID-TOPBITS: LLVM ERROR: -asan-preserve-topbits must be strictly smaller than the target pointer bit width.
+
+define void @test(ptr %p) sanitize_address {
+; CHECK-LABEL: define void @test(
+; CHECK-SAME: ptr [[P:%.*]])
+entry:
+; CHECK-NEXT: entry:
+; --- 32-bit RP2350 / Pico SDK layout ---
+; Memory Pivot: 0x20010000 (masked with 0x0FFFFFFF -> 0x00010000 = 65536)
+; Shadow Base: 0x20004000 (decimal 536887296)
+; Region Mask (top 4 bits stripped): 0x0FFFFFFF (decimal 268435455)
+; CHECK-32-MASK-NEXT: [[ADDR:%[0-9]+]] = ptrtoint ptr [[P]] to i32
+; CHECK-32-MASK-NEXT: [[MASKED:%[0-9]+]] = and i32 [[ADDR]], 268435455
+; CHECK-32-MASK-NEXT: [[DELTA:%[0-9]+]] = sub i32 [[MASKED]], 65536
+; CHECK-32-MASK-NEXT: [[SHIFTED:%[0-9]+]] = ashr i32 [[DELTA]], 3
+; CHECK-32-MASK-NEXT: [[SHADOW_ADDR:%[0-9]+]] = add i32 536887296, [[SHIFTED]]
+
+; CHECK-32-NOMASK-NEXT: [[ADDR:%[0-9]+]] = ptrtoint ptr [[P]] to i32
+; CHECK-32-NOMASK-NEXT: [[DELTA:%[0-9]+]] = sub i32 [[ADDR]], 536936448
+; CHECK-32-NOMASK-NEXT: [[SHIFTED:%[0-9]+]] = ashr i32 [[DELTA]], 3
+; CHECK-32-NOMASK-NEXT: [[SHADOW_ADDR:%[0-9]+]] = add i32 536887296, [[SHIFTED]]
+
+; --- 64-bit layout ---
+; Memory Pivot: 0x1000000000010000 (masked with 0x0000FFFFFFFFFFFF -> 0x00010000 = 65536)
+; Shadow Base: 0x2000000000004000 (decimal 2305843009213710336)
+; Region Mask (top 16 bits stripped): 0x0000FFFFFFFFFFFF (decimal 281474976710655)
+; CHECK-64-MASK-NEXT: [[ADDR:%[0-9]+]] = ptrtoint ptr [[P]] to i64
+; CHECK-64-MASK-NEXT: [[MASKED:%[0-9]+]] = and i64 [[ADDR]], 281474976710655
+; CHECK-64-MASK-NEXT: [[DELTA:%[0-9]+]] = sub i64 [[MASKED]], 65536
+; CHECK-64-MASK-NEXT: [[SHIFTED:%[0-9]+]] = ashr i64 [[DELTA]], 3
+; CHECK-64-MASK-NEXT: [[SHADOW_ADDR:%[0-9]+]] = add i64 2305843009213710336, [[SHIFTED]]
+
+; CHECK-NEXT: [[SHADOW_PTR:%[0-9]+]] = inttoptr {{i(64|32)}} [[SHADOW_ADDR]] to ptr
+; CHECK-NEXT: [[SHADOW_BYTE:%[0-9]+]] = load i8, ptr [[SHADOW_PTR]], align 1
+; CHECK-NEXT: [[IS_POISONED:%[0-9]+]] = icmp ne i8 [[SHADOW_BYTE]], 0
+; CHECK-NEXT: br i1 [[IS_POISONED]], label %[[L_REPORT:[0-9]+]], label %[[L_STORE:[0-9]+]]
+
+ store i32 0, ptr %p, align 4
+ ret void
+}
More information about the llvm-commits
mailing list