[clang] [compiler-rt] [llvm] [FlexFat] Modern Port of LowFat for Lean C/C++ Bounds Checking (PR #188893)
Kenneth Gao via llvm-commits
llvm-commits at lists.llvm.org
Thu Mar 26 19:00:08 PDT 2026
https://github.com/duckyfuz created https://github.com/llvm/llvm-project/pull/188893
None
>From d92871e642d01eeece130221150770a6cb887250 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 20 Jan 2026 21:03:44 +0800
Subject: [PATCH 01/55] [LowFat] Add LowFatSanitizer pass skeleton with debug
logging
---
.../Instrumentation/LowFatSanitizer.h | 30 +++++++++++++++++++
llvm/lib/Passes/PassBuilder.cpp | 11 +++++++
llvm/lib/Passes/PassRegistry.def | 4 +++
.../Transforms/Instrumentation/CMakeLists.txt | 1 +
.../Instrumentation/LowFatSanitizer.cpp | 25 ++++++++++++++++
5 files changed, 71 insertions(+)
create mode 100644 llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
create mode 100644 llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
diff --git a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
new file mode 100644
index 0000000000000..b5688505cb799
--- /dev/null
+++ b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
@@ -0,0 +1,30 @@
+//===- LowFatSanitizer.h - LowFat Pointer Bounds Checking -------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+#ifndef LLVM_TRANSFORMS_INSTRUMENTATION_LOWFATSANITIZER_H
+#define LLVM_TRANSFORMS_INSTRUMENTATION_LOWFATSANITIZER_H
+
+#include "llvm/IR/PassManager.h"
+
+namespace llvm {
+class Module;
+
+struct LowFatSanitizerOptions {
+ // LowFat currently does not support any options.
+};
+
+class LowFatSanitizerPass : public PassInfoMixin<LowFatSanitizerPass> {
+public:
+ LLVM_ABI
+ LowFatSanitizerPass(const LowFatSanitizerOptions &Options);
+ LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
+ static bool isRequired() { return true; }
+};
+
+} // namespace llvm
+
+#endif // LLVM_TRANSFORMS_INSTRUMENTATION_LOWFATSANITIZER_H
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index 8bb78c8c7df63..b57112a71ddb0 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -247,6 +247,7 @@
#include "llvm/Transforms/Instrumentation/DataFlowSanitizer.h"
#include "llvm/Transforms/Instrumentation/GCOVProfiler.h"
#include "llvm/Transforms/Instrumentation/HWAddressSanitizer.h"
+#include "llvm/Transforms/Instrumentation/LowFatSanitizer.h"
#include "llvm/Transforms/Instrumentation/InstrProfiling.h"
#include "llvm/Transforms/Instrumentation/KCFI.h"
#include "llvm/Transforms/Instrumentation/LowerAllowCheckPass.h"
@@ -978,6 +979,16 @@ Expected<HWAddressSanitizerOptions> parseHWASanPassOptions(StringRef Params) {
return Result;
}
+Expected<LowFatSanitizerOptions> parseLowFatPassOptions(StringRef Params) {
+ LowFatSanitizerOptions Result;
+ if (!Params.empty()) {
+ return make_error<StringError>(
+ formatv("invalid LowFatSanitizer pass parameter '{}'", Params).str(),
+ inconvertibleErrorCode());
+ }
+ return Result;
+}
+
Expected<EmbedBitcodeOptions> parseEmbedBitcodePassOptions(StringRef Params) {
EmbedBitcodeOptions Result;
while (!Params.empty()) {
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index 2cfb5b2592601..791dfbce17165 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -216,6 +216,10 @@ MODULE_PASS_WITH_PARAMS(
"hwasan", "HWAddressSanitizerPass",
[](HWAddressSanitizerOptions Opts) { return HWAddressSanitizerPass(Opts); },
parseHWASanPassOptions, "kernel;recover")
+MODULE_PASS_WITH_PARAMS(
+ "lowfat", "LowFatSanitizerPass",
+ [](LowFatSanitizerOptions Opts) { return LowFatSanitizerPass(Opts); },
+ parseLowFatPassOptions, "")
MODULE_PASS_WITH_PARAMS(
"internalize", "InternalizePass",
[](SmallVector<std::string, 0> PreservedGVs) {
diff --git a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
index 80576c61fd80c..36fef5c50ddd0 100644
--- a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
+++ b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
@@ -14,6 +14,7 @@ add_llvm_component_library(LLVMInstrumentation
IndirectCallPromotion.cpp
InstrProfiling.cpp
KCFI.cpp
+ LowFatSanitizer.cpp
LowerAllowCheckPass.cpp
PGOCtxProfFlattening.cpp
PGOCtxProfLowering.cpp
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
new file mode 100644
index 0000000000000..913c4d33d37b6
--- /dev/null
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -0,0 +1,25 @@
+//===- LowFatSanitizer.cpp - LowFat Pointer Bounds Checking ---------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Transforms/Instrumentation/LowFatSanitizer.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/Module.h"
+#include "llvm/Support/Debug.h"
+
+using namespace llvm;
+
+#define DEBUG_TYPE "lowfat"
+
+LowFatSanitizerPass::LowFatSanitizerPass(const LowFatSanitizerOptions &Options) {}
+
+PreservedAnalyses LowFatSanitizerPass::run(Module &M,
+ ModuleAnalysisManager &AM) {
+ LLVM_DEBUG(dbgs() << "[LowFat] Running on module: " << M.getName() << "\n");
+
+ return PreservedAnalyses::all();
+}
>From bc1546a58a0b45192e22662276730fc8fa8baf79 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 21 Jan 2026 23:25:30 +0800
Subject: [PATCH 02/55] [LowFat] Initialize LowFatSanitizerPass with empty
options
---
llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h | 3 +++
llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp | 3 ++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
index b5688505cb799..9a39b532a296d 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
@@ -23,6 +23,9 @@ class LowFatSanitizerPass : public PassInfoMixin<LowFatSanitizerPass> {
LowFatSanitizerPass(const LowFatSanitizerOptions &Options);
LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
static bool isRequired() { return true; }
+
+private:
+ LowFatSanitizerOptions Options;
};
} // namespace llvm
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 913c4d33d37b6..b31ae562a5f51 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -15,7 +15,8 @@ using namespace llvm;
#define DEBUG_TYPE "lowfat"
-LowFatSanitizerPass::LowFatSanitizerPass(const LowFatSanitizerOptions &Options) {}
+LowFatSanitizerPass::LowFatSanitizerPass(const LowFatSanitizerOptions &Options)
+ : Options(Options) {}
PreservedAnalyses LowFatSanitizerPass::run(Module &M,
ModuleAnalysisManager &AM) {
>From 5a3e08e48161be63d027d894197880ea52b7ef10 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 21 Jan 2026 23:41:48 +0800
Subject: [PATCH 03/55] [LowFat] Create stub runtime lib
---
compiler-rt/lib/lowfat/CMakeLists.txt | 32 ++++++++
compiler-rt/lib/lowfat/lf_interface.h | 48 +++++++++++
compiler-rt/lib/lowfat/lf_rtl.cpp | 109 +++++++++++++++++++++++++
compiler-rt/test/lowfat/CMakeLists.txt | 11 +++
4 files changed, 200 insertions(+)
create mode 100644 compiler-rt/lib/lowfat/CMakeLists.txt
create mode 100644 compiler-rt/lib/lowfat/lf_interface.h
create mode 100644 compiler-rt/lib/lowfat/lf_rtl.cpp
create mode 100644 compiler-rt/test/lowfat/CMakeLists.txt
diff --git a/compiler-rt/lib/lowfat/CMakeLists.txt b/compiler-rt/lib/lowfat/CMakeLists.txt
new file mode 100644
index 0000000000000..398e343d82c6d
--- /dev/null
+++ b/compiler-rt/lib/lowfat/CMakeLists.txt
@@ -0,0 +1,32 @@
+include_directories(..)
+
+set(LOWFAT_SOURCES
+ lf_rtl.cpp
+)
+
+set(LOWFAT_HEADERS
+ lf_interface.h
+)
+
+set(LOWFAT_CFLAGS ${SANITIZER_COMMON_CFLAGS})
+append_rtti_flag(OFF LOWFAT_CFLAGS)
+set(LOWFAT_COMMON_DEFINITIONS)
+
+# Static runtime library.
+add_compiler_rt_component(lowfat)
+
+if(COMPILER_RT_HAS_LOWFAT)
+ foreach(arch ${LOWFAT_SUPPORTED_ARCH})
+ add_compiler_rt_runtime(clang_rt.lowfat
+ STATIC
+ ARCHS ${arch}
+ SOURCES ${LOWFAT_SOURCES}
+ ADDITIONAL_HEADERS ${LOWFAT_HEADERS}
+ OBJECT_LIBS RTSanitizerCommon
+ RTSanitizerCommonLibc
+ RTSanitizerCommonSymbolizer
+ CFLAGS ${LOWFAT_CFLAGS}
+ DEFS ${LOWFAT_COMMON_DEFINITIONS}
+ PARENT_TARGET lowfat)
+ endforeach()
+endif()
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
new file mode 100644
index 0000000000000..7e6e90c9cf0e0
--- /dev/null
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -0,0 +1,48 @@
+//===-- lf_interface.h - LowFat Sanitizer Runtime Interface ----*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file declares the LowFat Sanitizer runtime interface functions.
+// The runtime library must define these functions so the instrumented program
+// can call them.
+//
+//===----------------------------------------------------------------------===//
+#ifndef LF_INTERFACE_H
+#define LF_INTERFACE_H
+
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+using __sanitizer::uptr;
+
+extern "C" {
+
+// Initialize the LowFat Sanitizer runtime. Called early during program startup.
+SANITIZER_INTERFACE_ATTRIBUTE void __lf_init();
+
+// Perform a bounds check on a pointer access.
+// ptr: The pointer being accessed
+// size: The size of the access in bytes
+SANITIZER_INTERFACE_ATTRIBUTE void __lf_check_bounds(uptr ptr, uptr size);
+
+// Report an out-of-bounds error.
+// ptr: The pointer that caused the violation
+// base: The base address of the allocation
+// bound: The size of the allocation
+SANITIZER_INTERFACE_ATTRIBUTE void __lf_report_oob(uptr ptr, uptr base,
+ uptr bound);
+
+// Get the base address of an allocation from a pointer.
+// Returns the base address, or 0 if the pointer is not within a LowFat region.
+SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_base(uptr ptr);
+
+// Get the size (bound) of an allocation from a pointer.
+// Returns the allocation size, or 0 if the pointer is not within a LowFat region.
+SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_size(uptr ptr);
+
+} // extern "C"
+
+#endif // LF_INTERFACE_H
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
new file mode 100644
index 0000000000000..66cbbaa9ad2ee
--- /dev/null
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -0,0 +1,109 @@
+//===-- lf_rtl.cpp - LowFat Sanitizer Runtime Library ---------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file is the main file of the LowFat Sanitizer runtime library.
+//
+// LowFat pointers encode allocation bounds information directly in the pointer
+// value through careful memory layout. This allows O(1) bounds checking without
+// maintaining separate metadata.
+//
+//===----------------------------------------------------------------------===//
+
+#include "lf_interface.h"
+#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_flags.h"
+#include "sanitizer_common/sanitizer_stacktrace.h"
+
+using namespace __sanitizer;
+
+namespace __lowfat {
+
+// Flag to track initialization state
+static bool lowfat_inited = false;
+
+// TODO: Add LowFat-specific configuration tables
+// These will define the memory regions and size classes for LowFat allocations
+
+static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound) {
+ Printf("=================================================================\n");
+ Printf("==ERROR: LowFat: out-of-bounds access detected\n");
+ Printf(" pointer: 0x%zx\n", ptr);
+ Printf(" base: 0x%zx\n", base);
+ Printf(" bound: %zu bytes\n", bound);
+ Printf("=================================================================\n");
+
+ // Print stack trace
+ BufferedStackTrace stack;
+ stack.Unwind(StackTrace::GetCurrentPc(), GET_CURRENT_FRAME(), nullptr,
+ common_flags()->fast_unwind_on_fatal);
+ stack.Print();
+
+ Die();
+}
+
+} // namespace __lowfat
+
+// ---------------------- Interface Functions ----------------------
+
+extern "C" {
+
+SANITIZER_INTERFACE_ATTRIBUTE
+void __lf_init() {
+ if (__lowfat::lowfat_inited)
+ return;
+
+ // Initialize sanitizer common
+ // InitializeCommonFlags() would go here if we had custom flags
+
+ Printf("LowFat Sanitizer: runtime initialized\n");
+
+ __lowfat::lowfat_inited = true;
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+void __lf_check_bounds(uptr ptr, uptr size) {
+ // TODO: Implement actual bounds checking
+ // For now, this is a stub that does nothing
+ //
+ // The full implementation should:
+ // 1. Extract the region index from the pointer's high bits
+ // 2. Look up the allocation size for that region
+ // 3. Compute the base address using the region's alignment
+ // 4. Check if ptr + size <= base + allocation_size
+ (void)ptr;
+ (void)size;
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+void __lf_report_oob(uptr ptr, uptr base, uptr bound) {
+ __lowfat::PrintErrorAndDie(ptr, base, bound);
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+uptr __lf_get_base(uptr ptr) {
+ // TODO: Implement base address extraction
+ // This will use the LowFat memory layout to compute the base
+ (void)ptr;
+ return 0;
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+uptr __lf_get_size(uptr ptr) {
+ // TODO: Implement size extraction
+ // This will look up the size class from the pointer's region
+ (void)ptr;
+ return 0;
+}
+
+} // extern "C"
+
+// Ensure initialization runs early via .preinit_array on ELF platforms
+#if SANITIZER_CAN_USE_PREINIT_ARRAY
+__attribute__((section(".preinit_array"), used)) static auto preinit =
+ __lf_init;
+#endif
\ No newline at end of file
diff --git a/compiler-rt/test/lowfat/CMakeLists.txt b/compiler-rt/test/lowfat/CMakeLists.txt
new file mode 100644
index 0000000000000..651b543e51a02
--- /dev/null
+++ b/compiler-rt/test/lowfat/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(LOWFAT_LIT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
+set(LOWFAT_LIT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR})
+
+set(LOWFAT_TESTSUITES)
+set(LOWFAT_TEST_DEPS ${SANITIZER_COMMON_LIT_TEST_DEPS})
+
+if(COMPILER_RT_HAS_LOWFAT)
+ list(APPEND LOWFAT_TEST_DEPS lowfat)
+endif()
+
+# TODO: add tests for lowfat
>From 47a57ccf0df9cb3be3918a52c4544967e2fe28cb Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 21 Jan 2026 23:45:51 +0800
Subject: [PATCH 04/55] [LowFat] Add lowfat to clang and allow link with LowFat
runtime library
---
clang/include/clang/Basic/Sanitizers.def | 3 +++
clang/lib/CodeGen/BackendUtil.cpp | 6 ++++++
clang/lib/Driver/SanitizerArgs.cpp | 3 +++
clang/lib/Driver/ToolChains/Darwin.cpp | 3 +++
clang/lib/Driver/ToolChains/Linux.cpp | 2 ++
.../cmake/Modules/AllSupportedArchDefs.cmake | 1 +
compiler-rt/cmake/config-ix.cmake | 13 ++++++++++++-
7 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/clang/include/clang/Basic/Sanitizers.def b/clang/include/clang/Basic/Sanitizers.def
index da85431625026..ab7da16c6b68f 100644
--- a/clang/include/clang/Basic/Sanitizers.def
+++ b/clang/include/clang/Basic/Sanitizers.def
@@ -142,6 +142,9 @@ SANITIZER("kcfi", KCFI)
// Safe Stack
SANITIZER("safe-stack", SafeStack)
+// LowFat Pointer Bounds Checking
+SANITIZER("lowfat", LowFat)
+
// Shadow Call Stack
SANITIZER("shadow-call-stack", ShadowCallStack)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index d411ef1bf8763..55025c43448f2 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -85,6 +85,7 @@
#include "llvm/Transforms/Instrumentation/SanitizerCoverage.h"
#include "llvm/Transforms/Instrumentation/ThreadSanitizer.h"
#include "llvm/Transforms/Instrumentation/TypeSanitizer.h"
+#include "llvm/Transforms/Instrumentation/LowFatSanitizer.h"
#include "llvm/Transforms/ObjCARC.h"
#include "llvm/Transforms/Scalar/EarlyCSE.h"
#include "llvm/Transforms/Scalar/GVN.h"
@@ -768,6 +769,11 @@ static void addSanitizers(const Triple &TargetTriple,
MPM.addPass(DataFlowSanitizerPass(LangOpts.NoSanitizeFiles,
PB.getVirtualFileSystemPtr()));
}
+
+ if (LangOpts.Sanitize.has(SanitizerKind::LowFat)) {
+ LowFatSanitizerOptions Opts;
+ MPM.addPass(LowFatSanitizerPass(Opts));
+ }
};
if (ClSanitizeOnOptimizerEarlyEP) {
PB.registerOptimizerEarlyEPCallback(
diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp
index be068b2381d06..8d59955b29515 100644
--- a/clang/lib/Driver/SanitizerArgs.cpp
+++ b/clang/lib/Driver/SanitizerArgs.cpp
@@ -663,6 +663,9 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
SanitizerKind::Address | SanitizerKind::HWAddress |
SanitizerKind::KernelAddress |
SanitizerKind::KernelHWAddress |
+ SanitizerKind::Memory),
+ std::make_pair(SanitizerKind::LowFat,
+ SanitizerKind::Address | SanitizerKind::HWAddress |
SanitizerKind::Memory)};
// Enable toolchain specific default sanitizers if not explicitly disabled.
diff --git a/clang/lib/Driver/ToolChains/Darwin.cpp b/clang/lib/Driver/ToolChains/Darwin.cpp
index fb75739360328..c4239c3cedd9b 100644
--- a/clang/lib/Driver/ToolChains/Darwin.cpp
+++ b/clang/lib/Driver/ToolChains/Darwin.cpp
@@ -3815,6 +3815,9 @@ SanitizerMask Darwin::getSupportedSanitizers() const {
if (IsX86_64)
Res |= SanitizerKind::NumericalStability;
+ if (IsX86_64 || IsAArch64)
+ Res |= SanitizerKind::LowFat;
+
return Res;
}
diff --git a/clang/lib/Driver/ToolChains/Linux.cpp b/clang/lib/Driver/ToolChains/Linux.cpp
index cdbf21fb90263..39c9868e07d3f 100644
--- a/clang/lib/Driver/ToolChains/Linux.cpp
+++ b/clang/lib/Driver/ToolChains/Linux.cpp
@@ -937,6 +937,8 @@ SanitizerMask Linux::getSupportedSanitizers() const {
}
if (IsX86_64)
Res |= SanitizerKind::NumericalStability;
+ if (IsX86_64 || IsAArch64)
+ Res |= SanitizerKind::LowFat;
if (!IsAndroid)
Res |= SanitizerKind::Memory;
diff --git a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
index 0cae5da26c3e7..0787e2fa61bfb 100644
--- a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
+++ b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
@@ -121,6 +121,7 @@ set(ALL_XRAY_SUPPORTED_ARCH ${X86_64} ${ARM32} ${ARM64} ${MIPS32} ${MIPS64}
endif()
set(ALL_XRAY_DSO_SUPPORTED_ARCH ${X86_64} ${ARM64})
set(ALL_SHADOWCALLSTACK_SUPPORTED_ARCH ${ARM64})
+set(ALL_LOWFAT_SUPPORTED_ARCH ${X86_64} ${ARM64})
if (UNIX)
if (OS_NAME MATCHES "Linux")
diff --git a/compiler-rt/cmake/config-ix.cmake b/compiler-rt/cmake/config-ix.cmake
index 1f82ff3cf7531..90b12c7f84d0e 100644
--- a/compiler-rt/cmake/config-ix.cmake
+++ b/compiler-rt/cmake/config-ix.cmake
@@ -689,6 +689,9 @@ if(APPLE)
list_intersect(SHADOWCALLSTACK_SUPPORTED_ARCH
ALL_SHADOWCALLSTACK_SUPPORTED_ARCH
SANITIZER_COMMON_SUPPORTED_ARCH)
+ list_intersect(LOWFAT_SUPPORTED_ARCH
+ ALL_LOWFAT_SUPPORTED_ARCH
+ SANITIZER_COMMON_SUPPORTED_ARCH)
list_intersect(ORC_SUPPORTED_ARCH
ALL_ORC_SUPPORTED_ARCH
SANITIZER_COMMON_SUPPORTED_ARCH)
@@ -724,6 +727,7 @@ else()
filter_available_targets(XRAY_DSO_SUPPORTED_ARCH ${ALL_XRAY_DSO_SUPPORTED_ARCH})
filter_available_targets(SHADOWCALLSTACK_SUPPORTED_ARCH
${ALL_SHADOWCALLSTACK_SUPPORTED_ARCH})
+ filter_available_targets(LOWFAT_SUPPORTED_ARCH ${ALL_LOWFAT_SUPPORTED_ARCH})
filter_available_targets(GWP_ASAN_SUPPORTED_ARCH ${ALL_GWP_ASAN_SUPPORTED_ARCH})
filter_available_targets(NSAN_SUPPORTED_ARCH ${ALL_NSAN_SUPPORTED_ARCH})
filter_available_targets(ORC_SUPPORTED_ARCH ${ALL_ORC_SUPPORTED_ARCH})
@@ -760,7 +764,7 @@ if(COMPILER_RT_SUPPORTED_ARCH)
endif()
message(STATUS "Compiler-RT supported architectures: ${COMPILER_RT_SUPPORTED_ARCH}")
-set(ALL_SANITIZERS asan;rtsan;dfsan;msan;hwasan;tsan;tysan;safestack;cfi;scudo_standalone;ubsan_minimal;gwp_asan;nsan;asan_abi)
+set(ALL_SANITIZERS asan;rtsan;dfsan;msan;hwasan;tsan;tysan;safestack;cfi;scudo_standalone;ubsan_minimal;gwp_asan;nsan;asan_abi;lowfat)
set(COMPILER_RT_SANITIZERS_TO_BUILD all CACHE STRING
"sanitizers to build if supported on the target (all;${ALL_SANITIZERS})")
list_replace(COMPILER_RT_SANITIZERS_TO_BUILD all "${ALL_SANITIZERS}")
@@ -902,6 +906,13 @@ else()
set(COMPILER_RT_HAS_SAFESTACK FALSE)
endif()
+if (COMPILER_RT_HAS_SANITIZER_COMMON AND LOWFAT_SUPPORTED_ARCH AND
+ OS_NAME MATCHES "Darwin|Linux")
+ set(COMPILER_RT_HAS_LOWFAT TRUE)
+else()
+ set(COMPILER_RT_HAS_LOWFAT FALSE)
+endif()
+
if (COMPILER_RT_HAS_SANITIZER_COMMON AND CFI_SUPPORTED_ARCH)
set(COMPILER_RT_HAS_CFI TRUE)
else()
>From 513eda0a777d3c2ca60720427b1027b15b99f4d5 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 21 Jan 2026 23:58:05 +0800
Subject: [PATCH 05/55] [LowFat] Add -fsanitize=lowfat linker support for Clang
---
clang/include/clang/Driver/SanitizerArgs.h | 1 +
clang/lib/Driver/ToolChains/CommonArgs.cpp | 2 ++
2 files changed, 3 insertions(+)
diff --git a/clang/include/clang/Driver/SanitizerArgs.h b/clang/include/clang/Driver/SanitizerArgs.h
index 84fb66e16bee3..82af822d55171 100644
--- a/clang/include/clang/Driver/SanitizerArgs.h
+++ b/clang/include/clang/Driver/SanitizerArgs.h
@@ -119,6 +119,7 @@ class SanitizerArgs {
return Sanitizers.has(SanitizerKind::NumericalStability);
}
bool needsRtsanRt() const { return Sanitizers.has(SanitizerKind::Realtime); }
+ bool needsLowFatRt() const { return Sanitizers.has(SanitizerKind::LowFat); }
bool hasMemTag() const {
return hasMemtagHeap() || hasMemtagStack() || hasMemtagGlobals();
diff --git a/clang/lib/Driver/ToolChains/CommonArgs.cpp b/clang/lib/Driver/ToolChains/CommonArgs.cpp
index 10a1a412eea08..d20108de6a309 100644
--- a/clang/lib/Driver/ToolChains/CommonArgs.cpp
+++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp
@@ -1718,6 +1718,8 @@ collectSanitizerRuntimes(const ToolChain &TC, const ArgList &Args,
if (SanArgs.linkCXXRuntimes())
StaticRuntimes.push_back("scudo_standalone_cxx");
}
+ if (SanArgs.needsLowFatRt())
+ StaticRuntimes.push_back("lowfat");
}
// Should be called before we add system libraries (C++ ABI, libstdc++/libc++,
>From 2b585b118db66767ecbc40613574fdd3379c3d37 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 31 Jan 2026 23:35:10 +0800
Subject: [PATCH 06/55] [LowFat] Add sanitizer infrastructure
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 18 +-
.../Instrumentation/LowFatSanitizer.cpp | 172 +++++++++++++++++-
2 files changed, 187 insertions(+), 3 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 66cbbaa9ad2ee..fb5b60427f041 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -48,6 +48,17 @@ static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound) {
} // namespace __lowfat
+namespace __sanitizer {
+void BufferedStackTrace::UnwindImpl(uptr pc, uptr bp, void *context,
+ bool request_fast, u32 max_depth) {
+ uptr top = 0;
+ uptr bottom = 0;
+ GetThreadStackTopAndBottom(false, &top, &bottom);
+ bool fast = StackTrace::WillUseFastUnwind(request_fast);
+ Unwind(max_depth, pc, bp, context, top, bottom, fast);
+}
+} // namespace __sanitizer
+
// ---------------------- Interface Functions ----------------------
extern "C" {
@@ -102,8 +113,13 @@ uptr __lf_get_size(uptr ptr) {
} // extern "C"
-// Ensure initialization runs early via .preinit_array on ELF platforms
#if SANITIZER_CAN_USE_PREINIT_ARRAY
+// ELF platforms: use .preinit_array for earliest possible initialization
__attribute__((section(".preinit_array"), used)) static auto preinit =
__lf_init;
+#else
+// macOS/other platforms: use constructor attribute
+__attribute__((constructor)) static void lowfat_constructor() {
+ __lf_init();
+}
#endif
\ No newline at end of file
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index b31ae562a5f51..0b57458fff0d2 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -5,22 +5,190 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
+//
+// This file implements the LowFat Sanitizer instrumentation pass.
+//
+// LowFat pointers encode allocation bounds information directly in the pointer
+// value through careful memory layout. This pass instruments memory accesses
+// to call runtime functions that verify bounds using this encoded information.
+//
+//===----------------------------------------------------------------------===//
#include "llvm/Transforms/Instrumentation/LowFatSanitizer.h"
+#include "llvm/ADT/Statistic.h"
+#include "llvm/IR/DataLayout.h"
#include "llvm/IR/Function.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/InstIterator.h"
+#include "llvm/IR/Instructions.h"
#include "llvm/IR/Module.h"
+#include "llvm/IR/Type.h"
#include "llvm/Support/Debug.h"
using namespace llvm;
#define DEBUG_TYPE "lowfat"
+STATISTIC(NumInstrumentedLoads, "Number of loads instrumented");
+STATISTIC(NumInstrumentedStores, "Number of stores instrumented");
+STATISTIC(NumInstrumentedAtomics, "Number of atomic operations instrumented");
+
+namespace {
+
+/// Helper class to instrument a module with LowFat bounds checks.
+class LowFatSanitizer {
+public:
+ LowFatSanitizer(Module &M, const LowFatSanitizerOptions &Options)
+ : M(M), Options(Options), DL(M.getDataLayout()),
+ IntptrTy(DL.getIntPtrType(M.getContext())) {}
+
+ bool run();
+
+private:
+ /// Get or create the declaration for __lf_check_bounds.
+ FunctionCallee getCheckBoundsFn();
+
+ /// Instrument a single function.
+ bool instrumentFunction(Function &F);
+
+ /// Instrument a memory access instruction.
+ /// Returns true if instrumentation was inserted.
+ bool instrumentMemoryAccess(Instruction *I, Value *Ptr, Type *AccessTy);
+
+ Module &M;
+ const LowFatSanitizerOptions &Options;
+ const DataLayout &DL;
+ Type *IntptrTy;
+ FunctionCallee CheckBoundsFn;
+};
+
+FunctionCallee LowFatSanitizer::getCheckBoundsFn() {
+ if (!CheckBoundsFn) {
+ // void __lf_check_bounds(uptr ptr, uptr size)
+ Type *VoidTy = Type::getVoidTy(M.getContext());
+ CheckBoundsFn = M.getOrInsertFunction(
+ "__lf_check_bounds", FunctionType::get(VoidTy, {IntptrTy, IntptrTy}, false));
+ }
+ return CheckBoundsFn;
+}
+
+bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
+ Type *AccessTy) {
+ // Skip if the access type size is not known at compile time
+ TypeSize AccessSize = DL.getTypeStoreSize(AccessTy);
+ if (AccessSize.isScalable()) {
+ LLVM_DEBUG(dbgs() << "[LowFat] Skipping scalable type access\n");
+ return false;
+ }
+
+ IRBuilder<> IRB(I);
+
+ // Convert pointer to integer
+ Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
+
+ // Create size constant
+ Value *SizeVal = ConstantInt::get(IntptrTy, AccessSize.getFixedValue());
+
+ // Insert call to __lf_check_bounds(ptr, size)
+ IRB.CreateCall(getCheckBoundsFn(), {PtrInt, SizeVal});
+
+ LLVM_DEBUG(dbgs() << "[LowFat] Instrumented: " << *I << "\n");
+ return true;
+}
+
+bool LowFatSanitizer::instrumentFunction(Function &F) {
+ // Skip functions that shouldn't be instrumented
+ if (F.isDeclaration())
+ return false;
+
+ // Skip the runtime library functions themselves
+ if (F.getName().starts_with("__lf_"))
+ return false;
+
+ // Skip functions with nosanitize attribute
+ if (F.hasFnAttribute(Attribute::NoSanitizeBounds))
+ return false;
+
+ LLVM_DEBUG(dbgs() << "[LowFat] Instrumenting function: " << F.getName() << "\n");
+
+ bool Modified = false;
+
+ // Collect instructions to instrument first to avoid iterator invalidation
+ SmallVector<std::pair<Instruction *, std::pair<Value *, Type *>>, 16> ToInstrument;
+
+ for (Instruction &I : instructions(F)) {
+ Value *Ptr = nullptr;
+ Type *AccessTy = nullptr;
+
+ if (auto *LI = dyn_cast<LoadInst>(&I)) {
+ if (!LI->isVolatile()) {
+ Ptr = LI->getPointerOperand();
+ AccessTy = LI->getType();
+ }
+ } else if (auto *SI = dyn_cast<StoreInst>(&I)) {
+ if (!SI->isVolatile()) {
+ Ptr = SI->getPointerOperand();
+ AccessTy = SI->getValueOperand()->getType();
+ }
+ } else if (auto *AI = dyn_cast<AtomicRMWInst>(&I)) {
+ if (!AI->isVolatile()) {
+ Ptr = AI->getPointerOperand();
+ AccessTy = AI->getValOperand()->getType();
+ }
+ } else if (auto *AI = dyn_cast<AtomicCmpXchgInst>(&I)) {
+ if (!AI->isVolatile()) {
+ Ptr = AI->getPointerOperand();
+ AccessTy = AI->getCompareOperand()->getType();
+ }
+ }
+
+ if (Ptr && AccessTy) {
+ ToInstrument.push_back({&I, {Ptr, AccessTy}});
+ }
+ }
+
+ // Now instrument collected instructions
+ for (auto &Entry : ToInstrument) {
+ Instruction *I = Entry.first;
+ Value *Ptr = Entry.second.first;
+ Type *AccessTy = Entry.second.second;
+
+ if (instrumentMemoryAccess(I, Ptr, AccessTy)) {
+ Modified = true;
+ if (isa<LoadInst>(I))
+ ++NumInstrumentedLoads;
+ else if (isa<StoreInst>(I))
+ ++NumInstrumentedStores;
+ else
+ ++NumInstrumentedAtomics;
+ }
+ }
+
+ return Modified;
+}
+
+bool LowFatSanitizer::run() {
+ LLVM_DEBUG(dbgs() << "[LowFat] Running on module: " << M.getName() << "\n");
+
+ bool Modified = false;
+
+ for (Function &F : M) {
+ Modified |= instrumentFunction(F);
+ }
+
+ return Modified;
+}
+
+} // anonymous namespace
+
LowFatSanitizerPass::LowFatSanitizerPass(const LowFatSanitizerOptions &Options)
: Options(Options) {}
PreservedAnalyses LowFatSanitizerPass::run(Module &M,
ModuleAnalysisManager &AM) {
- LLVM_DEBUG(dbgs() << "[LowFat] Running on module: " << M.getName() << "\n");
+ LowFatSanitizer Sanitizer(M, Options);
+ if (!Sanitizer.run())
+ return PreservedAnalyses::all();
- return PreservedAnalyses::all();
+ return PreservedAnalyses::none();
}
>From 19b978436a6341351a302e08523b137370f42c07 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 31 Jan 2026 23:39:58 +0800
Subject: [PATCH 07/55] [LowFat] Resolve linker issues on Darwin
---
clang/lib/Driver/ToolChains/Darwin.cpp | 3 +++
compiler-rt/lib/lowfat/CMakeLists.txt | 23 ++++++++++++++++++++---
2 files changed, 23 insertions(+), 3 deletions(-)
diff --git a/clang/lib/Driver/ToolChains/Darwin.cpp b/clang/lib/Driver/ToolChains/Darwin.cpp
index c4239c3cedd9b..abf3d67ada664 100644
--- a/clang/lib/Driver/ToolChains/Darwin.cpp
+++ b/clang/lib/Driver/ToolChains/Darwin.cpp
@@ -1621,6 +1621,9 @@ void DarwinClang::AddLinkRuntimeLibArgs(const ArgList &Args,
AddLinkRuntimeLib(Args, CmdArgs, "stats_client", RLO_AlwaysLink);
AddLinkSanitizerLibArgs(Args, CmdArgs, "stats");
}
+ if (Sanitize.needsLowFatRt()) {
+ AddLinkSanitizerLibArgs(Args, CmdArgs, "lowfat");
+ }
}
if (Sanitize.needsMemProfRt())
diff --git a/compiler-rt/lib/lowfat/CMakeLists.txt b/compiler-rt/lib/lowfat/CMakeLists.txt
index 398e343d82c6d..905bd66b41b88 100644
--- a/compiler-rt/lib/lowfat/CMakeLists.txt
+++ b/compiler-rt/lib/lowfat/CMakeLists.txt
@@ -16,17 +16,34 @@ set(LOWFAT_COMMON_DEFINITIONS)
add_compiler_rt_component(lowfat)
if(COMPILER_RT_HAS_LOWFAT)
- foreach(arch ${LOWFAT_SUPPORTED_ARCH})
+ if(APPLE)
+ add_weak_symbols("sanitizer_common" WEAK_SYMBOL_LINK_FLAGS)
+
+ add_compiler_rt_runtime(clang_rt.lowfat
+ SHARED
+ OS ${SANITIZER_COMMON_SUPPORTED_OS}
+ ARCHS ${LOWFAT_SUPPORTED_ARCH}
+ SOURCES ${LOWFAT_SOURCES}
+ ADDITIONAL_HEADERS ${LOWFAT_HEADERS}
+ OBJECT_LIBS RTSanitizerCommon
+ RTSanitizerCommonLibc
+ RTSanitizerCommonSymbolizer
+ CFLAGS ${LOWFAT_CFLAGS}
+ LINK_FLAGS ${WEAK_SYMBOL_LINK_FLAGS}
+ DEFS ${LOWFAT_COMMON_DEFINITIONS}
+ PARENT_TARGET lowfat)
+ else()
add_compiler_rt_runtime(clang_rt.lowfat
STATIC
- ARCHS ${arch}
+ ARCHS ${LOWFAT_SUPPORTED_ARCH}
SOURCES ${LOWFAT_SOURCES}
ADDITIONAL_HEADERS ${LOWFAT_HEADERS}
OBJECT_LIBS RTSanitizerCommon
RTSanitizerCommonLibc
RTSanitizerCommonSymbolizer
+ RTSanitizerCommonSymbolizerInternal
CFLAGS ${LOWFAT_CFLAGS}
DEFS ${LOWFAT_COMMON_DEFINITIONS}
PARENT_TARGET lowfat)
- endforeach()
+ endif()
endif()
>From f61ee705c0f61da5d959a0b32363654fbceba584 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 10 Feb 2026 15:52:15 +0800
Subject: [PATCH 08/55] [LowFat] Implement memory regions and bound check
functions
---
compiler-rt/lib/lowfat/CMakeLists.txt | 1 +
compiler-rt/lib/lowfat/lf_config.h | 152 ++++++++++++++++++++++++++
compiler-rt/lib/lowfat/lf_rtl.cpp | 87 +++++++++++----
3 files changed, 217 insertions(+), 23 deletions(-)
create mode 100644 compiler-rt/lib/lowfat/lf_config.h
diff --git a/compiler-rt/lib/lowfat/CMakeLists.txt b/compiler-rt/lib/lowfat/CMakeLists.txt
index 905bd66b41b88..f23d99d59a43a 100644
--- a/compiler-rt/lib/lowfat/CMakeLists.txt
+++ b/compiler-rt/lib/lowfat/CMakeLists.txt
@@ -5,6 +5,7 @@ set(LOWFAT_SOURCES
)
set(LOWFAT_HEADERS
+ lf_config.h
lf_interface.h
)
diff --git a/compiler-rt/lib/lowfat/lf_config.h b/compiler-rt/lib/lowfat/lf_config.h
new file mode 100644
index 0000000000000..02deb4375fff3
--- /dev/null
+++ b/compiler-rt/lib/lowfat/lf_config.h
@@ -0,0 +1,152 @@
+//===-- lf_config.h - LowFat Memory Layout Configuration -----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines the LowFat memory layout configuration.
+//
+// LowFat pointers encode allocation bounds directly in the pointer value:
+// - Memory is divided into regions, each for a specific size class
+// - Within each region, allocations are aligned to their size class
+// - Given a pointer, the base can be computed by masking off low bits
+// - The size can be looked up from a table using the region index
+//
+// Memory Layout (64-bit, example):
+// Region 0: [0x10_0000_0000, 0x20_0000_0000) - 16-byte allocations
+// Region 1: [0x20_0000_0000, 0x30_0000_0000) - 32-byte allocations
+// Region 2: [0x30_0000_0000, 0x40_0000_0000) - 64-byte allocations
+// ...
+// Region N: [0xN0_0000_0000, ...) - 2^(N+4)-byte allocations
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LF_CONFIG_H
+#define LF_CONFIG_H
+
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+namespace __lowfat {
+
+using namespace __sanitizer;
+
+//===----------------------------------------------------------------------===//
+// Size Class Configuration
+//===----------------------------------------------------------------------===//
+
+// Minimum allocation size (must be power of 2)
+constexpr uptr kMinSizeLog = 4; // 16 bytes
+constexpr uptr kMinSize = 1ULL << kMinSizeLog;
+
+// Maximum allocation size (must be power of 2)
+constexpr uptr kMaxSizeLog = 30; // 1 GB
+constexpr uptr kMaxSize = 1ULL << kMaxSizeLog;
+
+// Number of size classes (one per power of 2)
+constexpr uptr kNumSizeClasses = kMaxSizeLog - kMinSizeLog + 1;
+
+// Size class index for a given size (rounded up to next power of 2)
+// Returns 0 for sizes <= 16, 1 for sizes 17-32, etc.
+inline uptr SizeClassIndex(uptr size) {
+ if (size <= kMinSize)
+ return 0;
+ // Count leading zeros to find the highest set bit
+ uptr log2 = (sizeof(uptr) * 8 - 1) - __builtin_clzll(size);
+ // Round up if not exact power of 2
+ if (size > (1ULL << log2))
+ log2++;
+ return log2 - kMinSizeLog;
+}
+
+// Get the allocation size for a size class
+inline uptr SizeClassToSize(uptr class_index) {
+ return 1ULL << (class_index + kMinSizeLog);
+}
+
+//===----------------------------------------------------------------------===//
+// Memory Region Configuration
+//===----------------------------------------------------------------------===//
+
+// Each region is 4GB (32 bits of address space per region)
+constexpr uptr kRegionSizeLog = 32;
+constexpr uptr kRegionSize = 1ULL << kRegionSizeLog;
+
+// Base address where LowFat regions start
+// We use the upper portion of the address space
+// On 64-bit systems: 0x100000000000 (17.6 TB mark)
+constexpr uptr kRegionBase = 0x100000000000ULL;
+
+// Get the region number from a pointer
+inline uptr GetRegionIndex(uptr ptr) {
+ if (ptr < kRegionBase)
+ return (uptr)-1; // Not a LowFat pointer
+ return (ptr - kRegionBase) >> kRegionSizeLog;
+}
+
+// Get the start address of a region
+inline uptr GetRegionStart(uptr region_index) {
+ return kRegionBase + (region_index << kRegionSizeLog);
+}
+
+// Check if a pointer is within LowFat managed memory
+inline bool IsLowFatPointer(uptr ptr) {
+ uptr region = GetRegionIndex(ptr);
+ return region < kNumSizeClasses;
+}
+
+//===----------------------------------------------------------------------===//
+// Bounds Computation
+//===----------------------------------------------------------------------===//
+
+// Get the base address of an allocation from a LowFat pointer
+// This uses the key LowFat insight: allocations are aligned to their size
+inline uptr GetBase(uptr ptr) {
+ uptr region = GetRegionIndex(ptr);
+ if (region >= kNumSizeClasses)
+ return 0; // Not a valid LowFat pointer
+
+ uptr size = SizeClassToSize(region);
+ uptr mask = ~(size - 1); // Mask off low bits
+ return ptr & mask;
+}
+
+// Get the allocation size from a LowFat pointer
+inline uptr GetSize(uptr ptr) {
+ uptr region = GetRegionIndex(ptr);
+ if (region >= kNumSizeClasses)
+ return 0; // Not a valid LowFat pointer
+ return SizeClassToSize(region);
+}
+
+// Check if ptr..ptr+access_size is within bounds
+inline bool CheckBounds(uptr ptr, uptr access_size) {
+ uptr region = GetRegionIndex(ptr);
+ if (region >= kNumSizeClasses)
+ return true; // Not a LowFat pointer, assume valid (or could error)
+
+ uptr alloc_size = SizeClassToSize(region);
+ uptr base = ptr & ~(alloc_size - 1);
+ uptr end = base + alloc_size;
+
+ return (ptr + access_size) <= end;
+}
+
+//===----------------------------------------------------------------------===//
+// Region Table (for lookup by region index)
+//===----------------------------------------------------------------------===//
+
+struct RegionInfo {
+ uptr size; // Allocation size for this region
+ uptr alignment; // Alignment (same as size for LowFat)
+ uptr mask; // Mask to get base address: ptr & mask
+};
+
+// This table is indexed by region number
+// Initialized in lf_rtl.cpp
+extern RegionInfo kRegions[kNumSizeClasses];
+
+} // namespace __lowfat
+
+#endif // LF_CONFIG_H
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index fb5b60427f041..ee772ee196d1d 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
#include "lf_interface.h"
+#include "lf_config.h"
#include "sanitizer_common/sanitizer_common.h"
#include "sanitizer_common/sanitizer_flags.h"
#include "sanitizer_common/sanitizer_stacktrace.h"
@@ -26,8 +27,49 @@ namespace __lowfat {
// Flag to track initialization state
static bool lowfat_inited = false;
-// TODO: Add LowFat-specific configuration tables
-// These will define the memory regions and size classes for LowFat allocations
+// Region table - initialized in __lf_init
+// TODO: not actually needed, to use for convenience
+RegionInfo kRegions[kNumSizeClasses];
+
+// Pointers to the start of each mapped region
+static uptr region_bases[kNumSizeClasses];
+
+// Free list heads for each region (simple bump allocator for now)
+static uptr region_next_alloc[kNumSizeClasses];
+
+static void InitRegionTable() {
+ for (uptr i = 0; i < kNumSizeClasses; i++) {
+ uptr size = SizeClassToSize(i);
+ kRegions[i].size = size;
+ kRegions[i].alignment = size;
+ kRegions[i].mask = ~(size - 1);
+ }
+}
+
+// Initialize memory regions using mmap
+// Each region is mapped at a fixed address for the corresponding size class
+static bool InitMemoryRegions() {
+ for (uptr i = 0; i < kNumSizeClasses; i++) {
+ uptr region_start = GetRegionStart(i);
+
+ // Reserve the region without committing physical memory
+ // MmapFixedNoReserve maps memory but doesn't allocate physical pages
+ // until they're accessed (lazy allocation)
+ bool success = MmapFixedNoReserve(region_start, kRegionSize, "lowfat_region");
+
+ if (!success) {
+ Printf("LowFat: Failed to map region %zu at 0x%zx\n", i, region_start);
+ return false;
+ }
+
+ region_bases[i] = region_start;
+ region_next_alloc[i] = region_start;
+ }
+
+ Printf("LowFat: Mapped %zu regions starting at 0x%zx\n",
+ kNumSizeClasses, kRegionBase);
+ return true;
+}
static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound) {
Printf("=================================================================\n");
@@ -68,26 +110,31 @@ void __lf_init() {
if (__lowfat::lowfat_inited)
return;
- // Initialize sanitizer common
- // InitializeCommonFlags() would go here if we had custom flags
+ Printf("LowFat Sanitizer: initializing runtime\n");
+
+ __lowfat::InitRegionTable();
+
+ if (!__lowfat::InitMemoryRegions()) {
+ Printf("LowFat Sanitizer: failed to initialize memory regions\n");
+ Die();
+ }
- Printf("LowFat Sanitizer: runtime initialized\n");
+ Printf("LowFat Sanitizer: initialized runtime\n");
__lowfat::lowfat_inited = true;
}
SANITIZER_INTERFACE_ATTRIBUTE
void __lf_check_bounds(uptr ptr, uptr size) {
- // TODO: Implement actual bounds checking
- // For now, this is a stub that does nothing
- //
- // The full implementation should:
- // 1. Extract the region index from the pointer's high bits
- // 2. Look up the allocation size for that region
- // 3. Compute the base address using the region's alignment
- // 4. Check if ptr + size <= base + allocation_size
- (void)ptr;
- (void)size;
+ if (!__lowfat::IsLowFatPointer(ptr)) { // Not a LowFat-managed pointer, skip check
+ return;
+ }
+
+ if (!__lowfat::CheckBounds(ptr, size)) {
+ uptr base = __lowfat::GetBase(ptr);
+ uptr alloc_size = __lowfat::GetSize(ptr);
+ __lowfat::PrintErrorAndDie(ptr, base, alloc_size);
+ }
}
SANITIZER_INTERFACE_ATTRIBUTE
@@ -97,18 +144,12 @@ void __lf_report_oob(uptr ptr, uptr base, uptr bound) {
SANITIZER_INTERFACE_ATTRIBUTE
uptr __lf_get_base(uptr ptr) {
- // TODO: Implement base address extraction
- // This will use the LowFat memory layout to compute the base
- (void)ptr;
- return 0;
+ return __lowfat::GetBase(ptr);
}
SANITIZER_INTERFACE_ATTRIBUTE
uptr __lf_get_size(uptr ptr) {
- // TODO: Implement size extraction
- // This will look up the size class from the pointer's region
- (void)ptr;
- return 0;
+ return __lowfat::GetSize(ptr);
}
} // extern "C"
>From 3fbb1582d803af432adefcd8971c1591673adbfb Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 11 Feb 2026 10:37:33 +0800
Subject: [PATCH 09/55] [LowFat] Implement memory alloc and dealloc with free
list
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 113 ++++++++++++++++++++++++++----
1 file changed, 101 insertions(+), 12 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index ee772ee196d1d..3451ad7599f52 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -34,15 +34,23 @@ RegionInfo kRegions[kNumSizeClasses];
// Pointers to the start of each mapped region
static uptr region_bases[kNumSizeClasses];
-// Free list heads for each region (simple bump allocator for now)
+// Bump pointer: next fresh address to allocate from in each region
static uptr region_next_alloc[kNumSizeClasses];
+// Segregated free lists: one singly-linked list per size class
+// Free blocks store a pointer to the next free block at their start
+struct FreeBlock {
+ FreeBlock *next;
+};
+static FreeBlock *free_lists[kNumSizeClasses];
+
static void InitRegionTable() {
for (uptr i = 0; i < kNumSizeClasses; i++) {
uptr size = SizeClassToSize(i);
kRegions[i].size = size;
kRegions[i].alignment = size;
kRegions[i].mask = ~(size - 1);
+ free_lists[i] = nullptr;
}
}
@@ -58,7 +66,7 @@ static bool InitMemoryRegions() {
bool success = MmapFixedNoReserve(region_start, kRegionSize, "lowfat_region");
if (!success) {
- Printf("LowFat: Failed to map region %zu at 0x%zx\n", i, region_start);
+ // Printf("LowFat: Failed to map region %zu at 0x%zx\n", i, region_start);
return false;
}
@@ -66,11 +74,75 @@ static bool InitMemoryRegions() {
region_next_alloc[i] = region_start;
}
- Printf("LowFat: Mapped %zu regions starting at 0x%zx\n",
- kNumSizeClasses, kRegionBase);
+ // Printf("LowFat: Mapped %zu regions starting at 0x%zx\n",
+ // kNumSizeClasses, kRegionBase);
return true;
}
+// Allocate from a LowFat region
+// First checks the free list, then falls back to bump allocation
+static void *Allocate(uptr size) {
+ // Printf("LowFat: allocating %zu bytes\n", size);
+ if (size == 0)
+ size = 1;
+
+ uptr class_index = SizeClassIndex(size);
+ if (class_index >= kNumSizeClasses) {
+ // Printf("LowFat: allocation size %zu exceeds max size class\n", size);
+ return nullptr;
+ }
+
+ uptr alloc_size = SizeClassToSize(class_index);
+
+ // 1. Try free list first
+ FreeBlock *block = free_lists[class_index];
+ if (block) {
+ free_lists[class_index] = block->next;
+ // Zero the memory (free list pointer was stored here)
+ internal_memset(block, 0, alloc_size);
+ return (void *)block;
+ }
+
+ // 2. Fall back to bump allocation
+ uptr region_end = GetRegionStart(class_index) + kRegionSize;
+ uptr addr = region_next_alloc[class_index];
+
+ // Ensure alignment (should already be aligned)
+ addr = (addr + alloc_size - 1) & ~(alloc_size - 1);
+
+ if (addr + alloc_size > region_end) {
+ // Printf("LowFat: region %zu exhausted\n", class_index);
+ return nullptr;
+ }
+
+ region_next_alloc[class_index] = addr + alloc_size;
+ return (void *)addr;
+}
+
+// Free a LowFat allocation by adding it to the free list
+static void Deallocate(void *ptr) {
+ if (!ptr)
+ return;
+
+ uptr addr = (uptr)ptr;
+ // Printf("LowFat: freeing ptr 0x%zx\n", addr);
+
+ // Validate this is a LowFat pointer
+ if (!IsLowFatPointer(addr)) {
+ // Printf("LowFat: __lf_free called on non-LowFat pointer 0x%zx\n", addr);
+ return;
+ }
+
+ uptr region = GetRegionIndex(addr);
+ // Printf("LowFat: freed region %zu (size class %zu bytes)\n",
+ // region, SizeClassToSize(region));
+
+ // Add to the head of the free list for this size class
+ FreeBlock *block = (FreeBlock *)ptr;
+ block->next = free_lists[region];
+ free_lists[region] = block;
+}
+
static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound) {
Printf("=================================================================\n");
Printf("==ERROR: LowFat: out-of-bounds access detected\n");
@@ -110,29 +182,36 @@ void __lf_init() {
if (__lowfat::lowfat_inited)
return;
- Printf("LowFat Sanitizer: initializing runtime\n");
+ // Printf("LowFat Sanitizer: initializing runtime\n");
__lowfat::InitRegionTable();
if (!__lowfat::InitMemoryRegions()) {
- Printf("LowFat Sanitizer: failed to initialize memory regions\n");
+ // Printf("LowFat Sanitizer: failed to initialize memory regions\n");
Die();
}
- Printf("LowFat Sanitizer: initialized runtime\n");
+ // Printf("LowFat Sanitizer: initialized runtime\n");
__lowfat::lowfat_inited = true;
}
SANITIZER_INTERFACE_ATTRIBUTE
void __lf_check_bounds(uptr ptr, uptr size) {
- if (!__lowfat::IsLowFatPointer(ptr)) { // Not a LowFat-managed pointer, skip check
+ // Printf("LowFat: check_bounds(ptr=0x%zx, size=%zu)", ptr, size);
+ if (!__lowfat::IsLowFatPointer(ptr)) {
+ // Printf(" → not LowFat, skipping\n");
return;
}
-
- if (!__lowfat::CheckBounds(ptr, size)) {
- uptr base = __lowfat::GetBase(ptr);
- uptr alloc_size = __lowfat::GetSize(ptr);
+
+ uptr base = __lowfat::GetBase(ptr);
+ uptr alloc_size = __lowfat::GetSize(ptr);
+ bool in_bounds = __lowfat::CheckBounds(ptr, size);
+ // Printf(" → base=0x%zx, alloc=%zu, end=0x%zx, %s\n",
+ // base, alloc_size, base + alloc_size,
+ // in_bounds ? "OK" : "OOB!");
+
+ if (!in_bounds) {
__lowfat::PrintErrorAndDie(ptr, base, alloc_size);
}
}
@@ -152,6 +231,16 @@ uptr __lf_get_size(uptr ptr) {
return __lowfat::GetSize(ptr);
}
+SANITIZER_INTERFACE_ATTRIBUTE
+void *__lf_malloc(uptr size) {
+ return __lowfat::Allocate(size);
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+void __lf_free(void *ptr) {
+ __lowfat::Deallocate(ptr);
+}
+
} // extern "C"
#if SANITIZER_CAN_USE_PREINIT_ARRAY
>From ba4039155a84d4490ea9ed26429f2107aaec346f Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 11 Feb 2026 10:45:26 +0800
Subject: [PATCH 10/55] [LowFat] Add transform pass basic test
---
.../Instrumentation/LowFatSanitizer/basic.ll | 92 +++++++++++++++++++
1 file changed, 92 insertions(+)
create mode 100644 llvm/test/Instrumentation/LowFatSanitizer/basic.ll
diff --git a/llvm/test/Instrumentation/LowFatSanitizer/basic.ll b/llvm/test/Instrumentation/LowFatSanitizer/basic.ll
new file mode 100644
index 0000000000000..cbb4aa1046121
--- /dev/null
+++ b/llvm/test/Instrumentation/LowFatSanitizer/basic.ll
@@ -0,0 +1,92 @@
+; RUN: opt < %s -passes=lowfat -S | FileCheck %s
+target datalayout = "e-m:o-i64:64-i128:128-n32:64-S128-Fn32"
+
+; Test 1: Load should be instrumented with __lf_check_bounds
+define i32 @test_load(ptr %p) {
+; CHECK-LABEL: @test_load
+; CHECK: %[[PTR:.*]] = ptrtoint ptr %p to i64
+; CHECK-NEXT: call void @__lf_check_bounds(i64 %[[PTR]], i64 4)
+; CHECK-NEXT: %val = load i32, ptr %p
+ %val = load i32, ptr %p, align 4
+ ret i32 %val
+}
+
+; Test 2: Store should be instrumented
+define void @test_store(ptr %p, i32 %v) {
+; CHECK-LABEL: @test_store
+; CHECK: %[[PTR:.*]] = ptrtoint ptr %p to i64
+; CHECK-NEXT: call void @__lf_check_bounds(i64 %[[PTR]], i64 4)
+; CHECK-NEXT: store i32 %v, ptr %p
+ store i32 %v, ptr %p, align 4
+ ret void
+}
+
+; Test 3: Volatile accesses should NOT be instrumented
+define i32 @test_volatile_load(ptr %p) {
+; CHECK-LABEL: @test_volatile_load
+; CHECK-NOT: call void @__lf_check_bounds
+; CHECK: load volatile i32, ptr %p
+ %val = load volatile i32, ptr %p, align 4
+ ret i32 %val
+}
+
+define void @test_volatile_store(ptr %p, i32 %v) {
+; CHECK-LABEL: @test_volatile_store
+; CHECK-NOT: call void @__lf_check_bounds
+; CHECK: store volatile i32 %v, ptr %p
+ store volatile i32 %v, ptr %p, align 4
+ ret void
+}
+
+; Test 4: Runtime functions (__lf_*) should NOT be instrumented
+define void @__lf_check_bounds(i64 %ptr, i64 %size) {
+; CHECK-LABEL: @__lf_check_bounds
+; CHECK-NOT: call void @__lf_check_bounds
+ ret void
+}
+
+; Test 5: i8 load should use size 1
+define i8 @test_load_i8(ptr %p) {
+; CHECK-LABEL: @test_load_i8
+; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 1)
+ %val = load i8, ptr %p, align 1
+ ret i8 %val
+}
+
+; Test 6: i64 store should use size 8
+define void @test_store_i64(ptr %p, i64 %v) {
+; CHECK-LABEL: @test_store_i64
+; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 8)
+ store i64 %v, ptr %p, align 8
+ ret void
+}
+
+; Test 7: AtomicRMW should be instrumented
+define i32 @test_atomic_rmw(ptr %p) {
+; CHECK-LABEL: @test_atomic_rmw
+; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: atomicrmw add ptr %p, i32 1
+ %old = atomicrmw add ptr %p, i32 1 monotonic
+ ret i32 %old
+}
+
+; Test 8: AtomicCmpXchg should be instrumented
+define { i32, i1 } @test_atomic_cmpxchg(ptr %p) {
+; CHECK-LABEL: @test_atomic_cmpxchg
+; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: cmpxchg ptr %p, i32 0, i32 1
+ %val = cmpxchg ptr %p, i32 0, i32 1 monotonic monotonic
+ ret { i32, i1 } %val
+}
+
+; Test 9: Multiple accesses in one function
+define void @test_multiple(ptr %p, ptr %q) {
+; CHECK-LABEL: @test_multiple
+; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: load i32
+; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: store i32
+ %val = load i32, ptr %p, align 4
+ store i32 %val, ptr %q, align 4
+ ret void
+}
>From e89c30bee11afdbfb0a4bde632afea26422a1bc6 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 11 Feb 2026 10:49:01 +0800
Subject: [PATCH 11/55] [LowFat] Add basic testcases for lf_rtl
---
compiler-rt/test/lowfat/basic_inbounds.cpp | 23 ++++++++++++++++
.../test/lowfat/cross_boundary_oob.cpp | 27 +++++++++++++++++++
compiler-rt/test/lowfat/free_list_reuse.cpp | 25 +++++++++++++++++
3 files changed, 75 insertions(+)
create mode 100644 compiler-rt/test/lowfat/basic_inbounds.cpp
create mode 100644 compiler-rt/test/lowfat/cross_boundary_oob.cpp
create mode 100644 compiler-rt/test/lowfat/free_list_reuse.cpp
diff --git a/compiler-rt/test/lowfat/basic_inbounds.cpp b/compiler-rt/test/lowfat/basic_inbounds.cpp
new file mode 100644
index 0000000000000..05e6290614e12
--- /dev/null
+++ b/compiler-rt/test/lowfat/basic_inbounds.cpp
@@ -0,0 +1,23 @@
+// RUN: %clangxx_lowfat %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+
+// Verify that the LowFat runtime initializes and basic in-bounds
+// allocations work without errors.
+
+extern "C" void *__lf_malloc(unsigned long size);
+extern "C" void __lf_free(void *ptr);
+
+int main() {
+ // CHECK: LowFat Sanitizer: initialized runtime
+ int *arr = (int *)__lf_malloc(10 * sizeof(int));
+ if (!arr)
+ return 1;
+
+ // In-bounds accesses — should not trigger OOB
+ arr[0] = 42;
+ arr[9] = 99;
+
+ __lf_free(arr);
+ // CHECK-NOT: ERROR: LowFat
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/cross_boundary_oob.cpp b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
new file mode 100644
index 0000000000000..57dfa1434a032
--- /dev/null
+++ b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
@@ -0,0 +1,27 @@
+// RUN: %clangxx_lowfat %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// Verify that a cross-boundary out-of-bounds access is detected.
+// Writing 8 bytes starting at offset 12 of a 16-byte slot crosses
+// the slot boundary (bytes 12-19 > 16 bytes).
+
+extern "C" void *__lf_malloc(unsigned long size);
+
+int main() {
+ // Allocate exactly 16 bytes → 16-byte size class (smallest slot)
+ char *buf = (char *)__lf_malloc(16);
+ if (!buf)
+ return 1;
+
+ // In-bounds write
+ buf[0] = 'A';
+ buf[15] = 'B';
+
+ // Cross-boundary OOB: write 8 bytes at offset 12
+ // ptr + 8 = buf+20 > buf+16 → out of bounds
+ // CHECK: ERROR: LowFat: out-of-bounds access detected
+ double *cross = (double *)(buf + 12);
+ *cross = 3.14;
+
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/free_list_reuse.cpp b/compiler-rt/test/lowfat/free_list_reuse.cpp
new file mode 100644
index 0000000000000..6460277ed5f81
--- /dev/null
+++ b/compiler-rt/test/lowfat/free_list_reuse.cpp
@@ -0,0 +1,25 @@
+// RUN: %clangxx_lowfat %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+
+// Verify that allocation and free list reuse works correctly.
+
+extern "C" void *__lf_malloc(unsigned long size);
+extern "C" void __lf_free(void *ptr);
+
+int main() {
+ // CHECK: LowFat Sanitizer: initialized runtime
+
+ // Allocate and free, then allocate again — should reuse from free list
+ int *a = (int *)__lf_malloc(10 * sizeof(int));
+ a[0] = 1;
+ __lf_free(a);
+
+ int *b = (int *)__lf_malloc(10 * sizeof(int));
+ // After free list reuse, b should equal a (same address reused)
+ b[0] = 2;
+ b[9] = 3;
+ __lf_free(b);
+
+ // CHECK-NOT: ERROR: LowFat
+ return 0;
+}
>From a4f469c40c2959d467b1613b6c79eac677554df6 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Fri, 13 Feb 2026 12:42:47 +0800
Subject: [PATCH 12/55] [LowFat] Implement malloc/free interceptors
---
compiler-rt/lib/lowfat/CMakeLists.txt | 8 +-
compiler-rt/lib/lowfat/lf_allocator.h | 33 ++++
compiler-rt/lib/lowfat/lf_interceptors.cpp | 177 +++++++++++++++++++++
compiler-rt/lib/lowfat/lf_rtl.cpp | 11 +-
4 files changed, 223 insertions(+), 6 deletions(-)
create mode 100644 compiler-rt/lib/lowfat/lf_allocator.h
create mode 100644 compiler-rt/lib/lowfat/lf_interceptors.cpp
diff --git a/compiler-rt/lib/lowfat/CMakeLists.txt b/compiler-rt/lib/lowfat/CMakeLists.txt
index f23d99d59a43a..22fa2c5463c1e 100644
--- a/compiler-rt/lib/lowfat/CMakeLists.txt
+++ b/compiler-rt/lib/lowfat/CMakeLists.txt
@@ -2,9 +2,11 @@ include_directories(..)
set(LOWFAT_SOURCES
lf_rtl.cpp
+ lf_interceptors.cpp
)
set(LOWFAT_HEADERS
+ lf_allocator.h
lf_config.h
lf_interface.h
)
@@ -26,7 +28,8 @@ if(COMPILER_RT_HAS_LOWFAT)
ARCHS ${LOWFAT_SUPPORTED_ARCH}
SOURCES ${LOWFAT_SOURCES}
ADDITIONAL_HEADERS ${LOWFAT_HEADERS}
- OBJECT_LIBS RTSanitizerCommon
+ OBJECT_LIBS RTInterception
+ RTSanitizerCommon
RTSanitizerCommonLibc
RTSanitizerCommonSymbolizer
CFLAGS ${LOWFAT_CFLAGS}
@@ -39,7 +42,8 @@ if(COMPILER_RT_HAS_LOWFAT)
ARCHS ${LOWFAT_SUPPORTED_ARCH}
SOURCES ${LOWFAT_SOURCES}
ADDITIONAL_HEADERS ${LOWFAT_HEADERS}
- OBJECT_LIBS RTSanitizerCommon
+ OBJECT_LIBS RTInterception
+ RTSanitizerCommon
RTSanitizerCommonLibc
RTSanitizerCommonSymbolizer
RTSanitizerCommonSymbolizerInternal
diff --git a/compiler-rt/lib/lowfat/lf_allocator.h b/compiler-rt/lib/lowfat/lf_allocator.h
new file mode 100644
index 0000000000000..698b98045567a
--- /dev/null
+++ b/compiler-rt/lib/lowfat/lf_allocator.h
@@ -0,0 +1,33 @@
+//===-- lf_allocator.h - LowFat Allocator Internal Interface ----*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Internal allocator interface shared between lf_rtl.cpp and
+// lf_interceptors.cpp.
+//
+//===----------------------------------------------------------------------===//
+#ifndef LF_ALLOCATOR_H
+#define LF_ALLOCATOR_H
+
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+namespace __lowfat {
+
+using __sanitizer::uptr;
+
+// Allocate from a LowFat region. Returns nullptr if size exceeds max.
+void *Allocate(uptr size);
+
+// Free a LowFat allocation.
+void Deallocate(void *ptr);
+
+// Initialize interceptors (called from __lf_init).
+void InitializeInterceptors();
+
+} // namespace __lowfat
+
+#endif // LF_ALLOCATOR_H
diff --git a/compiler-rt/lib/lowfat/lf_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp
new file mode 100644
index 0000000000000..8d96b8d88ef0b
--- /dev/null
+++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp
@@ -0,0 +1,177 @@
+//===-- lf_interceptors.cpp - LowFat Malloc/Free Interceptors -------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Interceptors for malloc/free/calloc/realloc that route heap allocations
+// through the LowFat allocator so all heap memory gets bounds-checked.
+//
+//===----------------------------------------------------------------------===//
+
+#include "interception/interception.h"
+#include "lf_allocator.h"
+#include "lf_config.h"
+#include "sanitizer_common/sanitizer_allocator_dlsym.h"
+
+using namespace __sanitizer;
+
+namespace __lowfat {
+extern bool lowfat_inited;
+} // namespace __lowfat
+
+// DlsymAlloc handles allocations that happen before our runtime is initialized
+// (e.g., during dynamic linker symbol resolution). Uses a small static buffer.
+namespace {
+struct DlsymAlloc : public DlSymAllocator<DlsymAlloc> {
+ static bool UseImpl() { return !__lowfat::lowfat_inited; }
+};
+} // namespace
+
+// Helper: should this allocation go through LowFat?
+// Allocations larger than our max size class fall back to system malloc.
+static inline bool ShouldUseLowFat(uptr size) {
+ return __lowfat::lowfat_inited && size > 0 && size <= __lowfat::kMaxSize;
+}
+
+//===----------------------------------------------------------------------===//
+// Interceptors
+//===----------------------------------------------------------------------===//
+
+INTERCEPTOR(void *, malloc, uptr size) {
+ if (DlsymAlloc::Use())
+ return DlsymAlloc::Allocate(size);
+ if (ShouldUseLowFat(size))
+ return __lowfat::Allocate(size);
+ return REAL(malloc)(size);
+}
+
+INTERCEPTOR(void, free, void *ptr) {
+ if (!ptr)
+ return;
+ if (DlsymAlloc::PointerIsMine(ptr))
+ return DlsymAlloc::Free(ptr);
+ if (__lowfat::IsLowFatPointer((uptr)ptr)) {
+ __lowfat::Deallocate(ptr);
+ return;
+ }
+ REAL(free)(ptr);
+}
+
+INTERCEPTOR(void *, calloc, uptr nmemb, uptr size) {
+ if (DlsymAlloc::Use())
+ return DlsymAlloc::Callocate(nmemb, size);
+ uptr total = nmemb * size;
+ if (ShouldUseLowFat(total)) {
+ void *ptr = __lowfat::Allocate(total);
+ if (ptr)
+ internal_memset(ptr, 0, total);
+ return ptr;
+ }
+ return REAL(calloc)(nmemb, size);
+}
+
+INTERCEPTOR(void *, realloc, void *ptr, uptr size) {
+ if (DlsymAlloc::Use() || DlsymAlloc::PointerIsMine(ptr))
+ return DlsymAlloc::Realloc(ptr, size);
+
+ // realloc(nullptr, size) == malloc(size)
+ if (!ptr) {
+ if (ShouldUseLowFat(size))
+ return __lowfat::Allocate(size);
+ return REAL(malloc)(size);
+ }
+
+ // realloc(ptr, 0) == free(ptr)
+ if (size == 0) {
+ if (__lowfat::IsLowFatPointer((uptr)ptr))
+ __lowfat::Deallocate(ptr);
+ else
+ REAL(free)(ptr);
+ return nullptr;
+ }
+
+ bool old_is_lowfat = __lowfat::IsLowFatPointer((uptr)ptr);
+
+ if (ShouldUseLowFat(size)) {
+ void *new_ptr = __lowfat::Allocate(size);
+ if (!new_ptr)
+ return nullptr;
+ // Copy old data. For LowFat pointers, old size = size class.
+ // For system pointers, we don't know exact old size, copy 'size' bytes.
+ uptr copy_size = size;
+ if (old_is_lowfat) {
+ uptr old_size = __lowfat::GetSize((uptr)ptr);
+ if (old_size < copy_size)
+ copy_size = old_size;
+ }
+ internal_memcpy(new_ptr, ptr, copy_size);
+ // Free old
+ if (old_is_lowfat)
+ __lowfat::Deallocate(ptr);
+ else
+ REAL(free)(ptr);
+ return new_ptr;
+ }
+
+ // New size exceeds LowFat max — use system realloc
+ if (old_is_lowfat) {
+ // Must migrate from LowFat to system
+ void *new_ptr = REAL(malloc)(size);
+ if (!new_ptr)
+ return nullptr;
+ uptr old_size = __lowfat::GetSize((uptr)ptr);
+ uptr copy_size = old_size < size ? old_size : size;
+ internal_memcpy(new_ptr, ptr, copy_size);
+ __lowfat::Deallocate(ptr);
+ return new_ptr;
+ }
+
+ return REAL(realloc)(ptr, size);
+}
+
+INTERCEPTOR(void *, valloc, uptr size) {
+ // valloc requires page-aligned allocation; fall back to system
+ return REAL(valloc)(size);
+}
+
+INTERCEPTOR(int, posix_memalign, void **memptr, uptr alignment, uptr size) {
+ // LowFat allocations are aligned to their size class, which may satisfy
+ // the alignment requirement. For now, fall back to system.
+ return REAL(posix_memalign)(memptr, alignment, size);
+}
+
+#if SANITIZER_APPLE
+INTERCEPTOR(uptr, malloc_size, void *ptr) {
+ if (DlsymAlloc::PointerIsMine(ptr))
+ return DlsymAlloc::GetSize(ptr);
+ if (__lowfat::IsLowFatPointer((uptr)ptr))
+ return __lowfat::GetSize((uptr)ptr);
+ return REAL(malloc_size)(ptr);
+}
+#endif
+
+//===----------------------------------------------------------------------===//
+// Initialization
+//===----------------------------------------------------------------------===//
+
+namespace __lowfat {
+void InitializeInterceptors() {
+ static int inited = 0;
+ CHECK_EQ(inited, 0);
+
+ INTERCEPT_FUNCTION(malloc);
+ INTERCEPT_FUNCTION(free);
+ INTERCEPT_FUNCTION(calloc);
+ INTERCEPT_FUNCTION(realloc);
+ INTERCEPT_FUNCTION(valloc);
+ INTERCEPT_FUNCTION(posix_memalign);
+#if SANITIZER_APPLE
+ INTERCEPT_FUNCTION(malloc_size);
+#endif
+
+ inited = 1;
+}
+} // namespace __lowfat
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 3451ad7599f52..06a5d1dab9b67 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -14,6 +14,7 @@
//
//===----------------------------------------------------------------------===//
+#include "lf_allocator.h"
#include "lf_interface.h"
#include "lf_config.h"
#include "sanitizer_common/sanitizer_common.h"
@@ -24,8 +25,8 @@ using namespace __sanitizer;
namespace __lowfat {
-// Flag to track initialization state
-static bool lowfat_inited = false;
+// Flag to track initialization state (not static — accessed by lf_interceptors.cpp)
+bool lowfat_inited = false;
// Region table - initialized in __lf_init
// TODO: not actually needed, to use for convenience
@@ -81,7 +82,7 @@ static bool InitMemoryRegions() {
// Allocate from a LowFat region
// First checks the free list, then falls back to bump allocation
-static void *Allocate(uptr size) {
+void *Allocate(uptr size) {
// Printf("LowFat: allocating %zu bytes\n", size);
if (size == 0)
size = 1;
@@ -120,7 +121,7 @@ static void *Allocate(uptr size) {
}
// Free a LowFat allocation by adding it to the free list
-static void Deallocate(void *ptr) {
+void Deallocate(void *ptr) {
if (!ptr)
return;
@@ -194,6 +195,8 @@ void __lf_init() {
// Printf("LowFat Sanitizer: initialized runtime\n");
__lowfat::lowfat_inited = true;
+
+ __lowfat::InitializeInterceptors();
}
SANITIZER_INTERFACE_ATTRIBUTE
>From b96c2469682692131fe99a2941df2bfe8979a2c4 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 16 Feb 2026 11:00:16 +0800
Subject: [PATCH 13/55] [LowFat] Implement inline bounds checking
Replace the __lf_check_bounds runtime call with inline LLVM IR to perform bounds checking directly
---
.../Instrumentation/LowFatSanitizer.cpp | 79 +++++++++++++++----
1 file changed, 62 insertions(+), 17 deletions(-)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 0b57458fff0d2..b21f7005de618 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -24,6 +24,7 @@
#include "llvm/IR/Module.h"
#include "llvm/IR/Type.h"
#include "llvm/Support/Debug.h"
+#include "llvm/Transforms/Utils/BasicBlockUtils.h"
using namespace llvm;
@@ -45,8 +46,8 @@ class LowFatSanitizer {
bool run();
private:
- /// Get or create the declaration for __lf_check_bounds.
- FunctionCallee getCheckBoundsFn();
+ /// Get or create the declaration for __lf_report_oob.
+ FunctionCallee getReportOobFn();
/// Instrument a single function.
bool instrumentFunction(Function &F);
@@ -56,20 +57,27 @@ class LowFatSanitizer {
bool instrumentMemoryAccess(Instruction *I, Value *Ptr, Type *AccessTy);
Module &M;
- const LowFatSanitizerOptions &Options;
+ const LowFatSanitizerOptions &Options; // TODO: impelement options
const DataLayout &DL;
Type *IntptrTy;
- FunctionCallee CheckBoundsFn;
+ FunctionCallee ReportOobFn;
};
-FunctionCallee LowFatSanitizer::getCheckBoundsFn() {
- if (!CheckBoundsFn) {
- // void __lf_check_bounds(uptr ptr, uptr size)
+// LowFat Constants (must match lf_config.h)
+static const uint64_t RegionBase = 0x100000000000ULL;
+static const uint64_t RegionSizeLog = 32;
+static const uint64_t MinSizeLog = 4;
+static const uint64_t NumSizeClasses = 27;
+
+FunctionCallee LowFatSanitizer::getReportOobFn() {
+ if (!ReportOobFn) {
+ // void __lf_report_oob(uptr ptr, uptr base, uptr size)
Type *VoidTy = Type::getVoidTy(M.getContext());
- CheckBoundsFn = M.getOrInsertFunction(
- "__lf_check_bounds", FunctionType::get(VoidTy, {IntptrTy, IntptrTy}, false));
+ ReportOobFn = M.getOrInsertFunction(
+ "__lf_report_oob",
+ FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy}, false));
}
- return CheckBoundsFn;
+ return ReportOobFn;
}
bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
@@ -86,13 +94,50 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
// Convert pointer to integer
Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
- // Create size constant
- Value *SizeVal = ConstantInt::get(IntptrTy, AccessSize.getFixedValue());
-
- // Insert call to __lf_check_bounds(ptr, size)
- IRB.CreateCall(getCheckBoundsFn(), {PtrInt, SizeVal});
-
- LLVM_DEBUG(dbgs() << "[LowFat] Instrumented: " << *I << "\n");
+ // 1. Get region index: (Ptr - RegionBase) >> RegionSizeLog
+ Value *RegionBaseVal = ConstantInt::get(IntptrTy, RegionBase);
+ Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
+ Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
+
+ // 2. Check if LowFat pointer: Region < NumSizeClasses
+ Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
+ Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+
+ // Split block for the slow path (if LowFat)
+ Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
+ IRBuilder<> ThenIRB(ThenTerm);
+
+ // 3. Compute bounds inside the 'then' block
+ // Size = 1 << (Region + MinSizeLog)
+ Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
+ Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
+ Value *SizeOne = ConstantInt::get(IntptrTy, 1);
+ Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
+
+ // Mask = ~(AllocSize - 1)
+ Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
+ Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
+
+ // Base = Ptr & Mask
+ Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
+
+ // End = Base + AllocSize
+ Value *End = ThenIRB.CreateAdd(Base, AllocSize);
+
+ // 4. Check access: Ptr + AccessSize <= End
+ // Equivalent OOB check: (Ptr + AccessSize) > End
+ Value *AccessSizeVal = ConstantInt::get(IntptrTy, AccessSize.getFixedValue());
+ Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, AccessSizeVal);
+ Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
+
+ // Split again for OOB reporting
+ Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
+ IRBuilder<> OobIRB(OobTerm);
+
+ // 5. Report error (slow path)
+ OobIRB.CreateCall(getReportOobFn(), {PtrInt, Base, AllocSize});
+
+ LLVM_DEBUG(dbgs() << "[LowFat] Instrumented (inline): " << *I << "\n");
return true;
}
>From 19efaf97a26a247cec23c415c39d48ecee691b61 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 16 Feb 2026 11:01:10 +0800
Subject: [PATCH 14/55] [LowFat] Update tests for inline bounds checking
---
.../Instrumentation/LowFatSanitizer/basic.ll | 46 +++++++++++--------
1 file changed, 28 insertions(+), 18 deletions(-)
diff --git a/llvm/test/Instrumentation/LowFatSanitizer/basic.ll b/llvm/test/Instrumentation/LowFatSanitizer/basic.ll
index cbb4aa1046121..97588bb23bbc5 100644
--- a/llvm/test/Instrumentation/LowFatSanitizer/basic.ll
+++ b/llvm/test/Instrumentation/LowFatSanitizer/basic.ll
@@ -1,12 +1,15 @@
; RUN: opt < %s -passes=lowfat -S | FileCheck %s
target datalayout = "e-m:o-i64:64-i128:128-n32:64-S128-Fn32"
-; Test 1: Load should be instrumented with __lf_check_bounds
+; Test 1: Load should be instrumented with inline checks
define i32 @test_load(ptr %p) {
; CHECK-LABEL: @test_load
-; CHECK: %[[PTR:.*]] = ptrtoint ptr %p to i64
-; CHECK-NEXT: call void @__lf_check_bounds(i64 %[[PTR]], i64 4)
-; CHECK-NEXT: %val = load i32, ptr %p
+; CHECK: %[[PTR_INT:.*]] = ptrtoint ptr %p to i64
+; CHECK: sub i64 %[[PTR_INT]], 17592186044416
+; CHECK: lshr i64 {{.*}}, 32
+; CHECK: icmp ult i64 {{.*}}, 27
+; CHECK: call void @__lf_report_oob
+; CHECK: %val = load i32, ptr %p
%val = load i32, ptr %p, align 4
ret i32 %val
}
@@ -14,9 +17,9 @@ define i32 @test_load(ptr %p) {
; Test 2: Store should be instrumented
define void @test_store(ptr %p, i32 %v) {
; CHECK-LABEL: @test_store
-; CHECK: %[[PTR:.*]] = ptrtoint ptr %p to i64
-; CHECK-NEXT: call void @__lf_check_bounds(i64 %[[PTR]], i64 4)
-; CHECK-NEXT: store i32 %v, ptr %p
+; CHECK: ptrtoint ptr %p to i64
+; CHECK: call void @__lf_report_oob
+; CHECK: store i32 %v, ptr %p
store i32 %v, ptr %p, align 4
ret void
}
@@ -24,7 +27,7 @@ define void @test_store(ptr %p, i32 %v) {
; Test 3: Volatile accesses should NOT be instrumented
define i32 @test_volatile_load(ptr %p) {
; CHECK-LABEL: @test_volatile_load
-; CHECK-NOT: call void @__lf_check_bounds
+; CHECK-NOT: call void @__lf_report_oob
; CHECK: load volatile i32, ptr %p
%val = load volatile i32, ptr %p, align 4
ret i32 %val
@@ -32,23 +35,27 @@ define i32 @test_volatile_load(ptr %p) {
define void @test_volatile_store(ptr %p, i32 %v) {
; CHECK-LABEL: @test_volatile_store
-; CHECK-NOT: call void @__lf_check_bounds
+; CHECK-NOT: call void @__lf_report_oob
; CHECK: store volatile i32 %v, ptr %p
store volatile i32 %v, ptr %p, align 4
ret void
}
; Test 4: Runtime functions (__lf_*) should NOT be instrumented
-define void @__lf_check_bounds(i64 %ptr, i64 %size) {
-; CHECK-LABEL: @__lf_check_bounds
-; CHECK-NOT: call void @__lf_check_bounds
+define void @__lf_internal_test(ptr %p) {
+; CHECK-LABEL: @__lf_internal_test
+; CHECK-NOT: call void @__lf_report_oob
+ store i32 0, ptr %p
ret void
}
; Test 5: i8 load should use size 1
define i8 @test_load_i8(ptr %p) {
; CHECK-LABEL: @test_load_i8
-; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 1)
+; CHECK: %[[PTR_INT:.*]] = ptrtoint ptr %p to i64
+; CHECK: add i64 %[[PTR_INT]], 1
+; CHECK-NEXT: icmp ugt
+; CHECK: call void @__lf_report_oob
%val = load i8, ptr %p, align 1
ret i8 %val
}
@@ -56,7 +63,10 @@ define i8 @test_load_i8(ptr %p) {
; Test 6: i64 store should use size 8
define void @test_store_i64(ptr %p, i64 %v) {
; CHECK-LABEL: @test_store_i64
-; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 8)
+; CHECK: %[[PTR_INT:.*]] = ptrtoint ptr %p to i64
+; CHECK: add i64 %[[PTR_INT]], 8
+; CHECK-NEXT: icmp ugt
+; CHECK: call void @__lf_report_oob
store i64 %v, ptr %p, align 8
ret void
}
@@ -64,7 +74,7 @@ define void @test_store_i64(ptr %p, i64 %v) {
; Test 7: AtomicRMW should be instrumented
define i32 @test_atomic_rmw(ptr %p) {
; CHECK-LABEL: @test_atomic_rmw
-; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: call void @__lf_report_oob
; CHECK: atomicrmw add ptr %p, i32 1
%old = atomicrmw add ptr %p, i32 1 monotonic
ret i32 %old
@@ -73,7 +83,7 @@ define i32 @test_atomic_rmw(ptr %p) {
; Test 8: AtomicCmpXchg should be instrumented
define { i32, i1 } @test_atomic_cmpxchg(ptr %p) {
; CHECK-LABEL: @test_atomic_cmpxchg
-; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: call void @__lf_report_oob
; CHECK: cmpxchg ptr %p, i32 0, i32 1
%val = cmpxchg ptr %p, i32 0, i32 1 monotonic monotonic
ret { i32, i1 } %val
@@ -82,9 +92,9 @@ define { i32, i1 } @test_atomic_cmpxchg(ptr %p) {
; Test 9: Multiple accesses in one function
define void @test_multiple(ptr %p, ptr %q) {
; CHECK-LABEL: @test_multiple
-; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: call void @__lf_report_oob
; CHECK: load i32
-; CHECK: call void @__lf_check_bounds(i64 %{{.*}}, i64 4)
+; CHECK: call void @__lf_report_oob
; CHECK: store i32
%val = load i32, ptr %p, align 4
store i32 %val, ptr %q, align 4
>From 749aa9e240872b1869f79d8c66dd5b677edaf9c2 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 16 Feb 2026 11:02:22 +0800
Subject: [PATCH 15/55] [LowFat] Remove __lf_check_bounds from runtime
---
compiler-rt/lib/lowfat/lf_interface.h | 5 -----
compiler-rt/lib/lowfat/lf_rtl.cpp | 20 --------------------
2 files changed, 25 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
index 7e6e90c9cf0e0..3dd1e2028f42a 100644
--- a/compiler-rt/lib/lowfat/lf_interface.h
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -23,11 +23,6 @@ extern "C" {
// Initialize the LowFat Sanitizer runtime. Called early during program startup.
SANITIZER_INTERFACE_ATTRIBUTE void __lf_init();
-// Perform a bounds check on a pointer access.
-// ptr: The pointer being accessed
-// size: The size of the access in bytes
-SANITIZER_INTERFACE_ATTRIBUTE void __lf_check_bounds(uptr ptr, uptr size);
-
// Report an out-of-bounds error.
// ptr: The pointer that caused the violation
// base: The base address of the allocation
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 06a5d1dab9b67..a53767b1f5e76 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -199,26 +199,6 @@ void __lf_init() {
__lowfat::InitializeInterceptors();
}
-SANITIZER_INTERFACE_ATTRIBUTE
-void __lf_check_bounds(uptr ptr, uptr size) {
- // Printf("LowFat: check_bounds(ptr=0x%zx, size=%zu)", ptr, size);
- if (!__lowfat::IsLowFatPointer(ptr)) {
- // Printf(" → not LowFat, skipping\n");
- return;
- }
-
- uptr base = __lowfat::GetBase(ptr);
- uptr alloc_size = __lowfat::GetSize(ptr);
- bool in_bounds = __lowfat::CheckBounds(ptr, size);
- // Printf(" → base=0x%zx, alloc=%zu, end=0x%zx, %s\n",
- // base, alloc_size, base + alloc_size,
- // in_bounds ? "OK" : "OOB!");
-
- if (!in_bounds) {
- __lowfat::PrintErrorAndDie(ptr, base, alloc_size);
- }
-}
-
SANITIZER_INTERFACE_ATTRIBUTE
void __lf_report_oob(uptr ptr, uptr base, uptr bound) {
__lowfat::PrintErrorAndDie(ptr, base, bound);
>From c9b7afe8c92e92f20d408ef55d9c3b618f4818fc Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sun, 1 Mar 2026 19:02:25 +0800
Subject: [PATCH 16/55] [LowFat] Add options parsing and warnings for recover
---
clang/lib/CodeGen/BackendUtil.cpp | 1 +
.../cmake/Modules/AllSupportedArchDefs.cmake | 7 +++-
compiler-rt/lib/lowfat/lf_interface.h | 7 ++++
compiler-rt/lib/lowfat/lf_rtl.cpp | 37 +++++++------------
.../Instrumentation/LowFatSanitizer.h | 2 +-
llvm/lib/Passes/PassBuilder.cpp | 11 ++++--
.../Instrumentation/LowFatSanitizer.cpp | 32 +++++++++++++---
7 files changed, 64 insertions(+), 33 deletions(-)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index a5a1da5334df1..ef576fb649e2d 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -771,6 +771,7 @@ static void addSanitizers(const Triple &TargetTriple,
if (LangOpts.Sanitize.has(SanitizerKind::LowFat)) {
LowFatSanitizerOptions Opts;
+ Opts.Recover = CodeGenOpts.SanitizeRecover.has(SanitizerKind::LowFat);
MPM.addPass(LowFatSanitizerPass(Opts));
}
};
diff --git a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
index 0015b0fffce81..5fbf41a86bb4b 100644
--- a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
+++ b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
@@ -122,7 +122,12 @@ set(ALL_XRAY_SUPPORTED_ARCH ${X86_64} ${ARM32} ${ARM64} ${MIPS32} ${MIPS64}
endif()
set(ALL_XRAY_DSO_SUPPORTED_ARCH ${X86_64} ${ARM64})
set(ALL_SHADOWCALLSTACK_SUPPORTED_ARCH ${ARM64})
-set(ALL_LOWFAT_SUPPORTED_ARCH ${X86_64} ${ARM64})
+if(APPLE)
+ # Exclude x86_64h: lipo cannot combine x86_64 and x86_64h into one fat binary.
+ set(ALL_LOWFAT_SUPPORTED_ARCH x86_64 ${ARM64})
+else()
+ set(ALL_LOWFAT_SUPPORTED_ARCH ${X86_64} ${ARM64})
+endif()
if (UNIX)
if (OS_NAME MATCHES "Linux")
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
index 3dd1e2028f42a..aba0b9b4047c9 100644
--- a/compiler-rt/lib/lowfat/lf_interface.h
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -30,6 +30,13 @@ SANITIZER_INTERFACE_ATTRIBUTE void __lf_init();
SANITIZER_INTERFACE_ATTRIBUTE void __lf_report_oob(uptr ptr, uptr base,
uptr bound);
+// Warn about an out-of-bounds error without terminating.
+// ptr: The pointer that caused the violation
+// base: The base address of the allocation
+// bound: The size of the allocation
+SANITIZER_INTERFACE_ATTRIBUTE void __lf_warn_oob(uptr ptr, uptr base,
+ uptr bound);
+
// Get the base address of an allocation from a pointer.
// Returns the base address, or 0 if the pointer is not within a LowFat region.
SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_base(uptr ptr);
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index a53767b1f5e76..9a4f326c66f26 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -18,8 +18,6 @@
#include "lf_interface.h"
#include "lf_config.h"
#include "sanitizer_common/sanitizer_common.h"
-#include "sanitizer_common/sanitizer_flags.h"
-#include "sanitizer_common/sanitizer_stacktrace.h"
using namespace __sanitizer;
@@ -144,35 +142,23 @@ void Deallocate(void *ptr) {
free_lists[region] = block;
}
-static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound) {
- Printf("=================================================================\n");
- Printf("==ERROR: LowFat: out-of-bounds access detected\n");
+static void PrintOobHeader(const char *level, uptr ptr, uptr base, uptr bound) {
+ Printf("%s: LowFat: out-of-bounds access detected\n", level);
Printf(" pointer: 0x%zx\n", ptr);
Printf(" base: 0x%zx\n", base);
Printf(" bound: %zu bytes\n", bound);
- Printf("=================================================================\n");
-
- // Print stack trace
- BufferedStackTrace stack;
- stack.Unwind(StackTrace::GetCurrentPc(), GET_CURRENT_FRAME(), nullptr,
- common_flags()->fast_unwind_on_fatal);
- stack.Print();
+}
+static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound) {
+ PrintOobHeader("ERROR", ptr, base, bound);
Die();
}
-} // namespace __lowfat
-
-namespace __sanitizer {
-void BufferedStackTrace::UnwindImpl(uptr pc, uptr bp, void *context,
- bool request_fast, u32 max_depth) {
- uptr top = 0;
- uptr bottom = 0;
- GetThreadStackTopAndBottom(false, &top, &bottom);
- bool fast = StackTrace::WillUseFastUnwind(request_fast);
- Unwind(max_depth, pc, bp, context, top, bottom, fast);
+static void PrintWarning(uptr ptr, uptr base, uptr bound) {
+ PrintOobHeader("WARNING", ptr, base, bound);
}
-} // namespace __sanitizer
+
+} // namespace __lowfat
// ---------------------- Interface Functions ----------------------
@@ -204,6 +190,11 @@ void __lf_report_oob(uptr ptr, uptr base, uptr bound) {
__lowfat::PrintErrorAndDie(ptr, base, bound);
}
+SANITIZER_INTERFACE_ATTRIBUTE
+void __lf_warn_oob(uptr ptr, uptr base, uptr bound) {
+ __lowfat::PrintWarning(ptr, base, bound);
+}
+
SANITIZER_INTERFACE_ATTRIBUTE
uptr __lf_get_base(uptr ptr) {
return __lowfat::GetBase(ptr);
diff --git a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
index 9a39b532a296d..3509937334423 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
@@ -14,7 +14,7 @@ namespace llvm {
class Module;
struct LowFatSanitizerOptions {
- // LowFat currently does not support any options.
+ bool Recover = false;
};
class LowFatSanitizerPass : public PassInfoMixin<LowFatSanitizerPass> {
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index fa31ef272ef2b..ad5d78435617e 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -983,9 +983,14 @@ Expected<HWAddressSanitizerOptions> parseHWASanPassOptions(StringRef Params) {
Expected<LowFatSanitizerOptions> parseLowFatPassOptions(StringRef Params) {
LowFatSanitizerOptions Result;
if (!Params.empty()) {
- return make_error<StringError>(
- formatv("invalid LowFatSanitizer pass parameter '{}'", Params).str(),
- inconvertibleErrorCode());
+ for (StringRef Param : llvm::split(Params, ';')) {
+ if (Param == "recover")
+ Result.Recover = true;
+ else
+ return make_error<StringError>(
+ formatv("invalid LowFatSanitizer pass parameter '{}'", Param).str(),
+ inconvertibleErrorCode());
+ }
}
return Result;
}
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index b21f7005de618..d99390503e9eb 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -46,9 +46,12 @@ class LowFatSanitizer {
bool run();
private:
- /// Get or create the declaration for __lf_report_oob.
+ /// Get or create the declaration for __lf_report_oob (fatal).
FunctionCallee getReportOobFn();
+ /// Get or create the declaration for __lf_warn_oob (non-fatal).
+ FunctionCallee getWarnOobFn();
+
/// Instrument a single function.
bool instrumentFunction(Function &F);
@@ -57,10 +60,11 @@ class LowFatSanitizer {
bool instrumentMemoryAccess(Instruction *I, Value *Ptr, Type *AccessTy);
Module &M;
- const LowFatSanitizerOptions &Options; // TODO: impelement options
+ const LowFatSanitizerOptions &Options;
const DataLayout &DL;
Type *IntptrTy;
FunctionCallee ReportOobFn;
+ FunctionCallee WarnOobFn;
};
// LowFat Constants (must match lf_config.h)
@@ -76,10 +80,25 @@ FunctionCallee LowFatSanitizer::getReportOobFn() {
ReportOobFn = M.getOrInsertFunction(
"__lf_report_oob",
FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy}, false));
+ if (auto *F = dyn_cast<Function>(ReportOobFn.getCallee()))
+ F->addFnAttr(Attribute::NoReturn);
}
return ReportOobFn;
}
+FunctionCallee LowFatSanitizer::getWarnOobFn() {
+ if (!WarnOobFn) {
+ // void __lf_warn_oob(uptr ptr, uptr base, uptr size)
+ Type *VoidTy = Type::getVoidTy(M.getContext());
+ WarnOobFn = M.getOrInsertFunction(
+ "__lf_warn_oob",
+ FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy}, false));
+ if (auto *F = dyn_cast<Function>(WarnOobFn.getCallee()))
+ F->addFnAttr(Attribute::NoUnwind);
+ }
+ return WarnOobFn;
+}
+
bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Type *AccessTy) {
// Skip if the access type size is not known at compile time
@@ -134,10 +153,13 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
IRBuilder<> OobIRB(OobTerm);
- // 5. Report error (slow path)
- OobIRB.CreateCall(getReportOobFn(), {PtrInt, Base, AllocSize});
+ // 5. Report OOB (slow path)
+ FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
+ OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize});
- LLVM_DEBUG(dbgs() << "[LowFat] Instrumented (inline): " << *I << "\n");
+ LLVM_DEBUG(dbgs() << "[LowFat] Instrumented (inline, "
+ << (Options.Recover ? "recover" : "fatal")
+ << "): " << *I << "\n");
return true;
}
>From 443b76974bf97244f08a75f781e114a5943daf88 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sun, 1 Mar 2026 19:50:06 +0800
Subject: [PATCH 17/55] [LowFat] Add spinlocks for thread safety on mem alloc
and dealloc
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 56 ++++++++++++++++---------------
1 file changed, 29 insertions(+), 27 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 9a4f326c66f26..e491d6818edb6 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -18,6 +18,7 @@
#include "lf_interface.h"
#include "lf_config.h"
#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_mutex.h"
using namespace __sanitizer;
@@ -43,6 +44,11 @@ struct FreeBlock {
};
static FreeBlock *free_lists[kNumSizeClasses];
+// Per-size-class spin mutexes protecting region_next_alloc and free_lists.
+// Using one lock per size class allows concurrent allocation across different
+// size classes, which is the common case in multi-threaded programs.
+static StaticSpinMutex region_locks[kNumSizeClasses];
+
static void InitRegionTable() {
for (uptr i = 0; i < kNumSizeClasses; i++) {
uptr size = SizeClassToSize(i);
@@ -79,20 +85,20 @@ static bool InitMemoryRegions() {
}
// Allocate from a LowFat region
-// First checks the free list, then falls back to bump allocation
+// First checks the free list, then falls back to bump allocation.
+// Thread-safe: protected by per-size-class spin mutex.
void *Allocate(uptr size) {
- // Printf("LowFat: allocating %zu bytes\n", size);
if (size == 0)
size = 1;
-
+
uptr class_index = SizeClassIndex(size);
- if (class_index >= kNumSizeClasses) {
- // Printf("LowFat: allocation size %zu exceeds max size class\n", size);
+ if (class_index >= kNumSizeClasses)
return nullptr;
- }
-
+
uptr alloc_size = SizeClassToSize(class_index);
-
+
+ SpinMutexLock lock(®ion_locks[class_index]);
+
// 1. Try free list first
FreeBlock *block = free_lists[class_index];
if (block) {
@@ -101,42 +107,38 @@ void *Allocate(uptr size) {
internal_memset(block, 0, alloc_size);
return (void *)block;
}
-
+
// 2. Fall back to bump allocation
uptr region_end = GetRegionStart(class_index) + kRegionSize;
uptr addr = region_next_alloc[class_index];
-
+
// Ensure alignment (should already be aligned)
addr = (addr + alloc_size - 1) & ~(alloc_size - 1);
-
- if (addr + alloc_size > region_end) {
- // Printf("LowFat: region %zu exhausted\n", class_index);
+
+ if (addr + alloc_size > region_end)
return nullptr;
- }
-
+
region_next_alloc[class_index] = addr + alloc_size;
return (void *)addr;
}
-// Free a LowFat allocation by adding it to the free list
+// Free a LowFat allocation by pushing it onto the free list.
+// Thread-safe: protected by per-size-class spin mutex.
void Deallocate(void *ptr) {
if (!ptr)
return;
-
+
uptr addr = (uptr)ptr;
- // Printf("LowFat: freeing ptr 0x%zx\n", addr);
-
+
// Validate this is a LowFat pointer
- if (!IsLowFatPointer(addr)) {
- // Printf("LowFat: __lf_free called on non-LowFat pointer 0x%zx\n", addr);
+ if (!IsLowFatPointer(addr))
return;
- }
-
+
uptr region = GetRegionIndex(addr);
- // Printf("LowFat: freed region %zu (size class %zu bytes)\n",
- // region, SizeClassToSize(region));
-
- // Add to the head of the free list for this size class
+
+ SpinMutexLock lock(®ion_locks[region]);
+
+ // Push to the head of the free list for this size class
FreeBlock *block = (FreeBlock *)ptr;
block->next = free_lists[region];
free_lists[region] = block;
>From 10fa2f419c5e042f4a930c24dbbcf9d772e9be14 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sun, 1 Mar 2026 20:25:39 +0800
Subject: [PATCH 18/55] [LowFat] Report write/read, overflow, and size on OOB
---
compiler-rt/lib/lowfat/lf_interface.h | 12 ++++---
compiler-rt/lib/lowfat/lf_rtl.cpp | 36 ++++++++++++-------
.../Instrumentation/LowFatSanitizer.cpp | 20 +++++++----
3 files changed, 44 insertions(+), 24 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
index aba0b9b4047c9..d21cf9062e3b3 100644
--- a/compiler-rt/lib/lowfat/lf_interface.h
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -27,15 +27,17 @@ SANITIZER_INTERFACE_ATTRIBUTE void __lf_init();
// ptr: The pointer that caused the violation
// base: The base address of the allocation
// bound: The size of the allocation
+// Report a fatal out-of-bounds access and terminate.
+// ptr: the offending pointer, base: base of the allocation,
+// bound: size of the allocation, is_write: 1=write 0=read
SANITIZER_INTERFACE_ATTRIBUTE void __lf_report_oob(uptr ptr, uptr base,
- uptr bound);
+ uptr bound, int is_write);
// Warn about an out-of-bounds error without terminating.
-// ptr: The pointer that caused the violation
-// base: The base address of the allocation
-// bound: The size of the allocation
+// ptr: the offending pointer, base: base of the allocation,
+// bound: size of the allocation, is_write: 1=write 0=read
SANITIZER_INTERFACE_ATTRIBUTE void __lf_warn_oob(uptr ptr, uptr base,
- uptr bound);
+ uptr bound, int is_write);
// Get the base address of an allocation from a pointer.
// Returns the base address, or 0 if the pointer is not within a LowFat region.
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index e491d6818edb6..59ee761f7bdd6 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -144,20 +144,30 @@ void Deallocate(void *ptr) {
free_lists[region] = block;
}
-static void PrintOobHeader(const char *level, uptr ptr, uptr base, uptr bound) {
- Printf("%s: LowFat: out-of-bounds access detected\n", level);
- Printf(" pointer: 0x%zx\n", ptr);
- Printf(" base: 0x%zx\n", base);
- Printf(" bound: %zu bytes\n", bound);
+static void PrintOobHeader(const char *level, uptr ptr, uptr base, uptr bound,
+ int is_write) {
+ // Compute the signed overflow: how many bytes past the end of the allocation
+ // the access reached. 0 means exactly at the boundary.
+ sptr overflow = (sptr)(ptr) - (sptr)(base + bound);
+ const char *op = is_write ? "write" : "read";
+
+ Printf("LOWFAT %s: out-of-bounds error detected!\n", level);
+ Printf(" operation = %s\n", op);
+ Printf(" pointer = 0x%zx (heap)\n", ptr);
+ Printf(" base = 0x%zx\n", base);
+ Printf(" size = %zu\n", bound);
+ const char *sign = (overflow >= 0) ? "+" : "";
+ Printf(" overflow = %s%zd\n", sign, (long)overflow);
+ Printf("\n");
}
-static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound) {
- PrintOobHeader("ERROR", ptr, base, bound);
+static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound, int is_write) {
+ PrintOobHeader("ERROR", ptr, base, bound, is_write);
Die();
}
-static void PrintWarning(uptr ptr, uptr base, uptr bound) {
- PrintOobHeader("WARNING", ptr, base, bound);
+static void PrintWarning(uptr ptr, uptr base, uptr bound, int is_write) {
+ PrintOobHeader("WARNING", ptr, base, bound, is_write);
}
} // namespace __lowfat
@@ -188,13 +198,13 @@ void __lf_init() {
}
SANITIZER_INTERFACE_ATTRIBUTE
-void __lf_report_oob(uptr ptr, uptr base, uptr bound) {
- __lowfat::PrintErrorAndDie(ptr, base, bound);
+void __lf_report_oob(uptr ptr, uptr base, uptr bound, int is_write) {
+ __lowfat::PrintErrorAndDie(ptr, base, bound, is_write);
}
SANITIZER_INTERFACE_ATTRIBUTE
-void __lf_warn_oob(uptr ptr, uptr base, uptr bound) {
- __lowfat::PrintWarning(ptr, base, bound);
+void __lf_warn_oob(uptr ptr, uptr base, uptr bound, int is_write) {
+ __lowfat::PrintWarning(ptr, base, bound, is_write);
}
SANITIZER_INTERFACE_ATTRIBUTE
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index d99390503e9eb..1e7ea2beea6c7 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -75,11 +75,12 @@ static const uint64_t NumSizeClasses = 27;
FunctionCallee LowFatSanitizer::getReportOobFn() {
if (!ReportOobFn) {
- // void __lf_report_oob(uptr ptr, uptr base, uptr size)
+ // void __lf_report_oob(uptr ptr, uptr base, uptr size, i8 is_write)
Type *VoidTy = Type::getVoidTy(M.getContext());
+ Type *I8Ty = Type::getInt8Ty(M.getContext());
ReportOobFn = M.getOrInsertFunction(
"__lf_report_oob",
- FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy}, false));
+ FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy, I8Ty}, false));
if (auto *F = dyn_cast<Function>(ReportOobFn.getCallee()))
F->addFnAttr(Attribute::NoReturn);
}
@@ -88,11 +89,12 @@ FunctionCallee LowFatSanitizer::getReportOobFn() {
FunctionCallee LowFatSanitizer::getWarnOobFn() {
if (!WarnOobFn) {
- // void __lf_warn_oob(uptr ptr, uptr base, uptr size)
+ // void __lf_warn_oob(uptr ptr, uptr base, uptr size, i8 is_write)
Type *VoidTy = Type::getVoidTy(M.getContext());
+ Type *I8Ty = Type::getInt8Ty(M.getContext());
WarnOobFn = M.getOrInsertFunction(
"__lf_warn_oob",
- FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy}, false));
+ FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy, I8Ty}, false));
if (auto *F = dyn_cast<Function>(WarnOobFn.getCallee()))
F->addFnAttr(Attribute::NoUnwind);
}
@@ -153,12 +155,18 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
IRBuilder<> OobIRB(OobTerm);
- // 5. Report OOB (slow path)
+ // 5. Report OOB (slow path) — pass is_write so the runtime can print
+ // "read" or "write" in the diagnostic.
FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
- OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize});
+ Type *I8Ty = Type::getInt8Ty(M.getContext());
+ bool IsWrite = isa<StoreInst>(I) || isa<AtomicRMWInst>(I) ||
+ isa<AtomicCmpXchgInst>(I);
+ Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
+ OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
LLVM_DEBUG(dbgs() << "[LowFat] Instrumented (inline, "
<< (Options.Recover ? "recover" : "fatal")
+ << ", " << (IsWrite ? "write" : "read")
<< "): " << *I << "\n");
return true;
}
>From 2605f0cf90f304b7f52538d132a94cb17aacf80c Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 2 Mar 2026 10:54:33 +0800
Subject: [PATCH 19/55] [LowFat] Temporary fix for lipo arch tag for x86_64h
---
compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake | 7 +------
compiler-rt/cmake/Modules/CompilerRTDarwinUtils.cmake | 7 +++++++
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
index 5fbf41a86bb4b..0015b0fffce81 100644
--- a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
+++ b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
@@ -122,12 +122,7 @@ set(ALL_XRAY_SUPPORTED_ARCH ${X86_64} ${ARM32} ${ARM64} ${MIPS32} ${MIPS64}
endif()
set(ALL_XRAY_DSO_SUPPORTED_ARCH ${X86_64} ${ARM64})
set(ALL_SHADOWCALLSTACK_SUPPORTED_ARCH ${ARM64})
-if(APPLE)
- # Exclude x86_64h: lipo cannot combine x86_64 and x86_64h into one fat binary.
- set(ALL_LOWFAT_SUPPORTED_ARCH x86_64 ${ARM64})
-else()
- set(ALL_LOWFAT_SUPPORTED_ARCH ${X86_64} ${ARM64})
-endif()
+set(ALL_LOWFAT_SUPPORTED_ARCH ${X86_64} ${ARM64})
if (UNIX)
if (OS_NAME MATCHES "Linux")
diff --git a/compiler-rt/cmake/Modules/CompilerRTDarwinUtils.cmake b/compiler-rt/cmake/Modules/CompilerRTDarwinUtils.cmake
index 069d76b78e10e..a387ef4a70ca2 100644
--- a/compiler-rt/cmake/Modules/CompilerRTDarwinUtils.cmake
+++ b/compiler-rt/cmake/Modules/CompilerRTDarwinUtils.cmake
@@ -144,6 +144,13 @@ function(darwin_test_archs os valid_archs)
endif()
endif()
+ # x86_64h maps to the same lipo architecture tag as x86_64, causing fat
+ # binary creation to fail when both are present. Sanitizer runtimes gain
+ # no benefit from Haswell-specific codegen, so drop it for macOS as well.
+ if(${os} STREQUAL "osx")
+ list(REMOVE_ITEM archs "x86_64h")
+ endif()
+
if(${os} MATCHES "^ios$")
message(STATUS "Disabling sanitizers armv7* slice for ios")
list(FILTER archs EXCLUDE REGEX "armv7.*")
>From e8df0187338892ca5210a809d11dc8b788a2da17 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 2 Mar 2026 11:17:45 +0800
Subject: [PATCH 20/55] [LowFat] Detect OOB for memcpy and memset
---
.../Instrumentation/LowFatSanitizer.cpp | 99 ++++++++++++++++++-
1 file changed, 94 insertions(+), 5 deletions(-)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 1e7ea2beea6c7..d33a3ede4259d 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -21,6 +21,7 @@
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InstIterator.h"
#include "llvm/IR/Instructions.h"
+#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Type.h"
#include "llvm/Support/Debug.h"
@@ -33,6 +34,7 @@ using namespace llvm;
STATISTIC(NumInstrumentedLoads, "Number of loads instrumented");
STATISTIC(NumInstrumentedStores, "Number of stores instrumented");
STATISTIC(NumInstrumentedAtomics, "Number of atomic operations instrumented");
+STATISTIC(NumInstrumentedMemIntrinsics, "Number of mem intrinsics instrumented");
namespace {
@@ -55,10 +57,16 @@ class LowFatSanitizer {
/// Instrument a single function.
bool instrumentFunction(Function &F);
- /// Instrument a memory access instruction.
+ /// Instrument a load/store/atomic with a compile-time-known access size.
/// Returns true if instrumentation was inserted.
bool instrumentMemoryAccess(Instruction *I, Value *Ptr, Type *AccessTy);
+ /// Instrument a mem intrinsic (memcpy/memset/memmove) with a runtime size.
+ /// Ptr is the pointer to check, SizeVal is the runtime length, IsWrite
+ /// indicates the direction. Returns true if instrumentation was inserted.
+ bool instrumentMemoryRange(Instruction *I, Value *Ptr, Value *SizeVal,
+ bool IsWrite);
+
Module &M;
const LowFatSanitizerOptions &Options;
const DataLayout &DL;
@@ -171,6 +179,64 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
return true;
}
+bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
+ Value *SizeVal, bool IsWrite) {
+ // Same bounds-checking logic as instrumentMemoryAccess, but uses a runtime
+ // SizeVal instead of a compile-time constant AccessSize.
+ IRBuilder<> IRB(I);
+
+ // Cast pointer and size to intptr
+ Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
+ // Zero-extend SizeVal to IntptrTy if needed (len may be i32 or i64)
+ Value *Size = IRB.CreateZExtOrTrunc(SizeVal, IntptrTy);
+
+ // 1. Get region index: (Ptr - RegionBase) >> RegionSizeLog
+ Value *RegionBaseVal = ConstantInt::get(IntptrTy, RegionBase);
+ Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
+ Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
+
+ // 2. Check if LowFat pointer: Region < NumSizeClasses
+ Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
+ Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+
+ // Split block for the LowFat slow path
+ Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
+ IRBuilder<> ThenIRB(ThenTerm);
+
+ // 3. Compute allocation size from region index: 1 << (region + MinSizeLog)
+ Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
+ Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
+ Value *SizeOne = ConstantInt::get(IntptrTy, 1);
+ Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
+
+ // 4. Compute base: Ptr & ~(AllocSize - 1)
+ Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
+ Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
+ Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
+
+ // 5. Compute end of allocation and end of access range
+ Value *AllocEnd = ThenIRB.CreateAdd(Base, AllocSize);
+ Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, Size);
+
+ // 6. OOB if access end exceeds allocation end
+ Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, AllocEnd);
+
+ // 7. Report OOB
+ Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
+ IRBuilder<> OobIRB(OobTerm);
+
+ FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
+ Type *I8Ty = Type::getInt8Ty(M.getContext());
+ Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
+ OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
+
+ LLVM_DEBUG(dbgs() << "[LowFat] Instrumented mem intrinsic ("
+ << (Options.Recover ? "recover" : "fatal")
+ << ", " << (IsWrite ? "write" : "read")
+ << "): " << *I << "\n");
+ return true;
+}
+
bool LowFatSanitizer::instrumentFunction(Function &F) {
// Skip functions that shouldn't be instrumented
if (F.isDeclaration())
@@ -188,9 +254,15 @@ bool LowFatSanitizer::instrumentFunction(Function &F) {
bool Modified = false;
- // Collect instructions to instrument first to avoid iterator invalidation
+ // Track two kinds of instrumentation targets:
+ // 1. Load/store/atomic: compile-time access size, from instruction type
+ // 2. Mem intrinsics: runtime access size (the 'len' argument)
SmallVector<std::pair<Instruction *, std::pair<Value *, Type *>>, 16> ToInstrument;
+ // Each MemRange entry is {I, Ptr, SizeVal, IsWrite}
+ struct MemRange { Instruction *I; Value *Ptr; Value *Size; bool IsWrite; };
+ SmallVector<MemRange, 8> MemRanges;
+
for (Instruction &I : instructions(F)) {
Value *Ptr = nullptr;
Type *AccessTy = nullptr;
@@ -215,14 +287,23 @@ bool LowFatSanitizer::instrumentFunction(Function &F) {
Ptr = AI->getPointerOperand();
AccessTy = AI->getCompareOperand()->getType();
}
+ } else if (auto *MI = dyn_cast<MemSetInst>(&I)) {
+ // memset(dst, val, len) — write-range check on dst
+ if (!MI->isVolatile())
+ MemRanges.push_back({&I, MI->getDest(), MI->getLength(), /*IsWrite=*/true});
+ } else if (auto *MI = dyn_cast<MemTransferInst>(&I)) {
+ // memcpy/memmove(dst, src, len) — write dst, read src
+ if (!MI->isVolatile()) {
+ MemRanges.push_back({&I, MI->getDest(), MI->getLength(), /*IsWrite=*/true});
+ MemRanges.push_back({&I, MI->getSource(), MI->getLength(), /*IsWrite=*/false});
+ }
}
- if (Ptr && AccessTy) {
+ if (Ptr && AccessTy)
ToInstrument.push_back({&I, {Ptr, AccessTy}});
- }
}
- // Now instrument collected instructions
+ // Instrument load/store/atomics
for (auto &Entry : ToInstrument) {
Instruction *I = Entry.first;
Value *Ptr = Entry.second.first;
@@ -239,6 +320,14 @@ bool LowFatSanitizer::instrumentFunction(Function &F) {
}
}
+ // Instrument mem intrinsics
+ for (auto &MR : MemRanges) {
+ if (instrumentMemoryRange(MR.I, MR.Ptr, MR.Size, MR.IsWrite)) {
+ Modified = true;
+ ++NumInstrumentedMemIntrinsics;
+ }
+ }
+
return Modified;
}
>From 6b381e613e54eef617c483dc02d1da65fbd5daff Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 2 Mar 2026 11:20:23 +0800
Subject: [PATCH 21/55] [LowFat] Fix OOB overflow sign by passing AccessEnd
---
llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index d33a3ede4259d..0f6d5d300e13e 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -228,7 +228,7 @@ bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
Type *I8Ty = Type::getInt8Ty(M.getContext());
Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
- OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
+ OobIRB.CreateCall(OobFn, {AccessEnd, Base, AllocSize, IsWriteVal});
LLVM_DEBUG(dbgs() << "[LowFat] Instrumented mem intrinsic ("
<< (Options.Recover ? "recover" : "fatal")
>From 0528297bace13a1cbfc2b120874a092982a3449c Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 12:38:51 +0800
Subject: [PATCH 22/55] [LowFat] Remove commented prints
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 59ee761f7bdd6..a6593fba796f5 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -181,16 +181,10 @@ void __lf_init() {
if (__lowfat::lowfat_inited)
return;
- // Printf("LowFat Sanitizer: initializing runtime\n");
-
__lowfat::InitRegionTable();
- if (!__lowfat::InitMemoryRegions()) {
- // Printf("LowFat Sanitizer: failed to initialize memory regions\n");
+ if (!__lowfat::InitMemoryRegions())
Die();
- }
-
- // Printf("LowFat Sanitizer: initialized runtime\n");
__lowfat::lowfat_inited = true;
>From dd45c7a8e9d18cd4f23bfa43b1b24a7ea2f6f243 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 13:20:29 +0800
Subject: [PATCH 23/55] [LowFat] Kill process with internal__exit instead of
Die()
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index a6593fba796f5..466bf0bdc4841 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -163,7 +163,7 @@ static void PrintOobHeader(const char *level, uptr ptr, uptr base, uptr bound,
static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound, int is_write) {
PrintOobHeader("ERROR", ptr, base, bound, is_write);
- Die();
+ internal__exit(1);
}
static void PrintWarning(uptr ptr, uptr base, uptr bound, int is_write) {
>From 6793bde50fe6d103bd8f7bafb6c4964f388491b4 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 13:22:07 +0800
Subject: [PATCH 24/55] [LowFat] Move LowFat to run before optimization
---
clang/lib/CodeGen/BackendUtil.cpp | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index ef576fb649e2d..2dad552af2928 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -768,12 +768,7 @@ static void addSanitizers(const Triple &TargetTriple,
MPM.addPass(DataFlowSanitizerPass(LangOpts.NoSanitizeFiles,
PB.getVirtualFileSystemPtr()));
}
-
- if (LangOpts.Sanitize.has(SanitizerKind::LowFat)) {
- LowFatSanitizerOptions Opts;
- Opts.Recover = CodeGenOpts.SanitizeRecover.has(SanitizerKind::LowFat);
- MPM.addPass(LowFatSanitizerPass(Opts));
- }
+ // TODO: move LowFat sanitizer back here.
};
if (ClSanitizeOnOptimizerEarlyEP) {
PB.registerOptimizerEarlyEPCallback(
@@ -791,6 +786,19 @@ static void addSanitizers(const Triple &TargetTriple,
// LastEP does not need GlobalsAA.
PB.registerOptimizerLastEPCallback(SanitizersCallback);
}
+
+ // LowFat must run BEFORE any optimization or attribute inference so that the
+ // memory accesses it needs to instrument are not eliminated by DCE/DSE first.
+ // OptimizerEarlyEP is too late (InferFunctionAttrs already ran); use
+ // PipelineStart which fires before any analysis or optimization pass.
+ if (LangOpts.Sanitize.has(SanitizerKind::LowFat)) {
+ LowFatSanitizerOptions LFOpts;
+ LFOpts.Recover = CodeGenOpts.SanitizeRecover.has(SanitizerKind::LowFat);
+ PB.registerPipelineStartEPCallback(
+ [LFOpts](ModulePassManager &MPM, OptimizationLevel) {
+ MPM.addPass(LowFatSanitizerPass(LFOpts));
+ });
+ }
}
void addLowerAllowCheckPass(const CodeGenOptions &CodeGenOpts,
>From 73a11fc6284409b29e7aa249e4b2ccfb6ba51b2c Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 13:27:32 +0800
Subject: [PATCH 25/55] [LowFat] Fix test with new error text
---
compiler-rt/test/lowfat/basic_inbounds.cpp | 7 +++++--
compiler-rt/test/lowfat/cross_boundary_oob.cpp | 2 +-
compiler-rt/test/lowfat/free_list_reuse.cpp | 11 ++++++-----
3 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/compiler-rt/test/lowfat/basic_inbounds.cpp b/compiler-rt/test/lowfat/basic_inbounds.cpp
index 05e6290614e12..9184c025878f6 100644
--- a/compiler-rt/test/lowfat/basic_inbounds.cpp
+++ b/compiler-rt/test/lowfat/basic_inbounds.cpp
@@ -4,11 +4,12 @@
// Verify that the LowFat runtime initializes and basic in-bounds
// allocations work without errors.
+#include <cstdio>
+
extern "C" void *__lf_malloc(unsigned long size);
extern "C" void __lf_free(void *ptr);
int main() {
- // CHECK: LowFat Sanitizer: initialized runtime
int *arr = (int *)__lf_malloc(10 * sizeof(int));
if (!arr)
return 1;
@@ -18,6 +19,8 @@ int main() {
arr[9] = 99;
__lf_free(arr);
- // CHECK-NOT: ERROR: LowFat
+ // CHECK: basic_inbounds: ok
+ // CHECK-NOT: LOWFAT ERROR
+ printf("basic_inbounds: ok\n");
return 0;
}
diff --git a/compiler-rt/test/lowfat/cross_boundary_oob.cpp b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
index 57dfa1434a032..3f1ec753b7efd 100644
--- a/compiler-rt/test/lowfat/cross_boundary_oob.cpp
+++ b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
@@ -19,7 +19,7 @@ int main() {
// Cross-boundary OOB: write 8 bytes at offset 12
// ptr + 8 = buf+20 > buf+16 → out of bounds
- // CHECK: ERROR: LowFat: out-of-bounds access detected
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
double *cross = (double *)(buf + 12);
*cross = 3.14;
diff --git a/compiler-rt/test/lowfat/free_list_reuse.cpp b/compiler-rt/test/lowfat/free_list_reuse.cpp
index 6460277ed5f81..27ab57e000eb1 100644
--- a/compiler-rt/test/lowfat/free_list_reuse.cpp
+++ b/compiler-rt/test/lowfat/free_list_reuse.cpp
@@ -1,25 +1,26 @@
// RUN: %clangxx_lowfat %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
-// Verify that allocation and free list reuse works correctly.
+// Verify that allocation and free list reuse works correctly, with no OOB errors.
+
+#include <cstdio>
extern "C" void *__lf_malloc(unsigned long size);
extern "C" void __lf_free(void *ptr);
int main() {
- // CHECK: LowFat Sanitizer: initialized runtime
-
// Allocate and free, then allocate again — should reuse from free list
int *a = (int *)__lf_malloc(10 * sizeof(int));
a[0] = 1;
__lf_free(a);
int *b = (int *)__lf_malloc(10 * sizeof(int));
- // After free list reuse, b should equal a (same address reused)
b[0] = 2;
b[9] = 3;
__lf_free(b);
- // CHECK-NOT: ERROR: LowFat
+ // CHECK: free_list_reuse: ok
+ // CHECK-NOT: LOWFAT ERROR
+ printf("free_list_reuse: ok\n");
return 0;
}
>From cb646b5000a917163107e6c7279fe63160e10408 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 13:30:26 +0800
Subject: [PATCH 26/55] [LowFat] Scaffold testing infrastructure
---
compiler-rt/test/lowfat/CMakeLists.txt | 31 ++++++++--
.../test/lowfat/inbounds_no_report.cpp | 33 +++++++++++
compiler-rt/test/lowfat/lit.cfg.py | 57 +++++++++++++++++++
compiler-rt/test/lowfat/lit.site.cfg.py.in | 13 +++++
compiler-rt/test/lowfat/memcpy_oob_fatal.cpp | 25 ++++++++
.../test/lowfat/memcpy_oob_recover.cpp | 30 ++++++++++
compiler-rt/test/lowfat/memset_oob_fatal.cpp | 22 +++++++
.../test/lowfat/memset_oob_recover.cpp | 27 +++++++++
compiler-rt/test/lowfat/oob_read_fatal.cpp | 26 +++++++++
9 files changed, 260 insertions(+), 4 deletions(-)
create mode 100644 compiler-rt/test/lowfat/inbounds_no_report.cpp
create mode 100644 compiler-rt/test/lowfat/lit.cfg.py
create mode 100644 compiler-rt/test/lowfat/lit.site.cfg.py.in
create mode 100644 compiler-rt/test/lowfat/memcpy_oob_fatal.cpp
create mode 100644 compiler-rt/test/lowfat/memcpy_oob_recover.cpp
create mode 100644 compiler-rt/test/lowfat/memset_oob_fatal.cpp
create mode 100644 compiler-rt/test/lowfat/memset_oob_recover.cpp
create mode 100644 compiler-rt/test/lowfat/oob_read_fatal.cpp
diff --git a/compiler-rt/test/lowfat/CMakeLists.txt b/compiler-rt/test/lowfat/CMakeLists.txt
index 651b543e51a02..089d35f63d163 100644
--- a/compiler-rt/test/lowfat/CMakeLists.txt
+++ b/compiler-rt/test/lowfat/CMakeLists.txt
@@ -1,11 +1,34 @@
set(LOWFAT_LIT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
-set(LOWFAT_LIT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR})
set(LOWFAT_TESTSUITES)
set(LOWFAT_TEST_DEPS ${SANITIZER_COMMON_LIT_TEST_DEPS})
+list(APPEND LOWFAT_TEST_DEPS lowfat)
-if(COMPILER_RT_HAS_LOWFAT)
- list(APPEND LOWFAT_TEST_DEPS lowfat)
+set(LOWFAT_TEST_ARCH ${LOWFAT_SUPPORTED_ARCH})
+if(APPLE)
+ darwin_filter_host_archs(LOWFAT_SUPPORTED_ARCH LOWFAT_TEST_ARCH)
endif()
-# TODO: add tests for lowfat
+foreach(arch ${LOWFAT_TEST_ARCH})
+ set(LOWFAT_TEST_TARGET_ARCH ${arch})
+ # Use COMPILER_RT_TEST_COMPILER directly — get_test_cc_for_arch only populates
+ # the CC output when cross-compiling; on Darwin native builds it returns empty.
+ set(LOWFAT_TEST_TARGET_CC ${COMPILER_RT_TEST_COMPILER})
+ get_test_cc_for_arch(${arch} _unused LOWFAT_TEST_TARGET_CFLAGS)
+ string(TOLOWER "-${arch}-${OS_NAME}" LOWFAT_TEST_CONFIG_SUFFIX)
+ string(TOUPPER ${arch} ARCH_UPPER_CASE)
+ set(CONFIG_NAME ${ARCH_UPPER_CASE}${OS_NAME}Config)
+
+ configure_lit_site_cfg(
+ ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in
+ ${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_NAME}/lit.site.cfg.py
+ MAIN_CONFIG "${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.py"
+ )
+ list(APPEND LOWFAT_TESTSUITES ${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_NAME})
+endforeach()
+
+add_lit_testsuite(check-lowfat "Running LowFat Sanitizer tests"
+ ${LOWFAT_TESTSUITES}
+ DEPENDS ${LOWFAT_TEST_DEPS}
+)
+set_target_properties(check-lowfat PROPERTIES FOLDER "Compiler-RT/Tests")
diff --git a/compiler-rt/test/lowfat/inbounds_no_report.cpp b/compiler-rt/test/lowfat/inbounds_no_report.cpp
new file mode 100644
index 0000000000000..0c4863e26a87d
--- /dev/null
+++ b/compiler-rt/test/lowfat/inbounds_no_report.cpp
@@ -0,0 +1,33 @@
+// RUN: %clangxx_lowfat %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+
+// Verify that purely in-bounds accesses — including memcpy and memset within
+// the allocation size — do not trigger any OOB report.
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+int main() {
+ char *buf = (char *)malloc(32);
+ if (!buf) return 1;
+
+ // Scalar: first and last byte.
+ buf[0] = 'A';
+ buf[31] = 'Z';
+
+ // memcpy exactly fits.
+ const char src[32] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+ memcpy(buf, src, 32);
+
+ // memset exactly fits.
+ memset(buf, 0, 32);
+
+ // CHECK: OK
+ // CHECK-NOT: ERROR: LowFat
+ // CHECK-NOT: WARNING: LowFat
+ printf("OK\n");
+
+ free(buf);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/lit.cfg.py b/compiler-rt/test/lowfat/lit.cfg.py
new file mode 100644
index 0000000000000..993b5ba162dc1
--- /dev/null
+++ b/compiler-rt/test/lowfat/lit.cfg.py
@@ -0,0 +1,57 @@
+import lit.formats
+import os
+
+# Setup config name.
+config.name = "LowFatSanitizer" + getattr(config, "name_suffix", "")
+
+# Setup source root.
+config.test_source_root = os.path.dirname(__file__)
+config.suffixes = [".c", ".cpp"]
+
+# Teach lit that these are shell tests (// RUN: ... lines).
+# When loaded via the build-dir site config, lit.common.configured sets this;
+# when invoked directly against the source dir, we must set it ourselves.
+if not hasattr(config, "test_format"):
+ config.test_format = lit.formats.ShTest(execute_external=True)
+
+
+# Find clang++:
+# 1. config.clang is set by lit.common.configured (when run via build-dir site config)
+# 2. config.llvm_tools_dir is set by lit.common.configured
+# 3. Fall back to bare "clang++" on PATH for direct source-dir invocations
+def _find_clang():
+ clang = getattr(config, "clang", None)
+ if clang:
+ return clang
+ tools_dir = getattr(config, "llvm_tools_dir", None)
+ if tools_dir:
+ candidate = os.path.join(tools_dir, "clang++")
+ if os.path.isfile(candidate):
+ return candidate
+ return "clang++"
+
+
+clang = _find_clang()
+
+
+def build_invocation(flags):
+ return " " + " ".join([clang] + flags) + " "
+
+
+# Fatal mode : terminate on first OOB
+lowfat_flags = ["-fsanitize=lowfat", "-O1"]
+# Recover mode: warn + continue
+lowfat_recover_flags = lowfat_flags + ["-fsanitize-recover=lowfat"]
+
+config.substitutions.append(("%clangxx_lowfat ", build_invocation(lowfat_flags)))
+config.substitutions.append(
+ ("%clangxx_lowfat_recover ", build_invocation(lowfat_recover_flags))
+)
+
+# Only Darwin and Linux are supported.
+if getattr(config, "target_os", "Unknown") not in ["Darwin", "Linux"]:
+ # When running directly (no site config), target_os may be unset; don't
+ # mark unsupported in that case — let the tests fail naturally if the
+ # platform truly isn't supported.
+ if hasattr(config, "target_os"):
+ config.unsupported = True
diff --git a/compiler-rt/test/lowfat/lit.site.cfg.py.in b/compiler-rt/test/lowfat/lit.site.cfg.py.in
new file mode 100644
index 0000000000000..1dbb2cb1367b6
--- /dev/null
+++ b/compiler-rt/test/lowfat/lit.site.cfg.py.in
@@ -0,0 +1,13 @@
+ at LIT_SITE_CFG_IN_HEADER@
+
+# Tool-specific config options.
+config.name_suffix = "@LOWFAT_TEST_CONFIG_SUFFIX@"
+config.target_cflags = "@LOWFAT_TEST_TARGET_CFLAGS@"
+config.clang = "@LOWFAT_TEST_TARGET_CC@"
+config.target_arch = "@LOWFAT_TEST_TARGET_ARCH@"
+
+# Load common config for all compiler-rt lit tests.
+lit_config.load_config(config, "@COMPILER_RT_BINARY_DIR@/test/lit.common.configured")
+
+# Load tool-specific config that would do the real work.
+lit_config.load_config(config, "@LOWFAT_LIT_SOURCE_DIR@/lit.cfg.py")
diff --git a/compiler-rt/test/lowfat/memcpy_oob_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_fatal.cpp
new file mode 100644
index 0000000000000..23f29460211fb
--- /dev/null
+++ b/compiler-rt/test/lowfat/memcpy_oob_fatal.cpp
@@ -0,0 +1,25 @@
+// RUN: %clangxx_lowfat %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// Verify that memcpy writing past the end of a LowFat allocation is detected
+// in fatal mode. The process must exit with a non-zero code (checked by
+// "not %run") and print the expected diagnostic.
+
+#include <cstdlib>
+#include <cstring>
+
+int main() {
+ char *dst = (char *)malloc(16);
+ char *guard = (char *)malloc(16); // keep adjacent memory mapped
+ if (!dst || !guard) return 1;
+
+ const char payload[32] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+
+ // memcpy of 32 bytes into a 16-byte allocation — overflows by 16 bytes.
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ memcpy(dst, payload, 32);
+
+ free(guard);
+ free(dst);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/memcpy_oob_recover.cpp b/compiler-rt/test/lowfat/memcpy_oob_recover.cpp
new file mode 100644
index 0000000000000..8257a6b1db46a
--- /dev/null
+++ b/compiler-rt/test/lowfat/memcpy_oob_recover.cpp
@@ -0,0 +1,30 @@
+// RUN: %clangxx_lowfat_recover %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+
+// Verify that memcpy OOB in recover mode:
+// 1. Prints a WARNING (not ERROR).
+// 2. Execution continues past the call (process exits 0).
+// 3. The reported overflow is positive (access end past allocation end).
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+int main() {
+ char *dst = (char *)malloc(16);
+ char *guard = (char *)malloc(16);
+ if (!dst || !guard) return 1;
+
+ const char payload[32] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+
+ // CHECK: LOWFAT WARNING: out-of-bounds error detected!
+ memcpy(dst, payload, 32);
+
+ // Execution must reach here in recover mode.
+ // CHECK: after memcpy
+ printf("after memcpy\n");
+
+ free(guard);
+ free(dst);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/memset_oob_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_fatal.cpp
new file mode 100644
index 0000000000000..aaa511dbfcb5d
--- /dev/null
+++ b/compiler-rt/test/lowfat/memset_oob_fatal.cpp
@@ -0,0 +1,22 @@
+// RUN: %clangxx_lowfat %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// Verify that memset writing past the end of a LowFat allocation is detected
+// in fatal mode.
+
+#include <cstdlib>
+#include <cstring>
+
+int main() {
+ char *dst = (char *)malloc(16);
+ char *guard = (char *)malloc(16); // keep adjacent memory mapped
+ if (!dst || !guard) return 1;
+
+ // memset of 32 bytes into a 16-byte allocation — overflows by 16 bytes.
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ memset(dst, 0, 32);
+
+ free(guard);
+ free(dst);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/memset_oob_recover.cpp b/compiler-rt/test/lowfat/memset_oob_recover.cpp
new file mode 100644
index 0000000000000..36d9d921fbe2e
--- /dev/null
+++ b/compiler-rt/test/lowfat/memset_oob_recover.cpp
@@ -0,0 +1,27 @@
+// RUN: %clangxx_lowfat_recover %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+
+// Verify that memset OOB in recover mode warns and continues.
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+int main() {
+ char *dst = (char *)malloc(16);
+ char *guard = (char *)malloc(16);
+ if (!dst || !guard) return 1;
+
+ // FIXME: memset with a compile-time constant ensures clang emits the llvm.memset
+ // intrinsic, which the LowFat pass instruments. A runtime size would become
+ // a plain libc call that the pass cannot see.
+ // CHECK: LOWFAT WARNING: out-of-bounds error detected!
+ memset(dst, 0, 32); // 32 bytes into a 16-byte allocation → OOB
+
+ // CHECK: after memset
+ printf("after memset\n");
+
+ free(guard);
+ free(dst);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/oob_read_fatal.cpp b/compiler-rt/test/lowfat/oob_read_fatal.cpp
new file mode 100644
index 0000000000000..6b8c33e4a0ab1
--- /dev/null
+++ b/compiler-rt/test/lowfat/oob_read_fatal.cpp
@@ -0,0 +1,26 @@
+// RUN: %clangxx_lowfat %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// Verify that an OOB load (scalar read across allocation boundary) is detected
+// in fatal mode.
+
+#include <cstdlib>
+
+int main() {
+ char *buf = (char *)malloc(32);
+ if (!buf) return 1;
+
+ buf[0] = 'H';
+ buf[31] = 'i';
+
+ // Read 8 bytes at offset 28 of a 32-byte allocation:
+ // bytes 28–35 exceed the 32-byte boundary → OOB.
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ // FIXME: must NOT use volatile here — the LowFat pass skips volatile accesses.
+ double *p = (double *)(buf + 28);
+ double val = *p; // 8-byte read at offset 28 of 32-byte alloc → OOB (bytes 28–35)
+ (void)val; // keep live to prevent DSE; crash fires on the load above
+
+ free(buf);
+ return 0;
+}
>From 5ccd44990782940ec8b6b319b075cef6cad3a5b8 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 13:42:44 +0800
Subject: [PATCH 27/55] [LowFat] Initialize flags to Die() on error and resolve
workaround
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 30 +++++++++++++++++++++++++++---
1 file changed, 27 insertions(+), 3 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 466bf0bdc4841..3977445520e28 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -15,9 +15,11 @@
//===----------------------------------------------------------------------===//
#include "lf_allocator.h"
-#include "lf_interface.h"
#include "lf_config.h"
+#include "lf_interface.h"
#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_flag_parser.h"
+#include "sanitizer_common/sanitizer_flags.h"
#include "sanitizer_common/sanitizer_mutex.h"
using namespace __sanitizer;
@@ -49,6 +51,26 @@ static FreeBlock *free_lists[kNumSizeClasses];
// size classes, which is the common case in multi-threaded programs.
static StaticSpinMutex region_locks[kNumSizeClasses];
+static void InitializeFlags() {
+ SetCommonFlagsDefaults();
+
+ {
+ CommonFlags cf;
+ cf.CopyFrom(*common_flags());
+ cf.exitcode = 1; // Fatal OOB exits with code 1 by default
+ cf.abort_on_error = false; // Use the exitcode path, not SIGABRT, so output is flushed before the process exits
+ OverrideCommonFlags(cf);
+ }
+
+ // Register all common flags with a parser and read LOWFAT_OPTIONS.
+ // Allow overriding flags at runtime, e.g.: LOWFAT_OPTIONS=exitcode=42:verbosity=1 ./my_program
+ FlagParser parser;
+ RegisterCommonFlags(&parser);
+ parser.ParseStringFromEnv("LOWFAT_OPTIONS");
+
+ InitializeCommonFlags();
+}
+
static void InitRegionTable() {
for (uptr i = 0; i < kNumSizeClasses; i++) {
uptr size = SizeClassToSize(i);
@@ -163,7 +185,7 @@ static void PrintOobHeader(const char *level, uptr ptr, uptr base, uptr bound,
static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound, int is_write) {
PrintOobHeader("ERROR", ptr, base, bound, is_write);
- internal__exit(1);
+ Die();
}
static void PrintWarning(uptr ptr, uptr base, uptr bound, int is_write) {
@@ -181,8 +203,10 @@ void __lf_init() {
if (__lowfat::lowfat_inited)
return;
+ __lowfat::InitializeFlags();
+
__lowfat::InitRegionTable();
-
+
if (!__lowfat::InitMemoryRegions())
Die();
>From 068ff0d5e8d279e98b8c7cb7ebe0d91cfac4bb99 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 14:02:45 +0800
Subject: [PATCH 28/55] [LowFat] Add tests for libc memcpy, memsetand memmove
---
.../test/lowfat/memcpy_oob_dynamic_fatal.cpp | 28 +++++++++++++++++++
..._fatal.cpp => memcpy_oob_static_fatal.cpp} | 0
...over.cpp => memcpy_oob_static_recover.cpp} | 0
.../test/lowfat/memmove_oob_dynamic_fatal.cpp | 25 +++++++++++++++++
.../test/lowfat/memset_oob_dynamic_fatal.cpp | 27 ++++++++++++++++++
..._fatal.cpp => memset_oob_static_fatal.cpp} | 0
...over.cpp => memset_oob_static_recover.cpp} | 6 ++--
7 files changed, 82 insertions(+), 4 deletions(-)
create mode 100644 compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
rename compiler-rt/test/lowfat/{memcpy_oob_fatal.cpp => memcpy_oob_static_fatal.cpp} (100%)
rename compiler-rt/test/lowfat/{memcpy_oob_recover.cpp => memcpy_oob_static_recover.cpp} (100%)
create mode 100644 compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
create mode 100644 compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
rename compiler-rt/test/lowfat/{memset_oob_fatal.cpp => memset_oob_static_fatal.cpp} (100%)
rename compiler-rt/test/lowfat/{memset_oob_recover.cpp => memset_oob_static_recover.cpp} (62%)
diff --git a/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
new file mode 100644
index 0000000000000..7e5d76df38dc6
--- /dev/null
+++ b/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
@@ -0,0 +1,28 @@
+// RUN: %clangxx_lowfat -fno-builtin-memcpy %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// Verify that memcpy writing past the end of a LowFat allocation is detected
+// in fatal mode, even when the size isn't a compile-time constant.
+
+#include <string.h>
+#include <stdlib.h>
+
+int main(int argc, char **argv) {
+ char *dst = (char *)malloc(16);
+ char *src = strdup("ABCDEFGHIJKLMNOPQRS"); // 19 bytes
+ if (!dst || !src) return 1;
+
+ // Use argc to prevent the optimizer from knowing the size at compile time.
+ // This forces a call to libc memcpy instead of llvm.memcpy.
+ size_t size = 16 + argc; // 17
+
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ // CHECK: operation = write
+ // CHECK: size = 16
+ // CHECK: overflow = +1
+ memcpy(dst, src, size);
+
+ free(dst);
+ free(src);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/memcpy_oob_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memcpy_oob_fatal.cpp
rename to compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
diff --git a/compiler-rt/test/lowfat/memcpy_oob_recover.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memcpy_oob_recover.cpp
rename to compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
diff --git a/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
new file mode 100644
index 0000000000000..9984b2b0bd8a4
--- /dev/null
+++ b/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
@@ -0,0 +1,25 @@
+// RUN: %clangxx_lowfat -fno-builtin-memmove %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// Verify that memmove writing past the end of a LowFat allocation is detected
+
+#include <string.h>
+#include <stdlib.h>
+
+int main(int argc, char **argv) {
+ char *dst = (char *)malloc(16);
+ char *src = dst + 4;
+ if (!dst) return 1;
+
+ // Use argc to prevent the optimizer from knowing the size at compile time.
+ size_t size = 12 + argc; // 13. dst+13 is within 16, but src+13 = dst+17, which is OOB (+1)
+
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ // CHECK: operation = read
+ // CHECK: size = 16
+ // CHECK: overflow = +1
+ memmove(dst, src, size);
+
+ free(dst);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
new file mode 100644
index 0000000000000..ea6f6083cdecc
--- /dev/null
+++ b/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
@@ -0,0 +1,27 @@
+// RUN: %clangxx_lowfat -fno-builtin-memset %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// Verify that memset writing past the end of a LowFat allocation is detected
+// in fatal mode, even when the size isn't a compile-time constant.
+
+#include <string.h>
+#include <stdlib.h>
+
+int main(int argc, char **argv) {
+ char *buf = (char *)malloc(16);
+ if (!buf) return 1;
+
+ // Use argc to prevent the optimizer from knowing the size at compile time.
+ // This forces a call to libc memset instead of llvm.memset, which proves
+ // our runtime interceptor handles it.
+ size_t size = 16 + argc; // 17
+
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ // CHECK: operation = write
+ // CHECK: size = 16
+ // CHECK: overflow = +1
+ memset(buf, 'A', size);
+
+ free(buf);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/memset_oob_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memset_oob_fatal.cpp
rename to compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
diff --git a/compiler-rt/test/lowfat/memset_oob_recover.cpp b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
similarity index 62%
rename from compiler-rt/test/lowfat/memset_oob_recover.cpp
rename to compiler-rt/test/lowfat/memset_oob_static_recover.cpp
index 36d9d921fbe2e..9932af6e6f703 100644
--- a/compiler-rt/test/lowfat/memset_oob_recover.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
@@ -12,11 +12,9 @@ int main() {
char *guard = (char *)malloc(16);
if (!dst || !guard) return 1;
- // FIXME: memset with a compile-time constant ensures clang emits the llvm.memset
- // intrinsic, which the LowFat pass instruments. A runtime size would become
- // a plain libc call that the pass cannot see.
+ // memset of 32 bytes into a 16-byte allocation — overflows by 16 bytes.
// CHECK: LOWFAT WARNING: out-of-bounds error detected!
- memset(dst, 0, 32); // 32 bytes into a 16-byte allocation → OOB
+ memset(dst, 0, 32);
// CHECK: after memset
printf("after memset\n");
>From 7e539bf3091b65156610b700afdea5d8be290931 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 15:11:49 +0800
Subject: [PATCH 29/55] [LowFat] Run tests with 3 optimization levels
---
compiler-rt/test/lowfat/basic_inbounds.cpp | 5 ++++-
compiler-rt/test/lowfat/cross_boundary_oob.cpp | 5 ++++-
compiler-rt/test/lowfat/free_list_reuse.cpp | 5 ++++-
compiler-rt/test/lowfat/inbounds_no_report.cpp | 5 ++++-
compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp | 5 ++++-
compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp | 5 ++++-
compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp | 5 ++++-
compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp | 5 ++++-
compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp | 5 ++++-
compiler-rt/test/lowfat/memset_oob_static_fatal.cpp | 5 ++++-
compiler-rt/test/lowfat/memset_oob_static_recover.cpp | 5 ++++-
compiler-rt/test/lowfat/oob_read_fatal.cpp | 5 ++++-
12 files changed, 48 insertions(+), 12 deletions(-)
diff --git a/compiler-rt/test/lowfat/basic_inbounds.cpp b/compiler-rt/test/lowfat/basic_inbounds.cpp
index 9184c025878f6..6ea912984b57a 100644
--- a/compiler-rt/test/lowfat/basic_inbounds.cpp
+++ b/compiler-rt/test/lowfat/basic_inbounds.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat %s -o %t
+// RUN: %clangxx_lowfat -O0 %s -o %t
+// RUN: %clangxx_lowfat -O1 %s -o %t
+// RUN: %clangxx_lowfat -O2 %s -o %t
+// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
// Verify that the LowFat runtime initializes and basic in-bounds
diff --git a/compiler-rt/test/lowfat/cross_boundary_oob.cpp b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
index 3f1ec753b7efd..46f428f3a95f7 100644
--- a/compiler-rt/test/lowfat/cross_boundary_oob.cpp
+++ b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat %s -o %t
+// RUN: %clangxx_lowfat -O0 %s -o %t
+// RUN: %clangxx_lowfat -O1 %s -o %t
+// RUN: %clangxx_lowfat -O2 %s -o %t
+// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// Verify that a cross-boundary out-of-bounds access is detected.
diff --git a/compiler-rt/test/lowfat/free_list_reuse.cpp b/compiler-rt/test/lowfat/free_list_reuse.cpp
index 27ab57e000eb1..2384eab006362 100644
--- a/compiler-rt/test/lowfat/free_list_reuse.cpp
+++ b/compiler-rt/test/lowfat/free_list_reuse.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat %s -o %t
+// RUN: %clangxx_lowfat -O0 %s -o %t
+// RUN: %clangxx_lowfat -O1 %s -o %t
+// RUN: %clangxx_lowfat -O2 %s -o %t
+// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
// Verify that allocation and free list reuse works correctly, with no OOB errors.
diff --git a/compiler-rt/test/lowfat/inbounds_no_report.cpp b/compiler-rt/test/lowfat/inbounds_no_report.cpp
index 0c4863e26a87d..72b0fb07c63ec 100644
--- a/compiler-rt/test/lowfat/inbounds_no_report.cpp
+++ b/compiler-rt/test/lowfat/inbounds_no_report.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat %s -o %t
+// RUN: %clangxx_lowfat -O0 %s -o %t
+// RUN: %clangxx_lowfat -O1 %s -o %t
+// RUN: %clangxx_lowfat -O2 %s -o %t
+// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
// Verify that purely in-bounds accesses — including memcpy and memset within
diff --git a/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
index 7e5d76df38dc6..db8c65f25b918 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat -fno-builtin-memcpy %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memcpy -O0 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memcpy -O1 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memcpy -O2 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memcpy -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// Verify that memcpy writing past the end of a LowFat allocation is detected
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
index 23f29460211fb..1555fbe70bf4c 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat %s -o %t
+// RUN: %clangxx_lowfat -O0 %s -o %t
+// RUN: %clangxx_lowfat -O1 %s -o %t
+// RUN: %clangxx_lowfat -O2 %s -o %t
+// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// Verify that memcpy writing past the end of a LowFat allocation is detected
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
index 8257a6b1db46a..5a6b0ac647af0 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat_recover %s -o %t
+// RUN: %clangxx_lowfat_recover -O0 %s -o %t
+// RUN: %clangxx_lowfat_recover -O1 %s -o %t
+// RUN: %clangxx_lowfat_recover -O2 %s -o %t
+// RUN: %clangxx_lowfat_recover -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
// Verify that memcpy OOB in recover mode:
diff --git a/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
index 9984b2b0bd8a4..43b23a8ffca20 100644
--- a/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
+++ b/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat -fno-builtin-memmove %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memmove -O0 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memmove -O1 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memmove -O2 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memmove -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// Verify that memmove writing past the end of a LowFat allocation is detected
diff --git a/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
index ea6f6083cdecc..2f78d67e7577b 100644
--- a/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat -fno-builtin-memset %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memset -O0 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memset -O1 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memset -O2 %s -o %t
+// RUN: %clangxx_lowfat -fno-builtin-memset -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// Verify that memset writing past the end of a LowFat allocation is detected
diff --git a/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
index aaa511dbfcb5d..28e72de61f4b5 100644
--- a/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat %s -o %t
+// RUN: %clangxx_lowfat -O0 %s -o %t
+// RUN: %clangxx_lowfat -O1 %s -o %t
+// RUN: %clangxx_lowfat -O2 %s -o %t
+// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// Verify that memset writing past the end of a LowFat allocation is detected
diff --git a/compiler-rt/test/lowfat/memset_oob_static_recover.cpp b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
index 9932af6e6f703..ad4fbee318e13 100644
--- a/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat_recover %s -o %t
+// RUN: %clangxx_lowfat_recover -O0 %s -o %t
+// RUN: %clangxx_lowfat_recover -O1 %s -o %t
+// RUN: %clangxx_lowfat_recover -O2 %s -o %t
+// RUN: %clangxx_lowfat_recover -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
// Verify that memset OOB in recover mode warns and continues.
diff --git a/compiler-rt/test/lowfat/oob_read_fatal.cpp b/compiler-rt/test/lowfat/oob_read_fatal.cpp
index 6b8c33e4a0ab1..38d67e1ecbf0c 100644
--- a/compiler-rt/test/lowfat/oob_read_fatal.cpp
+++ b/compiler-rt/test/lowfat/oob_read_fatal.cpp
@@ -1,4 +1,7 @@
-// RUN: %clangxx_lowfat %s -o %t
+// RUN: %clangxx_lowfat -O0 %s -o %t
+// RUN: %clangxx_lowfat -O1 %s -o %t
+// RUN: %clangxx_lowfat -O2 %s -o %t
+// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
// Verify that an OOB load (scalar read across allocation boundary) is detected
>From fff71f47b1394d065a1ca0a1f97b8efc96ae7cb1 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 3 Mar 2026 15:13:25 +0800
Subject: [PATCH 30/55] [LowFat] Add libc interceptors for memset, memcopy and
memmove
---
compiler-rt/lib/lowfat/lf_interceptors.cpp | 38 +++++++++++++++++++---
1 file changed, 33 insertions(+), 5 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp
index 8d96b8d88ef0b..67186a8f28f9d 100644
--- a/compiler-rt/lib/lowfat/lf_interceptors.cpp
+++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp
@@ -14,6 +14,7 @@
#include "interception/interception.h"
#include "lf_allocator.h"
#include "lf_config.h"
+#include "lf_interface.h"
#include "sanitizer_common/sanitizer_allocator_dlsym.h"
using namespace __sanitizer;
@@ -143,6 +144,35 @@ INTERCEPTOR(int, posix_memalign, void **memptr, uptr alignment, uptr size) {
return REAL(posix_memalign)(memptr, alignment, size);
}
+static inline void check_bounds(const void *ptr, uptr access_size, int is_write) {
+ if (!ptr || access_size == 0) return;
+ if (!__lowfat::CheckBounds((uptr)ptr, access_size)) {
+ uptr start = (uptr)ptr;
+ uptr size = __lowfat::GetSize(start);
+ uptr base = __lowfat::GetBase(start);
+ __lf_report_oob(start + access_size, base, size, is_write);
+ }
+}
+
+// The compiler pass instruments direct memory accesses inline, but cannot
+// instrument external libc calls. We intercept them here to check bounds.
+INTERCEPTOR(void *, memset, void *dst, int v, uptr size) {
+ check_bounds(dst, size, 1 /* write */);
+ return REAL(memset)(dst, v, size);
+}
+
+INTERCEPTOR(void *, memcpy, void *dst, const void *src, uptr size) {
+ check_bounds(dst, size, 1 /* write */);
+ check_bounds(src, size, 0 /* read */);
+ return REAL(memcpy)(dst, src, size);
+}
+
+INTERCEPTOR(void *, memmove, void *dst, const void *src, uptr size) {
+ check_bounds(dst, size, 1 /* write */);
+ check_bounds(src, size, 0 /* read */);
+ return REAL(memmove)(dst, src, size);
+}
+
#if SANITIZER_APPLE
INTERCEPTOR(uptr, malloc_size, void *ptr) {
if (DlsymAlloc::PointerIsMine(ptr))
@@ -153,10 +183,6 @@ INTERCEPTOR(uptr, malloc_size, void *ptr) {
}
#endif
-//===----------------------------------------------------------------------===//
-// Initialization
-//===----------------------------------------------------------------------===//
-
namespace __lowfat {
void InitializeInterceptors() {
static int inited = 0;
@@ -168,10 +194,12 @@ void InitializeInterceptors() {
INTERCEPT_FUNCTION(realloc);
INTERCEPT_FUNCTION(valloc);
INTERCEPT_FUNCTION(posix_memalign);
+ INTERCEPT_FUNCTION(memset);
+ INTERCEPT_FUNCTION(memcpy);
+ INTERCEPT_FUNCTION(memmove);
#if SANITIZER_APPLE
INTERCEPT_FUNCTION(malloc_size);
#endif
-
inited = 1;
}
} // namespace __lowfat
>From f2cace424d8599550a5b8971c7b966554e035b64 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 4 Mar 2026 09:52:34 +0800
Subject: [PATCH 31/55] [LowFat] Create Safe to combat DAE and DCE
---
clang/lib/CodeGen/BackendUtil.cpp | 36 ++-
compiler-rt/lib/lowfat/lf_interface.h | 5 +
.../Instrumentation/LowFatSanitizer.h | 8 +
.../Instrumentation/LowFatSanitizer.cpp | 268 +++++++-----------
4 files changed, 147 insertions(+), 170 deletions(-)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 2dad552af2928..2aff1a03c6e5d 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -108,6 +108,15 @@ static cl::opt<bool> ClSanitizeOnOptimizerEarlyEP(
"sanitizer-early-opt-ep", cl::Optional,
cl::desc("Insert sanitizers on OptimizerEarlyEP."));
+static cl::opt<LowFatSanitizerOptions::LowFatMode> LowFatMode(
+ "lowfat-mode", cl::init(LowFatSanitizerOptions::LowFatMode::Fast),
+ cl::desc("Controls the placement and strictness of the LowFat pass"),
+ cl::values(
+ clEnumValN(LowFatSanitizerOptions::LowFatMode::Fast, "fast",
+ "Instrument at OptimizerLastEP (least overhead)"),
+ clEnumValN(LowFatSanitizerOptions::LowFatMode::Safe, "safe",
+ "Barrier at PipelineStartEP + instrument at OptimizerLastEP")));
+
// Experiment to mark cold functions as optsize/minsize/optnone.
// TODO: remove once this is exposed as a proper driver flag.
static cl::opt<PGOOptions::ColdFuncOpt> ClPGOColdFuncAttr(
@@ -768,7 +777,12 @@ static void addSanitizers(const Triple &TargetTriple,
MPM.addPass(DataFlowSanitizerPass(LangOpts.NoSanitizeFiles,
PB.getVirtualFileSystemPtr()));
}
- // TODO: move LowFat sanitizer back here.
+ if (LangOpts.Sanitize.has(SanitizerKind::LowFat)) {
+ LowFatSanitizerOptions LFOpts;
+ LFOpts.Recover = CodeGenOpts.SanitizeRecover.has(SanitizerKind::LowFat);
+ LFOpts.Mode = LowFatMode;
+ MPM.addPass(LowFatSanitizerPass(LFOpts));
+ }
};
if (ClSanitizeOnOptimizerEarlyEP) {
PB.registerOptimizerEarlyEPCallback(
@@ -787,17 +801,21 @@ static void addSanitizers(const Triple &TargetTriple,
PB.registerOptimizerLastEPCallback(SanitizersCallback);
}
- // LowFat must run BEFORE any optimization or attribute inference so that the
- // memory accesses it needs to instrument are not eliminated by DCE/DSE first.
- // OptimizerEarlyEP is too late (InferFunctionAttrs already ran); use
- // PipelineStart which fires before any analysis or optimization pass.
if (LangOpts.Sanitize.has(SanitizerKind::LowFat)) {
LowFatSanitizerOptions LFOpts;
LFOpts.Recover = CodeGenOpts.SanitizeRecover.has(SanitizerKind::LowFat);
- PB.registerPipelineStartEPCallback(
- [LFOpts](ModulePassManager &MPM, OptimizationLevel) {
- MPM.addPass(LowFatSanitizerPass(LFOpts));
- });
+ LFOpts.Mode = LowFatMode;
+
+ if (LFOpts.Mode == LowFatSanitizerOptions::LowFatMode::Safe) {
+ // Safe: insert barrier + fake.use at PipelineStartEP to preserve loads
+ // through Dead Argument Elimination, then instrument at OptimizerLastEP.
+ LowFatSanitizerOptions BarrierOpts = LFOpts;
+ BarrierOpts.InternalBarrierOnly_ = true;
+ PB.registerPipelineStartEPCallback(
+ [BarrierOpts](ModulePassManager &MPM, OptimizationLevel) {
+ MPM.addPass(LowFatSanitizerPass(BarrierOpts));
+ });
+ }
}
}
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
index d21cf9062e3b3..63577acf7d782 100644
--- a/compiler-rt/lib/lowfat/lf_interface.h
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -47,6 +47,11 @@ SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_base(uptr ptr);
// Returns the allocation size, or 0 if the pointer is not within a LowFat region.
SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_size(uptr ptr);
+// Allocate/Deallocate from LowFat regions.
+SANITIZER_INTERFACE_ATTRIBUTE void *__lf_malloc(uptr size);
+SANITIZER_INTERFACE_ATTRIBUTE void __lf_free(void *ptr);
+
+
} // extern "C"
#endif // LF_INTERFACE_H
diff --git a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
index 3509937334423..2403aa9da2430 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
@@ -15,6 +15,14 @@ class Module;
struct LowFatSanitizerOptions {
bool Recover = false;
+
+ enum class LowFatMode {
+ Fast, /// instrument at OptimizerLastEP
+ Safe, /// Barrier at PipelineStartEP + instrument at OptimizerLastEP
+ };
+ LowFatMode Mode = LowFatMode::Fast;
+
+ bool InternalBarrierOnly_ = false;
};
class LowFatSanitizerPass : public PassInfoMixin<LowFatSanitizerPass> {
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 0f6d5d300e13e..94587b5435eaf 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -19,12 +19,13 @@
#include "llvm/IR/DataLayout.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
-#include "llvm/IR/InstIterator.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IntrinsicInst.h"
+#include "llvm/IR/Intrinsics.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Type.h"
#include "llvm/Support/Debug.h"
+#include "llvm/Support/ModRef.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
using namespace llvm;
@@ -48,39 +49,29 @@ class LowFatSanitizer {
bool run();
private:
- /// Get or create the declaration for __lf_report_oob (fatal).
- FunctionCallee getReportOobFn();
+ Module &M;
+ const LowFatSanitizerOptions &Options;
+ const DataLayout &DL;
+ Type *IntptrTy;
+
+ FunctionCallee ReportOobFn = nullptr;
+ FunctionCallee WarnOobFn = nullptr;
- /// Get or create the declaration for __lf_warn_oob (non-fatal).
+ FunctionCallee getReportOobFn();
FunctionCallee getWarnOobFn();
- /// Instrument a single function.
bool instrumentFunction(Function &F);
-
- /// Instrument a load/store/atomic with a compile-time-known access size.
- /// Returns true if instrumentation was inserted.
bool instrumentMemoryAccess(Instruction *I, Value *Ptr, Type *AccessTy);
-
- /// Instrument a mem intrinsic (memcpy/memset/memmove) with a runtime size.
- /// Ptr is the pointer to check, SizeVal is the runtime length, IsWrite
- /// indicates the direction. Returns true if instrumentation was inserted.
- bool instrumentMemoryRange(Instruction *I, Value *Ptr, Value *SizeVal,
+ bool instrumentMemoryRange(Instruction *I, Value *Ptr, Value *Size,
bool IsWrite);
- Module &M;
- const LowFatSanitizerOptions &Options;
- const DataLayout &DL;
- Type *IntptrTy;
- FunctionCallee ReportOobFn;
- FunctionCallee WarnOobFn;
+ // Constants (must match lf_config.h)
+ static constexpr uint64_t RegionBase = 0x100000000000ULL;
+ static constexpr uint64_t RegionSizeLog = 32;
+ static constexpr uint64_t NumSizeClasses = 27; // kMaxSizeLog(30) - kMinSizeLog(4) + 1
+ static constexpr uint64_t MinSizeLog = 4;
};
-// LowFat Constants (must match lf_config.h)
-static const uint64_t RegionBase = 0x100000000000ULL;
-static const uint64_t RegionSizeLog = 32;
-static const uint64_t MinSizeLog = 4;
-static const uint64_t NumSizeClasses = 27;
-
FunctionCallee LowFatSanitizer::getReportOobFn() {
if (!ReportOobFn) {
// void __lf_report_oob(uptr ptr, uptr base, uptr size, i8 is_write)
@@ -89,8 +80,12 @@ FunctionCallee LowFatSanitizer::getReportOobFn() {
ReportOobFn = M.getOrInsertFunction(
"__lf_report_oob",
FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy, I8Ty}, false));
- if (auto *F = dyn_cast<Function>(ReportOobFn.getCallee()))
+ if (auto *F = dyn_cast<Function>(ReportOobFn.getCallee())) {
F->addFnAttr(Attribute::NoReturn);
+ // inaccessibleMemOnly: prevents the branch from being eliminated
+ // as "dead" if the optimizer can't prove OOB is impossible.
+ F->setMemoryEffects(MemoryEffects::inaccessibleMemOnly());
+ }
}
return ReportOobFn;
}
@@ -103,24 +98,21 @@ FunctionCallee LowFatSanitizer::getWarnOobFn() {
WarnOobFn = M.getOrInsertFunction(
"__lf_warn_oob",
FunctionType::get(VoidTy, {IntptrTy, IntptrTy, IntptrTy, I8Ty}, false));
- if (auto *F = dyn_cast<Function>(WarnOobFn.getCallee()))
+ if (auto *F = dyn_cast<Function>(WarnOobFn.getCallee())) {
F->addFnAttr(Attribute::NoUnwind);
+ F->setMemoryEffects(MemoryEffects::inaccessibleMemOnly());
+ }
}
return WarnOobFn;
}
bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Type *AccessTy) {
- // Skip if the access type size is not known at compile time
TypeSize AccessSize = DL.getTypeStoreSize(AccessTy);
- if (AccessSize.isScalable()) {
- LLVM_DEBUG(dbgs() << "[LowFat] Skipping scalable type access\n");
+ if (AccessSize.isScalable())
return false;
- }
IRBuilder<> IRB(I);
-
- // Convert pointer to integer
Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
// 1. Get region index: (Ptr - RegionBase) >> RegionSizeLog
@@ -132,39 +124,27 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
- // Split block for the slow path (if LowFat)
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
// 3. Compute bounds inside the 'then' block
- // Size = 1 << (Region + MinSizeLog)
Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
Value *SizeOne = ConstantInt::get(IntptrTy, 1);
Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
-
- // Mask = ~(AllocSize - 1)
Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
-
- // Base = Ptr & Mask
Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
-
- // End = Base + AllocSize
Value *End = ThenIRB.CreateAdd(Base, AllocSize);
// 4. Check access: Ptr + AccessSize <= End
- // Equivalent OOB check: (Ptr + AccessSize) > End
Value *AccessSizeVal = ConstantInt::get(IntptrTy, AccessSize.getFixedValue());
Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, AccessSizeVal);
Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
- // Split again for OOB reporting
Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
IRBuilder<> OobIRB(OobTerm);
- // 5. Report OOB (slow path) — pass is_write so the runtime can print
- // "read" or "write" in the diagnostic.
FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
Type *I8Ty = Type::getInt8Ty(M.getContext());
bool IsWrite = isa<StoreInst>(I) || isa<AtomicRMWInst>(I) ||
@@ -172,174 +152,140 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
- LLVM_DEBUG(dbgs() << "[LowFat] Instrumented (inline, "
- << (Options.Recover ? "recover" : "fatal")
- << ", " << (IsWrite ? "write" : "read")
- << "): " << *I << "\n");
+ if (isa<LoadInst>(I)) NumInstrumentedLoads++;
+ else if (isa<StoreInst>(I)) NumInstrumentedStores++;
+ else NumInstrumentedAtomics++;
+
return true;
}
bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
- Value *SizeVal, bool IsWrite) {
- // Same bounds-checking logic as instrumentMemoryAccess, but uses a runtime
- // SizeVal instead of a compile-time constant AccessSize.
+ Value *Size, bool IsWrite) {
IRBuilder<> IRB(I);
-
- // Cast pointer and size to intptr
Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
- // Zero-extend SizeVal to IntptrTy if needed (len may be i32 or i64)
- Value *Size = IRB.CreateZExtOrTrunc(SizeVal, IntptrTy);
+ Value *SizeInt = IRB.CreateZExtOrTrunc(Size, IntptrTy);
- // 1. Get region index: (Ptr - RegionBase) >> RegionSizeLog
Value *RegionBaseVal = ConstantInt::get(IntptrTy, RegionBase);
Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
- // 2. Check if LowFat pointer: Region < NumSizeClasses
Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
- // Split block for the LowFat slow path
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
- // 3. Compute allocation size from region index: 1 << (region + MinSizeLog)
Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
Value *SizeOne = ConstantInt::get(IntptrTy, 1);
Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
-
- // 4. Compute base: Ptr & ~(AllocSize - 1)
Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
+ Value *End = ThenIRB.CreateAdd(Base, AllocSize);
- // 5. Compute end of allocation and end of access range
- Value *AllocEnd = ThenIRB.CreateAdd(Base, AllocSize);
- Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, Size);
-
- // 6. OOB if access end exceeds allocation end
- Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, AllocEnd);
+ Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, SizeInt);
+ Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
- // 7. Report OOB
Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
IRBuilder<> OobIRB(OobTerm);
-
+
FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
Type *I8Ty = Type::getInt8Ty(M.getContext());
Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
- OobIRB.CreateCall(OobFn, {AccessEnd, Base, AllocSize, IsWriteVal});
+ OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
- LLVM_DEBUG(dbgs() << "[LowFat] Instrumented mem intrinsic ("
- << (Options.Recover ? "recover" : "fatal")
- << ", " << (IsWrite ? "write" : "read")
- << "): " << *I << "\n");
+ NumInstrumentedMemIntrinsics++;
return true;
}
bool LowFatSanitizer::instrumentFunction(Function &F) {
- // Skip functions that shouldn't be instrumented
- if (F.isDeclaration())
- return false;
-
- // Skip the runtime library functions themselves
- if (F.getName().starts_with("__lf_"))
- return false;
-
- // Skip functions with nosanitize attribute
- if (F.hasFnAttribute(Attribute::NoSanitizeBounds))
- return false;
-
- LLVM_DEBUG(dbgs() << "[LowFat] Instrumenting function: " << F.getName() << "\n");
-
bool Modified = false;
-
- // Track two kinds of instrumentation targets:
- // 1. Load/store/atomic: compile-time access size, from instruction type
- // 2. Mem intrinsics: runtime access size (the 'len' argument)
- SmallVector<std::pair<Instruction *, std::pair<Value *, Type *>>, 16> ToInstrument;
-
- // Each MemRange entry is {I, Ptr, SizeVal, IsWrite}
- struct MemRange { Instruction *I; Value *Ptr; Value *Size; bool IsWrite; };
- SmallVector<MemRange, 8> MemRanges;
-
- for (Instruction &I : instructions(F)) {
- Value *Ptr = nullptr;
- Type *AccessTy = nullptr;
-
- if (auto *LI = dyn_cast<LoadInst>(&I)) {
- if (!LI->isVolatile()) {
- Ptr = LI->getPointerOperand();
- AccessTy = LI->getType();
- }
- } else if (auto *SI = dyn_cast<StoreInst>(&I)) {
- if (!SI->isVolatile()) {
- Ptr = SI->getPointerOperand();
- AccessTy = SI->getValueOperand()->getType();
- }
- } else if (auto *AI = dyn_cast<AtomicRMWInst>(&I)) {
- if (!AI->isVolatile()) {
- Ptr = AI->getPointerOperand();
- AccessTy = AI->getValOperand()->getType();
- }
- } else if (auto *AI = dyn_cast<AtomicCmpXchgInst>(&I)) {
- if (!AI->isVolatile()) {
- Ptr = AI->getPointerOperand();
- AccessTy = AI->getCompareOperand()->getType();
- }
- } else if (auto *MI = dyn_cast<MemSetInst>(&I)) {
- // memset(dst, val, len) — write-range check on dst
- if (!MI->isVolatile())
- MemRanges.push_back({&I, MI->getDest(), MI->getLength(), /*IsWrite=*/true});
- } else if (auto *MI = dyn_cast<MemTransferInst>(&I)) {
- // memcpy/memmove(dst, src, len) — write dst, read src
- if (!MI->isVolatile()) {
- MemRanges.push_back({&I, MI->getDest(), MI->getLength(), /*IsWrite=*/true});
- MemRanges.push_back({&I, MI->getSource(), MI->getLength(), /*IsWrite=*/false});
- }
- }
-
- if (Ptr && AccessTy)
- ToInstrument.push_back({&I, {Ptr, AccessTy}});
- }
-
- // Instrument load/store/atomics
- for (auto &Entry : ToInstrument) {
- Instruction *I = Entry.first;
- Value *Ptr = Entry.second.first;
- Type *AccessTy = Entry.second.second;
-
- if (instrumentMemoryAccess(I, Ptr, AccessTy)) {
- Modified = true;
- if (isa<LoadInst>(I))
- ++NumInstrumentedLoads;
- else if (isa<StoreInst>(I))
- ++NumInstrumentedStores;
- else
- ++NumInstrumentedAtomics;
+ SmallVector<Instruction *, 16> ToInstrument;
+
+ for (auto &BB : F) {
+ for (auto &I : BB) {
+ if (isa<LoadInst>(&I) || isa<StoreInst>(&I) || isa<AtomicRMWInst>(&I) ||
+ isa<AtomicCmpXchgInst>(&I))
+ ToInstrument.push_back(&I);
+ else if (isa<MemIntrinsic>(&I))
+ ToInstrument.push_back(&I);
}
}
- // Instrument mem intrinsics
- for (auto &MR : MemRanges) {
- if (instrumentMemoryRange(MR.I, MR.Ptr, MR.Size, MR.IsWrite)) {
- Modified = true;
- ++NumInstrumentedMemIntrinsics;
+ for (Instruction *I : ToInstrument) {
+ if (auto *LI = dyn_cast<LoadInst>(I))
+ Modified |= instrumentMemoryAccess(I, LI->getPointerOperand(), LI->getType());
+ else if (auto *SI = dyn_cast<StoreInst>(I))
+ Modified |= instrumentMemoryAccess(I, SI->getPointerOperand(), SI->getValueOperand()->getType());
+ else if (auto *RMW = dyn_cast<AtomicRMWInst>(I))
+ Modified |= instrumentMemoryAccess(I, RMW->getPointerOperand(), RMW->getValOperand()->getType());
+ else if (auto *CmpXchg = dyn_cast<AtomicCmpXchgInst>(I))
+ Modified |= instrumentMemoryAccess(I, CmpXchg->getPointerOperand(), CmpXchg->getNewValOperand()->getType());
+ else if (auto *MS = dyn_cast<MemSetInst>(I))
+ Modified |= instrumentMemoryRange(I, MS->getDest(), MS->getLength(), true);
+ else if (auto *MT = dyn_cast<MemTransferInst>(I)) {
+ Modified |= instrumentMemoryRange(I, MT->getDest(), MT->getLength(), true);
+ Modified |= instrumentMemoryRange(I, MT->getSource(), MT->getLength(), false);
}
}
-
return Modified;
}
bool LowFatSanitizer::run() {
+ LLVM_DEBUG(dbgs() << "[LowFat] run() Mode=" << (int)Options.Mode
+ << " BarrierOnly=" << Options.InternalBarrierOnly_ << "\n");
LLVM_DEBUG(dbgs() << "[LowFat] Running on module: " << M.getName() << "\n");
- bool Modified = false;
+ // Safe mode: InternalBarrierOnly_ is set for the PipelineStartEP pass.
+ if (Options.InternalBarrierOnly_) {
+ LLVM_DEBUG(dbgs() << "[LowFat] Inserting barriers (Safe mode)\n");
+ bool Modified = false;
+ // Declare llvm.sideeffect and llvm.fake.use once for the module.
+ Function *SideEffectFn =
+ Intrinsic::getOrInsertDeclaration(&M, Intrinsic::sideeffect);
+ Function *FakeUseFn =
+ Intrinsic::getOrInsertDeclaration(&M, Intrinsic::fake_use);
+ for (Function &F : M) {
+ if (F.isDeclaration() || F.empty())
+ continue;
+
+ // Insert @llvm.sideeffect() at function entry to prevent FunctionAttrs
+ // from inferring memory(none) on callers, blocking call-level DCE.
+ IRBuilder<> IRB(&*F.getEntryBlock().getFirstInsertionPt());
+ IRB.CreateCall(SideEffectFn, {});
+ LLVM_DEBUG(dbgs() << " [LowFat] Inserted sideeffect barrier in: "
+ << F.getName() << "\n");
+
+ // Insert @llvm.fake.use(loaded_val) immediately after every load.
+ // Without this, Dead Argument Elimination (DAE) can prove that a
+ // function's return value is unused at all call sites and rewrite
+ // ret %loaded_val → ret undef
+ // making the load itself dead, which is then DCE'd before the LowFat
+ // pass at OptimizerLastEP ever sees it.
+ SmallVector<LoadInst *, 8> Loads;
+ for (BasicBlock &BB : F)
+ for (Instruction &I : BB)
+ if (auto *LI = dyn_cast<LoadInst>(&I))
+ Loads.push_back(LI);
+ for (LoadInst *LI : Loads) {
+ IRBuilder<> LIRB(LI->getNextNode());
+ LIRB.CreateCall(FakeUseFn, {LI});
+ LLVM_DEBUG(dbgs() << " [LowFat] Inserted fake.use for load in: "
+ << F.getName() << "\n");
+ }
+
+ Modified = true;
+ }
+ return Modified;
+ }
+ bool Modified = false;
for (Function &F : M) {
+ if (F.isDeclaration() || F.empty())
+ continue;
Modified |= instrumentFunction(F);
}
-
return Modified;
}
>From fed9bd9c19efd0cc289324a9e0d29c8860d0ad44 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 4 Mar 2026 09:53:57 +0800
Subject: [PATCH 32/55] [LowFat] Fix tests to run on all optimizations
---
compiler-rt/test/lowfat/lit.cfg.py | 27 ++++++++++++++-----
.../test/lowfat/memcpy_oob_static_fatal.cpp | 7 ++---
.../test/lowfat/memcpy_oob_static_recover.cpp | 9 +++----
.../test/lowfat/memset_oob_static_fatal.cpp | 7 ++---
.../test/lowfat/memset_oob_static_recover.cpp | 9 +++----
compiler-rt/test/lowfat/oob_read_fatal.cpp | 12 ++++-----
6 files changed, 38 insertions(+), 33 deletions(-)
diff --git a/compiler-rt/test/lowfat/lit.cfg.py b/compiler-rt/test/lowfat/lit.cfg.py
index 993b5ba162dc1..2c83322d99b94 100644
--- a/compiler-rt/test/lowfat/lit.cfg.py
+++ b/compiler-rt/test/lowfat/lit.cfg.py
@@ -38,14 +38,29 @@ def build_invocation(flags):
return " " + " ".join([clang] + flags) + " "
-# Fatal mode : terminate on first OOB
-lowfat_flags = ["-fsanitize=lowfat", "-O1"]
-# Recover mode: warn + continue
-lowfat_recover_flags = lowfat_flags + ["-fsanitize-recover=lowfat"]
+# Base flags
+lowfat_base = ["-fsanitize=lowfat"]
-config.substitutions.append(("%clangxx_lowfat ", build_invocation(lowfat_flags)))
+# Fast mode (OptimizerLastEP)
+lowfat_fast = lowfat_base + ["-mllvm", "-lowfat-mode=fast"]
+# Safe mode (Barrier at PipelineStartEP + instrument at OptimizerLastEP)
+lowfat_safe = lowfat_base + ["-mllvm", "-lowfat-mode=safe"]
+
+config.substitutions.append(("%clangxx_lowfat ", build_invocation(lowfat_fast)))
+config.substitutions.append(("%clangxx_lowfat_safe ", build_invocation(lowfat_safe)))
+
+# Recover mode versions
+config.substitutions.append(
+ (
+ "%clangxx_lowfat_recover ",
+ build_invocation(lowfat_fast + ["-fsanitize-recover=lowfat"]),
+ )
+)
config.substitutions.append(
- ("%clangxx_lowfat_recover ", build_invocation(lowfat_recover_flags))
+ (
+ "%clangxx_lowfat_safe_recover ",
+ build_invocation(lowfat_safe + ["-fsanitize-recover=lowfat"]),
+ )
)
# Only Darwin and Linux are supported.
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
index 1555fbe70bf4c..c95553fc80c2a 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
@@ -1,8 +1,5 @@
-// RUN: %clangxx_lowfat -O0 %s -o %t
-// RUN: %clangxx_lowfat -O1 %s -o %t
-// RUN: %clangxx_lowfat -O2 %s -o %t
-// RUN: %clangxx_lowfat -O3 %s -o %t
-// RUN: not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
// Verify that memcpy writing past the end of a LowFat allocation is detected
// in fatal mode. The process must exit with a non-zero code (checked by
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
index 5a6b0ac647af0..62316bf1a1a0e 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
@@ -1,8 +1,7 @@
-// RUN: %clangxx_lowfat_recover -O0 %s -o %t
-// RUN: %clangxx_lowfat_recover -O1 %s -o %t
-// RUN: %clangxx_lowfat_recover -O2 %s -o %t
-// RUN: %clangxx_lowfat_recover -O3 %s -o %t
-// RUN: %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O1 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O3 %s -o %t && %run %t 2>&1 | FileCheck %s
// Verify that memcpy OOB in recover mode:
// 1. Prints a WARNING (not ERROR).
diff --git a/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
index 28e72de61f4b5..638c7a57dee07 100644
--- a/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
@@ -1,8 +1,5 @@
-// RUN: %clangxx_lowfat -O0 %s -o %t
-// RUN: %clangxx_lowfat -O1 %s -o %t
-// RUN: %clangxx_lowfat -O2 %s -o %t
-// RUN: %clangxx_lowfat -O3 %s -o %t
-// RUN: not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
// Verify that memset writing past the end of a LowFat allocation is detected
// in fatal mode.
diff --git a/compiler-rt/test/lowfat/memset_oob_static_recover.cpp b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
index ad4fbee318e13..c5f3d5b2702c2 100644
--- a/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
@@ -1,8 +1,7 @@
-// RUN: %clangxx_lowfat_recover -O0 %s -o %t
-// RUN: %clangxx_lowfat_recover -O1 %s -o %t
-// RUN: %clangxx_lowfat_recover -O2 %s -o %t
-// RUN: %clangxx_lowfat_recover -O3 %s -o %t
-// RUN: %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O1 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe_recover -O3 %s -o %t && %run %t 2>&1 | FileCheck %s
// Verify that memset OOB in recover mode warns and continues.
diff --git a/compiler-rt/test/lowfat/oob_read_fatal.cpp b/compiler-rt/test/lowfat/oob_read_fatal.cpp
index 38d67e1ecbf0c..290f450ad626d 100644
--- a/compiler-rt/test/lowfat/oob_read_fatal.cpp
+++ b/compiler-rt/test/lowfat/oob_read_fatal.cpp
@@ -1,11 +1,10 @@
-// RUN: %clangxx_lowfat -O0 %s -o %t
-// RUN: %clangxx_lowfat -O1 %s -o %t
-// RUN: %clangxx_lowfat -O2 %s -o %t
-// RUN: %clangxx_lowfat -O3 %s -o %t
-// RUN: not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s
// Verify that an OOB load (scalar read across allocation boundary) is detected
-// in fatal mode.
+// in fatal mode across all optimisation levels and modes.
#include <cstdlib>
@@ -19,7 +18,6 @@ int main() {
// Read 8 bytes at offset 28 of a 32-byte allocation:
// bytes 28–35 exceed the 32-byte boundary → OOB.
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
- // FIXME: must NOT use volatile here — the LowFat pass skips volatile accesses.
double *p = (double *)(buf + 28);
double val = *p; // 8-byte read at offset 28 of 32-byte alloc → OOB (bytes 28–35)
(void)val; // keep live to prevent DSE; crash fires on the load above
>From e03580ca0f0a6e38eb1fb9595683e55452636bd5 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 4 Mar 2026 09:54:34 +0800
Subject: [PATCH 33/55] [LowFat] Add tests to differentiate between fast and
safe mode
---
compiler-rt/test/lowfat/dead_load.cpp | 24 +++++++++
.../lowfat/malloc_intercepted_inbounds.cpp | 33 ++++++++++++
.../test/lowfat/mode_baseline_all_detect.cpp | 27 ++++++++++
.../test/lowfat/mode_diff_pure_call.cpp | 52 +++++++++++++++++++
4 files changed, 136 insertions(+)
create mode 100644 compiler-rt/test/lowfat/dead_load.cpp
create mode 100644 compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp
create mode 100644 compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
create mode 100644 compiler-rt/test/lowfat/mode_diff_pure_call.cpp
diff --git a/compiler-rt/test/lowfat/dead_load.cpp b/compiler-rt/test/lowfat/dead_load.cpp
new file mode 100644
index 0000000000000..15e30304506cd
--- /dev/null
+++ b/compiler-rt/test/lowfat/dead_load.cpp
@@ -0,0 +1,24 @@
+// RUN: %clangxx_lowfat -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ALL
+// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ALL
+
+// Verifies that a genuine OOB heap read is detected in both Fast and Safe mode
+// when the loaded value is actually used (returned and passed to printf).
+
+#include <cstdio>
+#include <cstdlib>
+
+__attribute__((noinline))
+double oob_read(char *p) {
+ // 8-byte read at offset 14 of a 16-byte allocation.
+ // Bytes [14, 22) exceed the slot boundary [0, 16) → genuine OOB.
+ return *(double *)(p + 14);
+}
+
+int main() {
+ char *p = (char *)malloc(16);
+ double val = oob_read(p); // return value kept live → load not DCE'd
+ // CHECK-ALL: LOWFAT ERROR: out-of-bounds error detected!
+ printf("val=%f\n", val);
+ free(p);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp b/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp
new file mode 100644
index 0000000000000..f8359b2d3f45c
--- /dev/null
+++ b/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp
@@ -0,0 +1,33 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+
+// Verify that malloc-intercepted allocations are correctly handled for in-bounds accesses.
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+int main() {
+ // malloc goes through the LowFat interceptor → produces a LowFat pointer.
+ char *buf = (char *)malloc(32);
+ if (!buf) return 1;
+
+ // In-bounds scalar accesses — first and last byte.
+ buf[0] = 'A';
+ buf[31] = 'Z';
+
+ // In-bounds memcpy exactly within the allocation.
+ const char src[32] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+ memcpy(buf, src, 32);
+
+ // In-bounds memset exactly within the allocation.
+ memset(buf, 0, 32);
+
+ free(buf);
+
+ // CHECK: intercepted_inbounds: ok
+ // CHECK-NOT: LOWFAT ERROR
+ // CHECK-NOT: LOWFAT WARNING
+ printf("intercepted_inbounds: ok\n");
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp b/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
new file mode 100644
index 0000000000000..6fdf82f3acaf9
--- /dev/null
+++ b/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
@@ -0,0 +1,27 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
+// RUN: %clangxx_lowfat_safe -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
+// RUN: %clangxx_lowfat -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
+// RUN: %clangxx_lowfat_safe -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
+
+// Baseline: all three modes detect an OOB access whose result IS observable
+// Contrast with mode_diff_pure_call.cpp where a discarded-result call lets
+// Fast eliminate the load before the LowFat pass even sees it.
+
+#include <cstdio>
+#include <cstdlib>
+
+volatile char sink; // volatile global: any write/read here is always observable
+
+int main() {
+ char *p = (char *)malloc(16);
+
+ // 8-byte (double) OOB read at offset 14: bytes [14, 22) overflow the
+ // 16-byte LowFat slot [0, 16).
+ sink = (char)(*reinterpret_cast<volatile double *>(p + 14));
+
+ free(p);
+
+ // CHECK-OOB: LOWFAT ERROR: out-of-bounds error detected!
+ printf("DONE\n");
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/mode_diff_pure_call.cpp b/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
new file mode 100644
index 0000000000000..5f3109ea81d02
--- /dev/null
+++ b/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
@@ -0,0 +1,52 @@
+// RUN: %clangxx_lowfat -O3 %s -o %t && %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-MISS
+// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-CATCH
+
+// Demonstrates a behavioral difference between Fast mode and Safe/Comprehensive mode.
+//
+// The scenario: a noinline function performs an OOB heap read but its return value
+// is discarded by the caller.
+//
+// Fast mode has no PipelineStartEP pass. LLVM's Dead Argument Elimination (DAE)
+// sees peek()'s return value is unused at all call sites and rewrites:
+// ret %loaded_val → ret undef
+// The load becomes dead and is DCE'd. The LowFat pass at OptimizerLastEP finds
+// no load to instrument — the OOB is missed.
+//
+// Safe mode's barrier pass runs at PipelineStartEP and inserts:
+// 1. @llvm.sideeffect() — prevents call-level DCE by blocking memory(none)
+// inference on peek().
+// 2. @llvm.fake.use(loaded_val) — after every load. fake.use creates a data
+// dependency on the loaded value without emitting machine code,
+// preventing DAE from marking the return value as dead. The load survives
+// to OptimizerLastEP where LowFat instruments it → OOB is caught.
+//
+// Comprehensive mode instruments at PipelineStartEP before any optimization,
+// so the check call is inserted before DAE has a chance to remove the load.
+//
+// Expected outputs:
+// Fast (-O3): load DCE'd by DAE → no OOB check → program exits 0 → DONE
+// Safe (-O3): fake.use keeps load alive → OOB detected → LOWFAT ERROR
+
+#include <cstdio>
+#include <cstdlib>
+
+// noinline: the optimizer cannot see the body from the call site in Fast mode,
+// so it performs inter-procedural attribute inference rather than inlining.
+__attribute__((noinline))
+static double peek(char *p) {
+ // 8-byte (double) OOB read. p was allocated with malloc(16); a double starting
+ // at offset 14 spans bytes [14, 22), which overflows the 16-byte LowFat slot
+ // boundary at byte 16. LowFat detects this as an out-of-bounds access.
+ return *reinterpret_cast<double *>(p + 14);
+}
+
+int main() {
+ char *p = (char *)malloc(16);
+ peek(p); // Return value discarded — caller has no use for it.
+ free(p);
+
+ // CHECK-MISS: DONE
+ // CHECK-CATCH: LOWFAT ERROR: out-of-bounds error detected!
+ printf("DONE\n");
+ return 0;
+}
>From bc095022e0b1cd924f30315d28f8dbc7269d04e3 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 4 Mar 2026 10:58:43 +0800
Subject: [PATCH 34/55] [LowFat] Use recover flag for rtl check_bounds
---
compiler-rt/lib/lowfat/lf_interceptors.cpp | 6 ++++-
compiler-rt/lib/lowfat/lf_interface.h | 5 ++++
compiler-rt/lib/lowfat/lf_rtl.cpp | 9 ++++++++
.../Instrumentation/LowFatSanitizer.cpp | 23 +++++++++++++++++++
4 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/compiler-rt/lib/lowfat/lf_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp
index 67186a8f28f9d..b857ca845b3ff 100644
--- a/compiler-rt/lib/lowfat/lf_interceptors.cpp
+++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp
@@ -21,6 +21,7 @@ using namespace __sanitizer;
namespace __lowfat {
extern bool lowfat_inited;
+extern bool lowfat_recover;
} // namespace __lowfat
// DlsymAlloc handles allocations that happen before our runtime is initialized
@@ -150,7 +151,10 @@ static inline void check_bounds(const void *ptr, uptr access_size, int is_write)
uptr start = (uptr)ptr;
uptr size = __lowfat::GetSize(start);
uptr base = __lowfat::GetBase(start);
- __lf_report_oob(start + access_size, base, size, is_write);
+ if (__lowfat::lowfat_recover)
+ __lf_warn_oob(start + access_size, base, size, is_write);
+ else
+ __lf_report_oob(start + access_size, base, size, is_write);
}
}
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
index 63577acf7d782..a3691987ed247 100644
--- a/compiler-rt/lib/lowfat/lf_interface.h
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -30,6 +30,11 @@ SANITIZER_INTERFACE_ATTRIBUTE void __lf_init();
// Report a fatal out-of-bounds access and terminate.
// ptr: the offending pointer, base: base of the allocation,
// bound: size of the allocation, is_write: 1=write 0=read
+
+// Called from a compiler-generated module constructor to communicate
+// -fsanitize-recover=lowfat to the runtime interceptors.
+SANITIZER_INTERFACE_ATTRIBUTE void __lf_set_recover(int recover);
+
SANITIZER_INTERFACE_ATTRIBUTE void __lf_report_oob(uptr ptr, uptr base,
uptr bound, int is_write);
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 3977445520e28..94c7985569794 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -29,6 +29,10 @@ namespace __lowfat {
// Flag to track initialization state (not static — accessed by lf_interceptors.cpp)
bool lowfat_inited = false;
+// Set to true when -fsanitize-recover=lowfat is active. Controls whether
+// interceptor-level OOB (memset/memcpy/memmove) warns-and-continues or aborts.
+bool lowfat_recover = false;
+
// Region table - initialized in __lf_init
// TODO: not actually needed, to use for convenience
RegionInfo kRegions[kNumSizeClasses];
@@ -198,6 +202,11 @@ static void PrintWarning(uptr ptr, uptr base, uptr bound, int is_write) {
extern "C" {
+SANITIZER_INTERFACE_ATTRIBUTE
+void __lf_set_recover(int recover) {
+ __lowfat::lowfat_recover = (recover != 0);
+}
+
SANITIZER_INTERFACE_ATTRIBUTE
void __lf_init() {
if (__lowfat::lowfat_inited)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 94587b5435eaf..66170ec3dd367 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -27,6 +27,7 @@
#include "llvm/Support/Debug.h"
#include "llvm/Support/ModRef.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
using namespace llvm;
@@ -286,6 +287,28 @@ bool LowFatSanitizer::run() {
continue;
Modified |= instrumentFunction(F);
}
+
+ // Emit a module constructor that calls __lf_set_recover(Recover) so the
+ // runtime interceptors (memset/memcpy/memmove) know whether to warn or abort.
+ // This runs before main() via .init_array / __mod_init_func.
+ if (Options.Recover) {
+ LLVMContext &Ctx = M.getContext();
+ FunctionType *SetRecoverTy =
+ FunctionType::get(Type::getVoidTy(Ctx), {Type::getInt32Ty(Ctx)}, false);
+ FunctionCallee SetRecoverFn =
+ M.getOrInsertFunction("__lf_set_recover", SetRecoverTy);
+ Function *Ctor = Function::Create(
+ FunctionType::get(Type::getVoidTy(Ctx), false),
+ GlobalValue::InternalLinkage, "__lowfat_set_recover_ctor", &M);
+ BasicBlock *BB = BasicBlock::Create(Ctx, "entry", Ctor);
+ IRBuilder<> CtorBuilder(BB);
+ CtorBuilder.CreateCall(SetRecoverFn,
+ {ConstantInt::get(Type::getInt32Ty(Ctx), 1)});
+ CtorBuilder.CreateRetVoid();
+ appendToGlobalCtors(M, Ctor, /*Priority=*/0);
+ Modified = true;
+ }
+
return Modified;
}
>From d43345c865bad8cfbcd1395127b92854aee8cd6a Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 7 Mar 2026 21:41:05 +0800
Subject: [PATCH 35/55] [LowFat] Enable custom memory config mode
---
compiler-rt/lib/lowfat/CMakeLists.txt | 59 ++-
compiler-rt/lib/lowfat/lf_config.h | 97 ++++-
compiler-rt/lib/lowfat/lf_rtl.cpp | 52 ++-
compiler-rt/lib/lowfat/tools/lf_config_gen.c | 357 +++++++++++++++++
compiler-rt/lib/lowfat/tools/sizes.cfg | 57 +++
.../Transforms/Instrumentation/CMakeLists.txt | 42 ++
.../Instrumentation/LowFatSanitizer.cpp | 372 +++++++++++++++---
llvm/runtimes/CMakeLists.txt | 6 +
8 files changed, 969 insertions(+), 73 deletions(-)
create mode 100644 compiler-rt/lib/lowfat/tools/lf_config_gen.c
create mode 100644 compiler-rt/lib/lowfat/tools/sizes.cfg
diff --git a/compiler-rt/lib/lowfat/CMakeLists.txt b/compiler-rt/lib/lowfat/CMakeLists.txt
index 22fa2c5463c1e..07e4ffe85af06 100644
--- a/compiler-rt/lib/lowfat/CMakeLists.txt
+++ b/compiler-rt/lib/lowfat/CMakeLists.txt
@@ -1,5 +1,61 @@
include_directories(..)
+set(LOWFAT_COMMON_DEFINITIONS)
+
+#===------------------------------------------------------------------------===
+# Custom size-class configuration (non-POW2 support)
+#
+# Set -DLOWFAT_SIZES_CFG=/path/to/sizes.cfg at cmake time to enable.
+# This builds the lf_config_gen host tool, runs it to produce
+# lf_config_generated.h, and compiles the runtime with LOWFAT_CUSTOM_CONFIG.
+#===------------------------------------------------------------------------===
+set(LOWFAT_SIZES_CFG "" CACHE FILEPATH
+ "Path to sizes.cfg for non-POW2 LowFat size class generation. Empty = POW2-only mode.")
+
+if(LOWFAT_SIZES_CFG)
+ # ---- Build the generator tool (host compiler, no LLVM deps) ----
+ # Use add_custom_target / add_custom_command rather than add_executable so
+ # this host tool is never cross-compiled.
+ set(LOWFAT_CONFIG_GEN_SRC
+ ${CMAKE_CURRENT_SOURCE_DIR}/tools/lf_config_gen.c)
+ set(LOWFAT_CONFIG_GEN_BIN
+ ${CMAKE_CURRENT_BINARY_DIR}/lf_config_gen${CMAKE_EXECUTABLE_SUFFIX})
+ set(LOWFAT_GENERATED_HEADER
+ ${CMAKE_CURRENT_BINARY_DIR}/lf_config_generated.h)
+
+ add_custom_command(
+ OUTPUT ${LOWFAT_CONFIG_GEN_BIN}
+ COMMAND ${CMAKE_C_COMPILER} -std=c11 -O2
+ ${LOWFAT_CONFIG_GEN_SRC}
+ -o ${LOWFAT_CONFIG_GEN_BIN}
+ DEPENDS ${LOWFAT_CONFIG_GEN_SRC}
+ COMMENT "Compiling lf_config_gen host tool"
+ VERBATIM
+ )
+
+ add_custom_command(
+ OUTPUT ${LOWFAT_GENERATED_HEADER}
+ COMMAND ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG} ${LOWFAT_GENERATED_HEADER}
+ DEPENDS ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG}
+ COMMENT "Generating LowFat size tables from ${LOWFAT_SIZES_CFG}"
+ VERBATIM
+ )
+
+ add_custom_target(lowfat_config_generated DEPENDS ${LOWFAT_GENERATED_HEADER})
+ set(LOWFAT_DEPS lowfat_config_generated)
+
+ # Make the generated header visible to all sources in this directory.
+ include_directories(${CMAKE_CURRENT_BINARY_DIR})
+
+ # Activate the custom-config code paths in the runtime AND the LLVM pass.
+ list(APPEND LOWFAT_COMMON_DEFINITIONS LOWFAT_CUSTOM_CONFIG=1)
+
+ message(STATUS "LowFat: custom config enabled: ${LOWFAT_SIZES_CFG}")
+ message(STATUS "LowFat: generated header: ${LOWFAT_GENERATED_HEADER}")
+else()
+ message(STATUS "LowFat: using default POW2-only mode (no LOWFAT_SIZES_CFG set)")
+ set(LOWFAT_DEPS)
+endif()
set(LOWFAT_SOURCES
lf_rtl.cpp
lf_interceptors.cpp
@@ -13,7 +69,6 @@ set(LOWFAT_HEADERS
set(LOWFAT_CFLAGS ${SANITIZER_COMMON_CFLAGS})
append_rtti_flag(OFF LOWFAT_CFLAGS)
-set(LOWFAT_COMMON_DEFINITIONS)
# Static runtime library.
add_compiler_rt_component(lowfat)
@@ -35,6 +90,7 @@ if(COMPILER_RT_HAS_LOWFAT)
CFLAGS ${LOWFAT_CFLAGS}
LINK_FLAGS ${WEAK_SYMBOL_LINK_FLAGS}
DEFS ${LOWFAT_COMMON_DEFINITIONS}
+ DEPS ${LOWFAT_DEPS}
PARENT_TARGET lowfat)
else()
add_compiler_rt_runtime(clang_rt.lowfat
@@ -49,6 +105,7 @@ if(COMPILER_RT_HAS_LOWFAT)
RTSanitizerCommonSymbolizerInternal
CFLAGS ${LOWFAT_CFLAGS}
DEFS ${LOWFAT_COMMON_DEFINITIONS}
+ DEPS ${LOWFAT_DEPS}
PARENT_TARGET lowfat)
endif()
endif()
diff --git a/compiler-rt/lib/lowfat/lf_config.h b/compiler-rt/lib/lowfat/lf_config.h
index 02deb4375fff3..a3451ce2cd6a0 100644
--- a/compiler-rt/lib/lowfat/lf_config.h
+++ b/compiler-rt/lib/lowfat/lf_config.h
@@ -11,16 +11,24 @@
// LowFat pointers encode allocation bounds directly in the pointer value:
// - Memory is divided into regions, each for a specific size class
// - Within each region, allocations are aligned to their size class
-// - Given a pointer, the base can be computed by masking off low bits
+// - Given a pointer, the base can be computed by masking off low bits (POW2)
+// or via fixed-point magic-number math (non-POW2)
// - The size can be looked up from a table using the region index
//
-// Memory Layout (64-bit, example):
+// Default Memory Layout (POW2-only mode, kRegionSizeLog=32):
// Region 0: [0x10_0000_0000, 0x20_0000_0000) - 16-byte allocations
// Region 1: [0x20_0000_0000, 0x30_0000_0000) - 32-byte allocations
// Region 2: [0x30_0000_0000, 0x40_0000_0000) - 64-byte allocations
// ...
// Region N: [0xN0_0000_0000, ...) - 2^(N+4)-byte allocations
//
+// Custom Config Mode (LOWFAT_CUSTOM_CONFIG, kRegionSizeLog=35):
+// Non-POW2 sizes (e.g. 48, 80, 96 bytes) are also supported.
+// kRegionSizeLog increases to 35 (32 GB per region) to preserve precision
+// of the magic-number arithmetic across the full region.
+// The key helpers (SizeClassIndex, SizeClassToSize) switch to table lookups.
+// All other logic (GetBase, GetSize, CheckBounds, etc.) is unchanged.
+//
//===----------------------------------------------------------------------===//
#ifndef LF_CONFIG_H
@@ -28,6 +36,10 @@
#include "sanitizer_common/sanitizer_internal_defs.h"
+#ifdef LOWFAT_CUSTOM_CONFIG
+#include "lf_config_generated.h"
+#endif
+
namespace __lowfat {
using namespace __sanitizer;
@@ -40,6 +52,26 @@ using namespace __sanitizer;
constexpr uptr kMinSizeLog = 4; // 16 bytes
constexpr uptr kMinSize = 1ULL << kMinSizeLog;
+#ifdef LOWFAT_CUSTOM_CONFIG
+
+constexpr uptr kNumSizeClasses = LOWFAT_NUM_SIZE_CLASSES;
+constexpr uptr kMaxSize = LOWFAT_MAX_SIZE;
+
+// SizeClassIndex: table lookup (binary search on kLowFatGenSizes[])
+// Replaces the POW2-only __builtin_clzll math.
+inline uptr SizeClassIndex(uptr size) {
+ return (uptr)lowfat_size_to_class((uint64_t)size);
+}
+
+// SizeClassToSize: direct table lookup — works for both POW2 and non-POW2.
+inline uptr SizeClassToSize(uptr class_index) {
+ if (class_index >= kNumSizeClasses)
+ return 0;
+ return (uptr)kLowFatGenSizes[class_index];
+}
+
+#else
+
// Maximum allocation size (must be power of 2)
constexpr uptr kMaxSizeLog = 30; // 1 GB
constexpr uptr kMaxSize = 1ULL << kMaxSizeLog;
@@ -65,12 +97,19 @@ inline uptr SizeClassToSize(uptr class_index) {
return 1ULL << (class_index + kMinSizeLog);
}
+#endif // LOWFAT_CUSTOM_CONFIG
+
//===----------------------------------------------------------------------===//
// Memory Region Configuration
//===----------------------------------------------------------------------===//
+#ifdef LOWFAT_CUSTOM_CONFIG
+constexpr uptr kRegionSizeLog = LOWFAT_REGION_SIZE_LOG; // 35
+#else
// Each region is 4GB (32 bits of address space per region)
constexpr uptr kRegionSizeLog = 32;
+#endif
+
constexpr uptr kRegionSize = 1ULL << kRegionSizeLog;
// Base address where LowFat regions start
@@ -100,6 +139,50 @@ inline bool IsLowFatPointer(uptr ptr) {
// Bounds Computation
//===----------------------------------------------------------------------===//
+// Get the allocation size from a LowFat pointer
+inline uptr GetSize(uptr ptr) {
+ uptr region = GetRegionIndex(ptr);
+ if (region >= kNumSizeClasses)
+ return 0; // Not a valid LowFat pointer
+ return SizeClassToSize(region);
+}
+
+#ifdef LOWFAT_CUSTOM_CONFIG
+
+// GetBase override for non-POW2: use magic-number multiplication instead
+// of the bitwise-AND fast path when the size class is not a power of two.
+//
+// For POW2 sizes: base = ptr & ~(size - 1) [fast path]
+// For non-POW2: base = ((u128)ptr * magic >> 64) * size [magic path]
+inline uptr GetBase(uptr ptr) {
+ uptr region = GetRegionIndex(ptr);
+ if (region >= kNumSizeClasses)
+ return 0;
+ if (kLowFatGenIsPow2[region]) {
+ // Fast path: bitwise AND
+ return ptr & (uptr)kLowFatGenMasks[region];
+ } else {
+ // Magic-number fixed-point path
+ typedef unsigned __int128 u128;
+ u128 mul = (u128)ptr * (u128)kLowFatGenMagics[region];
+ uptr idx = (uptr)(mul >> 64);
+ return idx * (uptr)kLowFatGenSizes[region];
+ }
+}
+
+// CheckBounds override: uses the custom GetBase above.
+inline bool CheckBounds(uptr ptr, uptr access_size) {
+ uptr region = GetRegionIndex(ptr);
+ if (region >= kNumSizeClasses)
+ return true; // Not a LowFat pointer — assume valid
+ uptr alloc_size = SizeClassToSize(region);
+ uptr base = GetBase(ptr);
+ uptr end = base + alloc_size;
+ return (ptr + access_size) <= end;
+}
+
+#else
+
// Get the base address of an allocation from a LowFat pointer
// This uses the key LowFat insight: allocations are aligned to their size
inline uptr GetBase(uptr ptr) {
@@ -112,14 +195,6 @@ inline uptr GetBase(uptr ptr) {
return ptr & mask;
}
-// Get the allocation size from a LowFat pointer
-inline uptr GetSize(uptr ptr) {
- uptr region = GetRegionIndex(ptr);
- if (region >= kNumSizeClasses)
- return 0; // Not a valid LowFat pointer
- return SizeClassToSize(region);
-}
-
// Check if ptr..ptr+access_size is within bounds
inline bool CheckBounds(uptr ptr, uptr access_size) {
uptr region = GetRegionIndex(ptr);
@@ -133,6 +208,8 @@ inline bool CheckBounds(uptr ptr, uptr access_size) {
return (ptr + access_size) <= end;
}
+#endif // LOWFAT_CUSTOM_CONFIG
+
//===----------------------------------------------------------------------===//
// Region Table (for lookup by region index)
//===----------------------------------------------------------------------===//
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 94c7985569794..b7d0b37e1450f 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -33,27 +33,36 @@ bool lowfat_inited = false;
// interceptor-level OOB (memset/memcpy/memmove) warns-and-continues or aborts.
bool lowfat_recover = false;
+// Maximum number of size classes across both modes.
+// In POW2-only mode kNumSizeClasses=27; with a custom config it can be up to
+// LOWFAT_MAX_ARRAY_SIZE. We size the static arrays at compile time using
+// whichever is larger so the same translation unit works in both modes.
+#ifdef LOWFAT_CUSTOM_CONFIG
+ static constexpr uptr kMaxSizeClasses = LOWFAT_NUM_SIZE_CLASSES;
+#else
+ static constexpr uptr kMaxSizeClasses = kNumSizeClasses;
+#endif
+
// Region table - initialized in __lf_init
-// TODO: not actually needed, to use for convenience
-RegionInfo kRegions[kNumSizeClasses];
+RegionInfo kRegions[kMaxSizeClasses];
// Pointers to the start of each mapped region
-static uptr region_bases[kNumSizeClasses];
+static uptr region_bases[kMaxSizeClasses];
// Bump pointer: next fresh address to allocate from in each region
-static uptr region_next_alloc[kNumSizeClasses];
+static uptr region_next_alloc[kMaxSizeClasses];
// Segregated free lists: one singly-linked list per size class
// Free blocks store a pointer to the next free block at their start
struct FreeBlock {
FreeBlock *next;
};
-static FreeBlock *free_lists[kNumSizeClasses];
+static FreeBlock *free_lists[kMaxSizeClasses];
// Per-size-class spin mutexes protecting region_next_alloc and free_lists.
// Using one lock per size class allows concurrent allocation across different
// size classes, which is the common case in multi-threaded programs.
-static StaticSpinMutex region_locks[kNumSizeClasses];
+static StaticSpinMutex region_locks[kMaxSizeClasses];
static void InitializeFlags() {
SetCommonFlagsDefaults();
@@ -78,9 +87,15 @@ static void InitializeFlags() {
static void InitRegionTable() {
for (uptr i = 0; i < kNumSizeClasses; i++) {
uptr size = SizeClassToSize(i);
- kRegions[i].size = size;
+ kRegions[i].size = size;
kRegions[i].alignment = size;
+#ifdef LOWFAT_CUSTOM_CONFIG
+ // For non-POW2 sizes the mask is meaningless (base computed via magic
+ // multiply); store 0 to make this explicit and catch accidental usage.
+ kRegions[i].mask = kLowFatGenIsPow2[i] ? (uptr)kLowFatGenMasks[i] : 0;
+#else
kRegions[i].mask = ~(size - 1);
+#endif
free_lists[i] = nullptr;
}
}
@@ -92,21 +107,25 @@ static bool InitMemoryRegions() {
uptr region_start = GetRegionStart(i);
// Reserve the region without committing physical memory
- // MmapFixedNoReserve maps memory but doesn't allocate physical pages
- // until they're accessed (lazy allocation)
+ // MmapFixedNoReserve maps memory but doesn't allocate physical pages until they're accessed
bool success = MmapFixedNoReserve(region_start, kRegionSize, "lowfat_region");
- if (!success) {
- // Printf("LowFat: Failed to map region %zu at 0x%zx\n", i, region_start);
+ if (!success)
return false;
- }
region_bases[i] = region_start;
- region_next_alloc[i] = region_start;
+
+ // The first allocation must be aligned to the object size relative to absolute zero.
+ // This is required for the magic-number fixed point math to securely compute object bases.
+ uptr size = kRegions[i].size;
+ uptr offset = region_start % size;
+ uptr initial_alloc = region_start;
+ if (offset != 0)
+ initial_alloc += (size - offset);
+
+ region_next_alloc[i] = initial_alloc;
}
- // Printf("LowFat: Mapped %zu regions starting at 0x%zx\n",
- // kNumSizeClasses, kRegionBase);
return true;
}
@@ -138,9 +157,6 @@ void *Allocate(uptr size) {
uptr region_end = GetRegionStart(class_index) + kRegionSize;
uptr addr = region_next_alloc[class_index];
- // Ensure alignment (should already be aligned)
- addr = (addr + alloc_size - 1) & ~(alloc_size - 1);
-
if (addr + alloc_size > region_end)
return nullptr;
diff --git a/compiler-rt/lib/lowfat/tools/lf_config_gen.c b/compiler-rt/lib/lowfat/tools/lf_config_gen.c
new file mode 100644
index 0000000000000..e9cd3d6d6502e
--- /dev/null
+++ b/compiler-rt/lib/lowfat/tools/lf_config_gen.c
@@ -0,0 +1,357 @@
+//===-- lf_config_gen.c - LowFat Size Class Config Generator --------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Standalone C tool (no LLVM dependencies) that reads a sizes.cfg file and
+// emits lf_config_generated.h containing:
+//
+// - kLowFatGenSizes[] : actual object sizes for each region index
+// - kLowFatGenMagics[] : precomputed 2^64/S values for non-POW2 sizes
+// - kLowFatGenIsPow2[] : true for power-of-two size classes
+// - kLowFatGenMasks[] : alignment masks for POW2 sizes (0 for non-POW2)
+// - lowfat_size_to_class(): binary-search mapping from alloc size → region index
+//
+// sizes.cfg format:
+// - One size per line (plain integer)
+// - Sizes must be multiples of 16
+// - First size must be 16
+// - Sizes must be in strictly ascending order
+// - Maximum size ≤ LOWFAT_REGION_SIZE_LOG (32 GB when kRegionSizeLog=35)
+//
+// Usage:
+// lf_config_gen <sizes.cfg> <output_header>
+//
+// The precision checker validates that the fixed-point formula
+// base = (ptr * magic >> 64) * S
+// correctly identifies the start of every object within a 32 GB region.
+// Where rounding errors exist, the effective size is reduced by the error
+// so the allocator never hands out the last few bytes that would be
+// mis-identified.
+//
+//===----------------------------------------------------------------------===//
+
+#include <assert.h>
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+// --------------------------------------------------------------------------
+// Configuration constants (must stay in sync with lf_config.h)
+// --------------------------------------------------------------------------
+
+// Each region is 32 GB when custom config is active.
+// The original LowFat research proves the magic-number math is precise up to
+// 32 GB boundaries; 4 GB is insufficient for non-POW2 sizes.
+#define REGION_SIZE_LOG 35
+#define REGION_SIZE ((uint64_t)1 << REGION_SIZE_LOG) // 32 GB
+#define MAX_SIZE_CLASSES 256
+
+// Minimum alignment / granularity
+#define MIN_SIZE 16
+
+// --------------------------------------------------------------------------
+// __int128 helpers (standard C99/C11 with GCC/Clang extension)
+// --------------------------------------------------------------------------
+
+typedef unsigned __int128 u128;
+
+// Compute ceil(2^64 / S) using 128-bit arithmetic.
+// This is the magic number M such that floor(P / S) = (P * M) >> 64
+// for all P in [0, REGION_SIZE).
+static uint64_t compute_magic(uint64_t S) {
+ if (S == 0) return 0;
+ u128 two64 = (u128)1 << 64;
+ uint64_t q = (uint64_t)(two64 / S);
+ uint64_t r = (uint64_t)(two64 % S);
+ return q + (r != 0 ? 1 : 0); // ceil division
+}
+
+// Returns 1 if n is an exact power of two, 0 otherwise.
+static int is_pow2(uint64_t n) {
+ return n != 0 && (n & (n - 1)) == 0;
+}
+
+// --------------------------------------------------------------------------
+// Precision Checker
+//
+// For each non-POW2 size S with magic M, scan backwards from the end of the
+// region to find the first pointer P where the reconstructed base is wrong.
+// Returns the number of bytes to subtract from S (the "error margin").
+//
+// The formula base(P) = ((u128)P * M >> 64) * S gives the start of the
+// object containing P. For it to be correct we need:
+// base(P) <= P && P < base(P) + S
+//
+// We walk from REGION_SIZE - 1 down to 0 in steps of S, stopping at the
+// first P where the formula breaks.
+// --------------------------------------------------------------------------
+
+static uint64_t precision_error(uint64_t S, uint64_t M) {
+ if (is_pow2(S))
+ return 0; // POW2 uses bitwise AND, no rounding error
+
+ uint64_t region = REGION_SIZE;
+ uint64_t error = 0;
+
+ // Walk the last few objects (the vulnerable zone is at the top of the region)
+ // Limit scan to last 1024 objects to keep tool fast; real errors are tiny.
+ uint64_t scan_start = region > S * 1024 ? region - S * 1024 : 0;
+
+ for (uint64_t ptr = region - 1; ptr >= scan_start && ptr != (uint64_t)-1; ptr--) {
+ u128 mul = (u128)ptr * (u128)M;
+ uint64_t idx = (uint64_t)(mul >> 64);
+ uint64_t base = idx * S;
+
+ // base must be the correct start: base <= ptr < base + S
+ if (ptr < base || ptr >= base + S) {
+ // ptr is mis-identified; error = bytes from end of last good object to ptr
+ // We shrink S by the difference so the allocator stays safe.
+ uint64_t last_good_end = (ptr / S) * S;
+ error = ptr - last_good_end + 1;
+ break;
+ }
+ }
+ return error;
+}
+
+// --------------------------------------------------------------------------
+// Main
+// --------------------------------------------------------------------------
+
+int main(int argc, char *argv[]) {
+ if (argc != 3) {
+ fprintf(stderr, "Usage: %s <sizes.cfg> <output_header>\n", argv[0]);
+ return 1;
+ }
+
+ const char *cfg_path = argv[1];
+ const char *out_path = argv[2];
+
+ // ---- Parse sizes.cfg ----
+ FILE *cfg = fopen(cfg_path, "r");
+ if (!cfg) {
+ fprintf(stderr, "Error: cannot open '%s'\n", cfg_path);
+ return 1;
+ }
+
+ uint64_t sizes[MAX_SIZE_CLASSES];
+ int num_sizes = 0;
+ char line[256];
+
+ while (fgets(line, sizeof(line), cfg)) {
+ // Skip blank lines and comments
+ char *p = line;
+ while (*p == ' ' || *p == '\t') p++;
+ if (*p == '#' || *p == '\n' || *p == '\r' || *p == '\0')
+ continue;
+
+ uint64_t s = (uint64_t)strtoull(p, NULL, 10);
+ if (s == 0) continue;
+
+ if (num_sizes >= MAX_SIZE_CLASSES) {
+ fprintf(stderr, "Error: too many size classes (max %d)\n", MAX_SIZE_CLASSES);
+ fclose(cfg);
+ return 1;
+ }
+ sizes[num_sizes++] = s;
+ }
+ fclose(cfg);
+
+ if (num_sizes == 0) {
+ fprintf(stderr, "Error: no valid sizes found in '%s'\n", cfg_path);
+ return 1;
+ }
+
+ // ---- Validate ----
+ if (sizes[0] != MIN_SIZE) {
+ fprintf(stderr, "Error: first size must be %d, got %" PRIu64 "\n",
+ MIN_SIZE, sizes[0]);
+ return 1;
+ }
+ for (int i = 0; i < num_sizes; i++) {
+ if (sizes[i] % MIN_SIZE != 0) {
+ fprintf(stderr, "Error: size %" PRIu64 " is not a multiple of %d\n",
+ sizes[i], MIN_SIZE);
+ return 1;
+ }
+ if (sizes[i] > REGION_SIZE) {
+ fprintf(stderr, "Error: size %" PRIu64 " exceeds max region size %" PRIu64 "\n",
+ sizes[i], REGION_SIZE);
+ return 1;
+ }
+ if (i > 0 && sizes[i] <= sizes[i - 1]) {
+ fprintf(stderr, "Error: sizes must be strictly ascending; "
+ "sizes[%d]=%" PRIu64 " <= sizes[%d]=%" PRIu64 "\n",
+ i, sizes[i], i - 1, sizes[i - 1]);
+ return 1;
+ }
+ }
+
+ // ---- Compute tables ----
+ uint64_t magics[MAX_SIZE_CLASSES];
+ int is_pow2_arr[MAX_SIZE_CLASSES];
+ uint64_t masks[MAX_SIZE_CLASSES];
+ uint64_t effective_sizes[MAX_SIZE_CLASSES]; // sizes adjusted for precision errors
+
+ for (int i = 0; i < num_sizes; i++) {
+ uint64_t S = sizes[i];
+ int pow2 = is_pow2(S);
+
+ is_pow2_arr[i] = pow2;
+
+ if (pow2) {
+ magics[i] = 0; // unused — POW2 uses AND
+ masks[i] = ~(S - 1);
+ effective_sizes[i] = S; // no precision error for POW2
+ } else {
+ uint64_t M = compute_magic(S);
+ uint64_t err = precision_error(S, M);
+ magics[i] = M;
+ masks[i] = 0; // not applicable for non-POW2
+ // Shrink effective size by error so allocator never gives out the
+ // bytes that the magic-number math would mis-identify.
+ effective_sizes[i] = S - err;
+
+ if (err > 0) {
+ fprintf(stderr, "Note: size %" PRIu64 " has precision error %" PRIu64
+ " bytes; effective size = %" PRIu64 "\n", S, err, effective_sizes[i]);
+ }
+ }
+ }
+
+ // ---- Open output ----
+ FILE *out = fopen(out_path, "w");
+ if (!out) {
+ fprintf(stderr, "Error: cannot open output '%s'\n", out_path);
+ return 1;
+ }
+
+ // ---- Emit header ----
+ fprintf(out,
+ "//===-- lf_config_generated.h - Auto-generated LowFat config ---------===//\n"
+ "//\n"
+ "// AUTO-GENERATED by lf_config_gen. DO NOT EDIT.\n"
+ "// Source: %s\n"
+ "//\n"
+ "//===----------------------------------------------------------------------===//\n"
+ "\n"
+ "#pragma once\n"
+ "#ifndef LF_CONFIG_GENERATED_H\n"
+ "#define LF_CONFIG_GENERATED_H\n"
+ "\n"
+ "#include <stdint.h>\n"
+ "\n"
+ "// Region layout: each region is 32 GB, matching the precision bounds of the\n"
+ "// magic-number fixed-point arithmetic proven in the original LowFat research.\n"
+ "#define LOWFAT_CUSTOM_CONFIG 1\n"
+ "#define LOWFAT_REGION_SIZE_LOG 35\n"
+ "#define LOWFAT_REGION_SIZE (UINT64_C(1) << LOWFAT_REGION_SIZE_LOG)\n"
+ "#define LOWFAT_NUM_SIZE_CLASSES %d\n"
+ "#define LOWFAT_MAX_SIZE UINT64_C(%" PRIu64 ")\n"
+ "\n",
+ cfg_path,
+ num_sizes,
+ sizes[num_sizes - 1]
+ );
+
+ // kLowFatGenSizes
+ fprintf(out,
+ "// Actual allocation size for each region index.\n"
+ "// For non-POW2 sizes this is the precision-adjusted effective size.\n"
+ "static const uint64_t kLowFatGenSizes[LOWFAT_NUM_SIZE_CLASSES] = {\n"
+ " /* idx: size */\n"
+ );
+ for (int i = 0; i < num_sizes; i++) {
+ fprintf(out, " /* %3d */ UINT64_C(%" PRIu64 ")%s\n",
+ i, effective_sizes[i], (i < num_sizes - 1) ? "," : "");
+ }
+ fprintf(out, "};\n\n");
+
+ // kLowFatGenMagics
+ fprintf(out,
+ "// Magic numbers for non-POW2 sizes: M = ceil(2^64 / S).\n"
+ "// For POW2 sizes this is 0 (they use the AND fast path).\n"
+ "static const uint64_t kLowFatGenMagics[LOWFAT_NUM_SIZE_CLASSES] = {\n"
+ " /* idx: magic */\n"
+ );
+ for (int i = 0; i < num_sizes; i++) {
+ fprintf(out, " /* %3d */ UINT64_C(0x%016" PRIx64 ")%s // size=%" PRIu64 "%s\n",
+ i, magics[i], (i < num_sizes - 1) ? "," : " ",
+ sizes[i], is_pow2_arr[i] ? " (POW2, unused)" : "");
+ }
+ fprintf(out, "};\n\n");
+
+ // kLowFatGenIsPow2
+ fprintf(out,
+ "// True if this size class is a power-of-two (uses AND path, not MUL path).\n"
+ "static const int kLowFatGenIsPow2[LOWFAT_NUM_SIZE_CLASSES] = {\n"
+ " /* idx: isPow2 */\n"
+ );
+ for (int i = 0; i < num_sizes; i++) {
+ fprintf(out, " /* %3d */ %d%s // %" PRIu64 "\n",
+ i, is_pow2_arr[i], (i < num_sizes - 1) ? "," : " ", sizes[i]);
+ }
+ fprintf(out, "};\n\n");
+
+ // kLowFatGenMasks
+ fprintf(out,
+ "// Alignment masks for POW2 sizes: ~(S-1). Zero for non-POW2 sizes.\n"
+ "static const uint64_t kLowFatGenMasks[LOWFAT_NUM_SIZE_CLASSES] = {\n"
+ " /* idx: mask */\n"
+ );
+ for (int i = 0; i < num_sizes; i++) {
+ fprintf(out, " /* %3d */ UINT64_C(0x%016" PRIx64 ")%s // size=%" PRIu64 "\n",
+ i, masks[i], (i < num_sizes - 1) ? "," : " ", sizes[i]);
+ }
+ fprintf(out, "};\n\n");
+
+ // lowfat_size_to_class: binary search — replaces SizeClassIndex()
+ fprintf(out,
+ "// Maps a requested allocation size to the smallest region index whose\n"
+ "// effective size >= requested size. Replaces SizeClassIndex() when\n"
+ "// LOWFAT_CUSTOM_CONFIG is active.\n"
+ "// Returns LOWFAT_NUM_SIZE_CLASSES if size exceeds all size classes.\n"
+ "static inline uint64_t lowfat_size_to_class(uint64_t size) {\n"
+ " // Binary search over kLowFatGenSizes[]\n"
+ " if (size == 0) size = 1;\n"
+ " uint64_t lo = 0, hi = LOWFAT_NUM_SIZE_CLASSES;\n"
+ " while (lo < hi) {\n"
+ " uint64_t mid = lo + (hi - lo) / 2;\n"
+ " if (kLowFatGenSizes[mid] < size)\n"
+ " lo = mid + 1;\n"
+ " else\n"
+ " hi = mid;\n"
+ " }\n"
+ " return lo; // lo == LOWFAT_NUM_SIZE_CLASSES means no fit\n"
+ "}\n"
+ "\n"
+ "#endif // LF_CONFIG_GENERATED_H\n"
+ );
+
+ fclose(out);
+
+ fprintf(stderr, "lf_config_gen: generated '%s' with %d size classes\n",
+ out_path, num_sizes);
+
+ // Print summary table to stdout
+ printf("Size Class Table:\n");
+ printf(" %-5s %-10s %-6s %-20s %-20s %-10s\n",
+ "Idx", "ReqSize", "POW2?", "EffectiveSize", "Magic", "Mask");
+ printf(" %s\n", "---------------------------------------------------------------------");
+ for (int i = 0; i < num_sizes; i++) {
+ printf(" %-5d %-10" PRIu64 " %-6s %-20" PRIu64 " %#-20" PRIx64 " %#-10" PRIx64 "\n",
+ i, sizes[i],
+ is_pow2_arr[i] ? "yes" : "no",
+ effective_sizes[i],
+ magics[i],
+ masks[i]);
+ }
+
+ return 0;
+}
diff --git a/compiler-rt/lib/lowfat/tools/sizes.cfg b/compiler-rt/lib/lowfat/tools/sizes.cfg
new file mode 100644
index 0000000000000..19036ab03aa68
--- /dev/null
+++ b/compiler-rt/lib/lowfat/tools/sizes.cfg
@@ -0,0 +1,57 @@
+# LowFat Size Class Configuration
+#
+# One size per line. Rules:
+# - First entry must be 16
+# - All sizes must be multiples of 16
+# - Sizes must be strictly ascending
+# - Maximum size <= 32 GB (LOWFAT_REGION_SIZE)
+#
+# POW2 sizes use the fast bitwise-AND path.
+# Non-POW2 sizes use the magic-number fixed-point multiplication path.
+#
+# This default config demonstrates a mix of POW2 and non-POW2 sizes.
+# Edit to your application's allocation profile for best results.
+#
+
+# Small sizes
+16
+32
+48
+64
+80
+96
+128
+192
+256
+320
+384
+512
+640
+768
+1024
+1280
+1536
+2048
+2560
+3072
+4096
+6144
+8192
+12288
+16384
+32768
+65536
+131072
+262144
+524288
+1048576
+2097152
+4194304
+8388608
+16777216
+33554432
+67108864
+134217728
+268435456
+536870912
+1073741824
diff --git a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
index 36fef5c50ddd0..b6c2d11511faf 100644
--- a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
+++ b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
@@ -45,3 +45,45 @@ add_llvm_component_library(LLVMInstrumentation
TransformUtils
ProfileData
)
+
+if(LOWFAT_SIZES_CFG)
+ # LLVM pass needs the generated header too. Since runtimes build AFTER LLVM,
+ # we must generate a local copy of the header for the pass to use.
+ #
+ # We use CMAKE_HOST_C_COMPILER (not add_executable) so that the generator is
+ # always built for the *host* machine. add_executable would produce a
+ # target-architecture binary on cross-compile setups (e.g. building for
+ # AArch64 on an x86 host) which cannot be executed during the build.
+ set(LOWFAT_CONFIG_GEN_SRC
+ ${LLVM_MAIN_SRC_DIR}/../compiler-rt/lib/lowfat/tools/lf_config_gen.c)
+ set(LOWFAT_CONFIG_GEN_BIN
+ ${CMAKE_CURRENT_BINARY_DIR}/lf_config_gen_llvm${CMAKE_EXECUTABLE_SUFFIX})
+ set(LOWFAT_GENERATED_HEADER ${CMAKE_CURRENT_BINARY_DIR}/lf_config_generated.h)
+
+ add_custom_command(
+ OUTPUT ${LOWFAT_CONFIG_GEN_BIN}
+ COMMAND ${CMAKE_C_COMPILER} -std=c11 -O2
+ ${LOWFAT_CONFIG_GEN_SRC}
+ -o ${LOWFAT_CONFIG_GEN_BIN}
+ DEPENDS ${LOWFAT_CONFIG_GEN_SRC}
+ COMMENT "Compiling lf_config_gen host tool (for LLVM pass)"
+ VERBATIM
+ )
+
+ add_custom_command(
+ OUTPUT ${LOWFAT_GENERATED_HEADER}
+ COMMAND ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG} ${LOWFAT_GENERATED_HEADER}
+ DEPENDS ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG}
+ COMMENT "Generating LowFat size config for LLVM pass"
+ VERBATIM
+ )
+
+ add_custom_target(lowfat_pass_config DEPENDS ${LOWFAT_GENERATED_HEADER})
+ add_dependencies(LLVMInstrumentation lowfat_pass_config)
+
+ set_property(SOURCE LowFatSanitizer.cpp APPEND PROPERTY COMPILE_DEFINITIONS "LOWFAT_CUSTOM_CONFIG=1")
+ set_property(SOURCE LowFatSanitizer.cpp APPEND PROPERTY OBJECT_DEPENDS ${LOWFAT_GENERATED_HEADER})
+
+ # Tell the compiler where to find the generated header
+ target_include_directories(LLVMInstrumentation PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+endif()
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 66170ec3dd367..169af093fa18f 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -29,6 +29,12 @@
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
#include "llvm/Transforms/Utils/ModuleUtils.h"
+// When the build generates a custom size-class config, pull in the tables so
+// the pass can emit the right IR (AND vs. 128-bit magic multiply).
+#ifdef LOWFAT_CUSTOM_CONFIG
+#include "lf_config_generated.h"
+#endif
+
using namespace llvm;
#define DEBUG_TYPE "lowfat"
@@ -37,6 +43,7 @@ STATISTIC(NumInstrumentedLoads, "Number of loads instrumented");
STATISTIC(NumInstrumentedStores, "Number of stores instrumented");
STATISTIC(NumInstrumentedAtomics, "Number of atomic operations instrumented");
STATISTIC(NumInstrumentedMemIntrinsics, "Number of mem intrinsics instrumented");
+STATISTIC(NumInstrumentedGEPs, "Number of GEP pointer-arithmetic operations instrumented");
namespace {
@@ -65,12 +72,45 @@ class LowFatSanitizer {
bool instrumentMemoryAccess(Instruction *I, Value *Ptr, Type *AccessTy);
bool instrumentMemoryRange(Instruction *I, Value *Ptr, Value *Size,
bool IsWrite);
-
- // Constants (must match lf_config.h)
- static constexpr uint64_t RegionBase = 0x100000000000ULL;
- static constexpr uint64_t RegionSizeLog = 32;
+ bool instrumentGEP(GetElementPtrInst *GEP);
+
+ // Emit the OOB-check block given a pre-computed (Base, AllocSize, PtrInt).
+ void emitOobCheck(IRBuilder<> &IRB, Value *PtrInt, Value *Base,
+ Value *AllocSize, uint64_t FixedAccessSize,
+ Value *DynAccessSize, Instruction *InsertBefore,
+ bool IsWrite);
+
+#ifdef LOWFAT_CUSTOM_CONFIG
+ // Build the IR to compute (AllocSize, Base) using runtime table lookups when
+ // the region index is only known at runtime.
+ std::pair<Value *, Value *> emitDynamicBaseMagic(IRBuilder<> &IRB,
+ Value *PtrInt,
+ Value *RegionIndex);
+
+ // Lazily get or create the global arrays that mirror the generated tables.
+ GlobalVariable *getSizesTable();
+ GlobalVariable *getMagicsTable();
+ GlobalVariable *getIsPow2Table();
+ GlobalVariable *getMasksTable();
+
+ GlobalVariable *SizesTableGV = nullptr;
+ GlobalVariable *MagicsTableGV = nullptr;
+ GlobalVariable *IsPow2TableGV = nullptr;
+ GlobalVariable *MasksTableGV = nullptr;
+#endif
+
+ // Constants (kept in sync with lf_config.h / lf_config_generated.h)
+#ifdef LOWFAT_CUSTOM_CONFIG
+ static constexpr uint64_t RegionBase = 0x100000000000ULL;
+ static constexpr uint64_t RegionSizeLog = LOWFAT_REGION_SIZE_LOG; // 35
+ static constexpr uint64_t NumSizeClasses = LOWFAT_NUM_SIZE_CLASSES;
+ static constexpr uint64_t MinSizeLog = 4; // unused in custom mode
+#else
+ static constexpr uint64_t RegionBase = 0x100000000000ULL;
+ static constexpr uint64_t RegionSizeLog = 32;
static constexpr uint64_t NumSizeClasses = 27; // kMaxSizeLog(30) - kMinSizeLog(4) + 1
- static constexpr uint64_t MinSizeLog = 4;
+ static constexpr uint64_t MinSizeLog = 4;
+#endif
};
FunctionCallee LowFatSanitizer::getReportOobFn() {
@@ -107,55 +147,218 @@ FunctionCallee LowFatSanitizer::getWarnOobFn() {
return WarnOobFn;
}
+#ifdef LOWFAT_CUSTOM_CONFIG
+// ---------------------------------------------------------------------------
+// Custom-config pass helpers: table-accessor lazy initializers
+// ---------------------------------------------------------------------------
+//
+// We mirror the four kLowFatGen* arrays from lf_config_generated.h as LLVM
+// GlobalVariable constants embedded inside the module. This lets the
+// optimiser see them as constant loads and fold them through inlining.
+//
+// Arrays are initialised once (lazy, per-module) with the same values that
+// lf_config_gen baked into the header.
+
+static GlobalVariable *makeConstantArray(Module &M, StringRef Name,
+ ArrayRef<uint64_t> Data,
+ Type *ElemTy) {
+ SmallVector<Constant *, 64> Elems;
+ for (uint64_t V : Data)
+ Elems.push_back(ConstantInt::get(ElemTy, V));
+ auto *ArrayTy = ArrayType::get(ElemTy, Elems.size());
+ auto *Init = ConstantArray::get(ArrayTy, Elems);
+ auto *GV = new GlobalVariable(M, ArrayTy, /*isConstant=*/true,
+ GlobalValue::PrivateLinkage, Init, Name);
+ GV->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
+ return GV;
+}
+
+GlobalVariable *LowFatSanitizer::getSizesTable() {
+ if (!SizesTableGV) {
+ SmallVector<uint64_t, 64> D(kLowFatGenSizes,
+ kLowFatGenSizes + LOWFAT_NUM_SIZE_CLASSES);
+ SizesTableGV = makeConstantArray(M, "__lf_gen_sizes", D,
+ Type::getInt64Ty(M.getContext()));
+ }
+ return SizesTableGV;
+}
+
+GlobalVariable *LowFatSanitizer::getMagicsTable() {
+ if (!MagicsTableGV) {
+ SmallVector<uint64_t, 64> D(kLowFatGenMagics,
+ kLowFatGenMagics + LOWFAT_NUM_SIZE_CLASSES);
+ MagicsTableGV = makeConstantArray(M, "__lf_gen_magics", D,
+ Type::getInt64Ty(M.getContext()));
+ }
+ return MagicsTableGV;
+}
+
+GlobalVariable *LowFatSanitizer::getIsPow2Table() {
+ if (!IsPow2TableGV) {
+ SmallVector<uint64_t, 64> D;
+ for (int i = 0; i < LOWFAT_NUM_SIZE_CLASSES; ++i)
+ D.push_back((uint64_t)kLowFatGenIsPow2[i]);
+ IsPow2TableGV = makeConstantArray(M, "__lf_gen_ispow2", D,
+ Type::getInt8Ty(M.getContext()));
+ }
+ return IsPow2TableGV;
+}
+
+GlobalVariable *LowFatSanitizer::getMasksTable() {
+ if (!MasksTableGV) {
+ SmallVector<uint64_t, 64> D(kLowFatGenMasks,
+ kLowFatGenMasks + LOWFAT_NUM_SIZE_CLASSES);
+ MasksTableGV = makeConstantArray(M, "__lf_gen_masks", D,
+ Type::getInt64Ty(M.getContext()));
+ }
+ return MasksTableGV;
+}
+
+// ---------------------------------------------------------------------------
+// emitDynamicBaseMagic
+//
+// Given a runtime RegionIndex, emit IR that loads the per-class size and
+// magic from the embedded tables and returns (AllocSize, Base) as IntptrTy.
+//
+// Generated IR (conceptually):
+//
+// %alloc_size = load i64, ptr getelementptr(__lf_gen_sizes, 0, %region_idx)
+// %magic = load i64, ptr getelementptr(__lf_gen_magics, 0, %region_idx)
+// %is_pow2 = load i8, ptr getelementptr(__lf_gen_ispow2, 0, %region_idx)
+// %mask = load i64, ptr getelementptr(__lf_gen_masks, 0, %region_idx)
+//
+// ; AND path (POW2 fast path)
+// %base_and = and i64 %ptr, %mask
+//
+// ; MUL path (non-POW2 magic multiply)
+// %ptr128 = zext i64 %ptr to i128
+// %magic128 = zext i64 %magic to i128
+// %mul128 = mul i128 %ptr128, %magic128
+// %idx128 = lshr i128 %mul128, 64
+// %idx = trunc i128 %idx128 to i64
+// %base_mul = mul i64 %idx, %alloc_size
+//
+// ; Select based on is_pow2 flag
+// %is_pow2_i1 = trunc i8 %is_pow2 to i1
+// %base = select i1 %is_pow2_i1, i64 %base_and, i64 %base_mul
+// ---------------------------------------------------------------------------
+std::pair<Value *, Value *>
+LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
+ Value *RegionIndex) {
+ LLVMContext &Ctx = M.getContext();
+ Type *I64Ty = Type::getInt64Ty(Ctx);
+ Type *I128Ty = Type::getInt128Ty(Ctx);
+ Type *I8Ty = Type::getInt8Ty(Ctx);
+
+ // Helper: GEP + load from a GlobalVariable array at runtime index.
+ auto loadFromTable = [&](GlobalVariable *GV, Type *ElemTy,
+ Value *Idx) -> Value * {
+ Value *Zero = ConstantInt::get(Type::getInt64Ty(Ctx), 0);
+ // Extend RegionIndex to i64 if it's a different width
+ Value *Idx64 = IRB.CreateZExtOrTrunc(Idx, I64Ty);
+ Value *GEP = IRB.CreateInBoundsGEP(GV->getValueType(), GV,
+ {Zero, Idx64});
+ return IRB.CreateLoad(ElemTy, GEP);
+ };
+
+ Value *AllocSize64 = loadFromTable(getSizesTable(), I64Ty, RegionIndex);
+ Value *Magic64 = loadFromTable(getMagicsTable(), I64Ty, RegionIndex);
+ Value *IsPow2_8 = loadFromTable(getIsPow2Table(), I8Ty, RegionIndex);
+ Value *Mask64 = loadFromTable(getMasksTable(), I64Ty, RegionIndex);
+
+ // Narrow to IntptrTy (which is i64 on 64-bit targets)
+ Value *AllocSize = IRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
+ Value *Mask = IRB.CreateZExtOrTrunc(Mask64, IntptrTy);
+
+ // --- AND (POW2) base ---
+ Value *BaseAnd = IRB.CreateAnd(PtrInt, Mask);
+
+ // --- MUL (non-POW2) base ---
+ Value *Ptr128 = IRB.CreateZExt(PtrInt, I128Ty);
+ Value *Magic128 = IRB.CreateZExt(IRB.CreateZExtOrTrunc(Magic64, IntptrTy),
+ I128Ty);
+ Value *Mul128 = IRB.CreateMul(Ptr128, Magic128);
+ Value *Idx128 = IRB.CreateLShr(Mul128, ConstantInt::get(I128Ty, 64));
+ Value *Idx = IRB.CreateTrunc(Idx128, IntptrTy);
+ Value *BaseMul = IRB.CreateMul(Idx, AllocSize);
+
+ // Select between the two paths
+ Value *IsPow2_1 = IRB.CreateTrunc(IsPow2_8, Type::getInt1Ty(Ctx));
+ Value *Base = IRB.CreateSelect(IsPow2_1, BaseAnd, BaseMul);
+
+ return {AllocSize, Base};
+}
+#endif // LOWFAT_CUSTOM_CONFIG
+
bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Type *AccessTy) {
TypeSize AccessSize = DL.getTypeStoreSize(AccessTy);
if (AccessSize.isScalable())
return false;
+ uint64_t FixedAccessSize = AccessSize.getFixedValue();
IRBuilder<> IRB(I);
Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
// 1. Get region index: (Ptr - RegionBase) >> RegionSizeLog
Value *RegionBaseVal = ConstantInt::get(IntptrTy, RegionBase);
- Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
- Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
+ Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
+ Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
- // 2. Check if LowFat pointer: Region < NumSizeClasses
- Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
- Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+ // 2. Check if LowFat pointer: RegionIndex < NumSizeClasses
+ Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
+ Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
- // 3. Compute bounds inside the 'then' block
+ bool IsWrite = isa<StoreInst>(I) || isa<AtomicRMWInst>(I) ||
+ isa<AtomicCmpXchgInst>(I);
+
+#ifdef LOWFAT_CUSTOM_CONFIG
+ // --- Custom config: dual-path (AND vs. magic multiply) ---
+ // Emit dynamic table-lookup path; the static constant-folded path would
+ // require knowing the region index at IR-construction time, which is only
+ // possible for stack/global accesses. Heap accesses always go through here.
+ auto [AllocSize, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
+
+ Value *AccessSizeVal = ConstantInt::get(IntptrTy, FixedAccessSize);
+ Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, AccessSizeVal);
+ Value *End = ThenIRB.CreateAdd(Base, AllocSize);
+ Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
+
+ Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
+ IRBuilder<> OobIRB(OobTerm);
+ FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
+ Type *I8Ty = Type::getInt8Ty(M.getContext());
+ Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
+ OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
+#else
+ // --- POW2-only mode: existing shift-and-mask logic ---
Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
- Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
- Value *SizeOne = ConstantInt::get(IntptrTy, 1);
- Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
- Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
- Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
- Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
- Value *End = ThenIRB.CreateAdd(Base, AllocSize);
-
- // 4. Check access: Ptr + AccessSize <= End
- Value *AccessSizeVal = ConstantInt::get(IntptrTy, AccessSize.getFixedValue());
- Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, AccessSizeVal);
- Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
+ Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
+ Value *SizeOne = ConstantInt::get(IntptrTy, 1);
+ Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
+ Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
+ Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
+ Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
+ Value *End = ThenIRB.CreateAdd(Base, AllocSize);
+
+ Value *AccessSizeVal = ConstantInt::get(IntptrTy, FixedAccessSize);
+ Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, AccessSizeVal);
+ Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
IRBuilder<> OobIRB(OobTerm);
-
FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
Type *I8Ty = Type::getInt8Ty(M.getContext());
- bool IsWrite = isa<StoreInst>(I) || isa<AtomicRMWInst>(I) ||
- isa<AtomicCmpXchgInst>(I);
Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
+#endif
- if (isa<LoadInst>(I)) NumInstrumentedLoads++;
- else if (isa<StoreInst>(I)) NumInstrumentedStores++;
- else NumInstrumentedAtomics++;
+ if (isa<LoadInst>(I)) NumInstrumentedLoads++;
+ else if (isa<StoreInst>(I)) NumInstrumentedStores++;
+ else NumInstrumentedAtomics++;
return true;
}
@@ -163,34 +366,37 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
Value *Size, bool IsWrite) {
IRBuilder<> IRB(I);
- Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
+ Value *PtrInt = IRB.CreatePtrToInt(Ptr, IntptrTy);
Value *SizeInt = IRB.CreateZExtOrTrunc(Size, IntptrTy);
Value *RegionBaseVal = ConstantInt::get(IntptrTy, RegionBase);
- Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
- Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
+ Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
+ Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
- Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+ Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
+#ifdef LOWFAT_CUSTOM_CONFIG
+ auto [AllocSize, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
+#else
Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
- Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
- Value *SizeOne = ConstantInt::get(IntptrTy, 1);
- Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
- Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
- Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
- Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
- Value *End = ThenIRB.CreateAdd(Base, AllocSize);
-
+ Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
+ Value *SizeOne = ConstantInt::get(IntptrTy, 1);
+ Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
+ Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
+ Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
+ Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
+#endif
+
+ Value *End = ThenIRB.CreateAdd(Base, AllocSize);
Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, SizeInt);
- Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
+ Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
IRBuilder<> OobIRB(OobTerm);
-
FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
Type *I8Ty = Type::getInt8Ty(M.getContext());
Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
@@ -200,6 +406,81 @@ bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
return true;
}
+// ---------------------------------------------------------------------------
+// instrumentGEP
+//
+// Instruments a GetElementPtr instruction to catch OOB pointer arithmetic
+// before the out-of-bounds pointer can escape to a neighbouring allocation
+// slot (where a load/store check would misidentify it as valid).
+//
+// Key insight: use the SOURCE pointer's allocation bounds, not the result's.
+//
+// ptr = p + 48 (src=p, result=p+48, alloc=48)
+//
+// Load/store check on p+48:
+// GetBase(p+48) = p+48 ← attributed to next slot
+// End = p+96 → p+49 ≤ p+96 → NOT OOB (false negative)
+//
+// GEP check using src=p:
+// GetBase(p) = p, End = p+48
+// result p+48 ≥ End → OOB (detected!)
+// ---------------------------------------------------------------------------
+bool LowFatSanitizer::instrumentGEP(GetElementPtrInst *GEP) {
+ // We need an insertion point after the GEP so we can use its result value.
+ Instruction *InsertPt = GEP->getNextNode();
+ if (!InsertPt)
+ return false;
+
+ IRBuilder<> IRB(InsertPt);
+
+ // SOURCE pointer — determines the allocation the GEP started from.
+ Value *SrcPtr = GEP->getPointerOperand();
+ Value *SrcInt = IRB.CreatePtrToInt(SrcPtr, IntptrTy);
+
+ // RESULT pointer — what we're checking stays within [Base, Base+AllocSize).
+ Value *ResInt = IRB.CreatePtrToInt(GEP, IntptrTy);
+
+ // 1. Is the source a LowFat pointer?
+ Value *RegionBaseVal = ConstantInt::get(IntptrTy, RegionBase);
+ Value *RegionOffset = IRB.CreateSub(SrcInt, RegionBaseVal);
+ Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
+ Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
+ Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+
+ Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, InsertPt, false);
+ IRBuilder<> ThenIRB(ThenTerm);
+
+ // 2. Compute Base and AllocSize from the SOURCE pointer.
+#ifdef LOWFAT_CUSTOM_CONFIG
+ auto [AllocSize, Base] = emitDynamicBaseMagic(ThenIRB, SrcInt, RegionIndex);
+#else
+ Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
+ Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
+ Value *SizeOne = ConstantInt::get(IntptrTy, 1);
+ Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
+ Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
+ Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
+ Value *Base = ThenIRB.CreateAnd(SrcInt, Mask);
+#endif
+
+ // 3. OOB if result underflows (< Base) or reaches/passes the end (>= Base+AllocSize).
+ Value *End = ThenIRB.CreateAdd(Base, AllocSize);
+ Value *TooLow = ThenIRB.CreateICmpULT(ResInt, Base);
+ Value *TooHigh = ThenIRB.CreateICmpUGE(ResInt, End);
+ Value *IsOOB = ThenIRB.CreateOr(TooLow, TooHigh);
+
+ Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
+ IRBuilder<> OobIRB(OobTerm);
+ FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
+ Type *I8Ty = Type::getInt8Ty(M.getContext());
+ // GEPs are neither reads nor writes; report as read (0).
+ OobIRB.CreateCall(OobFn,
+ {ResInt, Base, AllocSize, ConstantInt::get(I8Ty, 0)});
+
+ NumInstrumentedGEPs++;
+ return true;
+}
+
bool LowFatSanitizer::instrumentFunction(Function &F) {
bool Modified = false;
SmallVector<Instruction *, 16> ToInstrument;
@@ -211,6 +492,8 @@ bool LowFatSanitizer::instrumentFunction(Function &F) {
ToInstrument.push_back(&I);
else if (isa<MemIntrinsic>(&I))
ToInstrument.push_back(&I);
+ else if (isa<GetElementPtrInst>(&I))
+ ToInstrument.push_back(&I);
}
}
@@ -228,7 +511,8 @@ bool LowFatSanitizer::instrumentFunction(Function &F) {
else if (auto *MT = dyn_cast<MemTransferInst>(I)) {
Modified |= instrumentMemoryRange(I, MT->getDest(), MT->getLength(), true);
Modified |= instrumentMemoryRange(I, MT->getSource(), MT->getLength(), false);
- }
+ } else if (auto *GEP = dyn_cast<GetElementPtrInst>(I))
+ Modified |= instrumentGEP(GEP);
}
return Modified;
}
diff --git a/llvm/runtimes/CMakeLists.txt b/llvm/runtimes/CMakeLists.txt
index f0ef353a2c66c..140a1c6aa38a5 100644
--- a/llvm/runtimes/CMakeLists.txt
+++ b/llvm/runtimes/CMakeLists.txt
@@ -547,6 +547,12 @@ if(build_runtimes)
if(CMAKE_PROGRAM_PATH)
list(APPEND extra_cmake_args "-DCMAKE_PROGRAM_PATH=${CMAKE_PROGRAM_PATH}")
endif()
+ # Pass custom / reset LowFat Configuration file path to compiler-rt.
+ if(LOWFAT_SIZES_CFG)
+ list(APPEND extra_cmake_args "-DLOWFAT_SIZES_CFG=${LOWFAT_SIZES_CFG}")
+ else()
+ list(APPEND extra_cmake_args "-ULOWFAT_SIZES_CFG")
+ endif()
# TODO: We need to consider passing it as '-DRUNTIMES_x86_64_LLVM_ENABLE_RUNTIMES'.
if("libclc" IN_LIST LLVM_ENABLE_RUNTIMES)
>From 639359c4fcfc025c966d6941d62aa562ad0de4b5 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 7 Mar 2026 21:42:07 +0800
Subject: [PATCH 36/55] [LowFat] Add tests for custom mem sizes
---
compiler-rt/test/lowfat/CMakeLists.txt | 7 +++
compiler-rt/test/lowfat/lit.cfg.py | 6 +++
compiler-rt/test/lowfat/lit.site.cfg.py.in | 2 +
compiler-rt/test/lowfat/non_pow2_inbounds.cpp | 20 ++++++++
compiler-rt/test/lowfat/non_pow2_oob.cpp | 25 ++++++++++
.../test/lowfat/non_pow2_oob_boundary.cpp | 49 +++++++++++++++++++
6 files changed, 109 insertions(+)
create mode 100644 compiler-rt/test/lowfat/non_pow2_inbounds.cpp
create mode 100644 compiler-rt/test/lowfat/non_pow2_oob.cpp
create mode 100644 compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp
diff --git a/compiler-rt/test/lowfat/CMakeLists.txt b/compiler-rt/test/lowfat/CMakeLists.txt
index 089d35f63d163..f1670497c1dca 100644
--- a/compiler-rt/test/lowfat/CMakeLists.txt
+++ b/compiler-rt/test/lowfat/CMakeLists.txt
@@ -19,6 +19,13 @@ foreach(arch ${LOWFAT_TEST_ARCH})
string(TOUPPER ${arch} ARCH_UPPER_CASE)
set(CONFIG_NAME ${ARCH_UPPER_CASE}${OS_NAME}Config)
+ # Propagate custom-config flag to lit as a Python boolean literal.
+ if(LOWFAT_SIZES_CFG)
+ set(LOWFAT_CUSTOM_CONFIG_ENABLED "True")
+ else()
+ set(LOWFAT_CUSTOM_CONFIG_ENABLED "False")
+ endif()
+
configure_lit_site_cfg(
${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in
${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_NAME}/lit.site.cfg.py
diff --git a/compiler-rt/test/lowfat/lit.cfg.py b/compiler-rt/test/lowfat/lit.cfg.py
index 2c83322d99b94..ba7dd85d4bd13 100644
--- a/compiler-rt/test/lowfat/lit.cfg.py
+++ b/compiler-rt/test/lowfat/lit.cfg.py
@@ -70,3 +70,9 @@ def build_invocation(flags):
# platform truly isn't supported.
if hasattr(config, "target_os"):
config.unsupported = True
+
+# Expose 'lowfat-custom-config' feature when the runtime was built with a
+# custom sizes.cfg (i.e. -DLOWFAT_SIZES_CFG was set at cmake time).
+# Tests guarded with REQUIRES: lowfat-custom-config are skipped otherwise.
+if getattr(config, "lowfat_custom_config", False):
+ config.available_features.add("lowfat-custom-config")
diff --git a/compiler-rt/test/lowfat/lit.site.cfg.py.in b/compiler-rt/test/lowfat/lit.site.cfg.py.in
index 1dbb2cb1367b6..c89b8d682c688 100644
--- a/compiler-rt/test/lowfat/lit.site.cfg.py.in
+++ b/compiler-rt/test/lowfat/lit.site.cfg.py.in
@@ -5,6 +5,8 @@ config.name_suffix = "@LOWFAT_TEST_CONFIG_SUFFIX@"
config.target_cflags = "@LOWFAT_TEST_TARGET_CFLAGS@"
config.clang = "@LOWFAT_TEST_TARGET_CC@"
config.target_arch = "@LOWFAT_TEST_TARGET_ARCH@"
+# True when -DLOWFAT_SIZES_CFG was set at cmake time.
+config.lowfat_custom_config = @LOWFAT_CUSTOM_CONFIG_ENABLED@
# Load common config for all compiler-rt lit tests.
lit_config.load_config(config, "@COMPILER_RT_BINARY_DIR@/test/lit.common.configured")
diff --git a/compiler-rt/test/lowfat/non_pow2_inbounds.cpp b/compiler-rt/test/lowfat/non_pow2_inbounds.cpp
new file mode 100644
index 0000000000000..633aeeb9f1e38
--- /dev/null
+++ b/compiler-rt/test/lowfat/non_pow2_inbounds.cpp
@@ -0,0 +1,20 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t
+
+// Verify that an in-bounds write to the last byte of a 48-byte allocation
+// does not trigger a false positive OOB error.
+//
+// REQUIRES: lowfat-custom-config
+
+#include <cstdlib>
+
+int main() {
+ char *p = (char *)malloc(48);
+ if (!p) return 1;
+
+ // Write to the very last byte of the 48-byte allocation.
+ // This must NOT trigger a LowFat error.
+ p[47] = 'x';
+
+ free(p);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/non_pow2_oob.cpp b/compiler-rt/test/lowfat/non_pow2_oob.cpp
new file mode 100644
index 0000000000000..a79245447d67e
--- /dev/null
+++ b/compiler-rt/test/lowfat/non_pow2_oob.cpp
@@ -0,0 +1,25 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
+
+// Verify that an OOB write 2 bytes past a 48-byte allocation is detected.
+// This test exercises the non-POW2 magic-multiply path: without custom config
+// the allocation would be silently rounded to 64 bytes, making p[50] valid.
+//
+// REQUIRES: lowfat-custom-config
+
+#include <cstdlib>
+
+int main() {
+ char *p = (char *)malloc(48);
+ if (!p) return 1;
+
+ // Write 8 bytes starting at offset 44.
+ // Bytes 44-51 will cross the 48-byte allocation boundary, straddling
+ // into the next object grid. This triggers the OOB error.
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ double *val = (double *)(p + 44);
+ *val = 1.0;
+
+ free(p);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp b/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp
new file mode 100644
index 0000000000000..23fd8bee42568
--- /dev/null
+++ b/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp
@@ -0,0 +1,49 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
+
+// Demonstrates GEP-level (pointer arithmetic) instrumentation catching an OOB
+// pointer that escapes to a neighbouring allocation slot — a case where
+// load/store checking alone produces a false negative.
+//
+// SCENARIO
+// --------
+// p is a 48-byte LowFat allocation at address k*48 (absolute-zero aligned).
+// p+48 = (k+1)*48 is the exact start of the NEXT 48-byte slot.
+//
+// Load/store check on *(p+48):
+// GetBase(p+48) = (k+1)*48 = p+48 <-- attributed to NEXT slot
+// End = p+48 + 48 = p+96
+// AccessEnd = p+48 + 1 = p+49
+// p+49 <= p+96 --> NOT OOB (false negative — sink() would not catch it)
+//
+// GEP check at the arithmetic (p + 48) itself:
+// GetBase(p) = k*48 = p <-- uses SOURCE pointer's bounds
+// End = p + 48
+// result p+48 >= End --> OOB (detected before escape!)
+//
+// By checking at the point of pointer arithmetic, the error is caught before
+// the OOB pointer reaches sink(), regardless of which slot it happens to land in.
+//
+// REQUIRES: lowfat-custom-config
+
+#include <stdlib.h>
+
+// Prevent inlining so the OOB pointer is forced to cross a function boundary,
+// making the "escape" explicit. The store inside sink() is NOT detectable by
+// load/store checking alone (GetBase(q) = q, so it appears in-bounds).
+__attribute__((noinline))
+static void sink(volatile char *q) { *q = 'x'; }
+
+int main() {
+ char *p = (char *)malloc(48);
+ if (!p) return 1;
+
+ // GEP check: p+48 is computed using p's bounds (End = p+48).
+ // result == End --> OOB detected here, before the pointer reaches sink().
+ // CHECK: LOWFAT ERROR: out-of-bounds error detected!
+ sink(p + 48);
+
+ free(p);
+ return 0;
+}
+
>From 528eadbca916af9d45ca67375642096b71050507 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 7 Mar 2026 21:48:25 +0800
Subject: [PATCH 37/55] [LowFat] Refactor LowFat custom cmake propagation into
sep file
---
.../Transforms/Instrumentation/CMakeLists.txt | 42 +------------------
.../Instrumentation/LowFatPassConfig.cmake | 41 ++++++++++++++++++
2 files changed, 42 insertions(+), 41 deletions(-)
create mode 100644 llvm/lib/Transforms/Instrumentation/LowFatPassConfig.cmake
diff --git a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
index b6c2d11511faf..023d00d410c92 100644
--- a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
+++ b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
@@ -46,44 +46,4 @@ add_llvm_component_library(LLVMInstrumentation
ProfileData
)
-if(LOWFAT_SIZES_CFG)
- # LLVM pass needs the generated header too. Since runtimes build AFTER LLVM,
- # we must generate a local copy of the header for the pass to use.
- #
- # We use CMAKE_HOST_C_COMPILER (not add_executable) so that the generator is
- # always built for the *host* machine. add_executable would produce a
- # target-architecture binary on cross-compile setups (e.g. building for
- # AArch64 on an x86 host) which cannot be executed during the build.
- set(LOWFAT_CONFIG_GEN_SRC
- ${LLVM_MAIN_SRC_DIR}/../compiler-rt/lib/lowfat/tools/lf_config_gen.c)
- set(LOWFAT_CONFIG_GEN_BIN
- ${CMAKE_CURRENT_BINARY_DIR}/lf_config_gen_llvm${CMAKE_EXECUTABLE_SUFFIX})
- set(LOWFAT_GENERATED_HEADER ${CMAKE_CURRENT_BINARY_DIR}/lf_config_generated.h)
-
- add_custom_command(
- OUTPUT ${LOWFAT_CONFIG_GEN_BIN}
- COMMAND ${CMAKE_C_COMPILER} -std=c11 -O2
- ${LOWFAT_CONFIG_GEN_SRC}
- -o ${LOWFAT_CONFIG_GEN_BIN}
- DEPENDS ${LOWFAT_CONFIG_GEN_SRC}
- COMMENT "Compiling lf_config_gen host tool (for LLVM pass)"
- VERBATIM
- )
-
- add_custom_command(
- OUTPUT ${LOWFAT_GENERATED_HEADER}
- COMMAND ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG} ${LOWFAT_GENERATED_HEADER}
- DEPENDS ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG}
- COMMENT "Generating LowFat size config for LLVM pass"
- VERBATIM
- )
-
- add_custom_target(lowfat_pass_config DEPENDS ${LOWFAT_GENERATED_HEADER})
- add_dependencies(LLVMInstrumentation lowfat_pass_config)
-
- set_property(SOURCE LowFatSanitizer.cpp APPEND PROPERTY COMPILE_DEFINITIONS "LOWFAT_CUSTOM_CONFIG=1")
- set_property(SOURCE LowFatSanitizer.cpp APPEND PROPERTY OBJECT_DEPENDS ${LOWFAT_GENERATED_HEADER})
-
- # Tell the compiler where to find the generated header
- target_include_directories(LLVMInstrumentation PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
-endif()
+include(${CMAKE_CURRENT_LIST_DIR}/LowFatPassConfig.cmake)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatPassConfig.cmake b/llvm/lib/Transforms/Instrumentation/LowFatPassConfig.cmake
new file mode 100644
index 0000000000000..037415da4a488
--- /dev/null
+++ b/llvm/lib/Transforms/Instrumentation/LowFatPassConfig.cmake
@@ -0,0 +1,41 @@
+if(LOWFAT_SIZES_CFG)
+ # LLVM pass needs the generated header too. Since runtimes build AFTER LLVM,
+ # we must generate a local copy of the header for the pass to use.
+ #
+ # We use CMAKE_C_COMPILER (not add_executable) so that the generator is
+ # always built for the host machine. add_executable would produce a
+ # target-architecture binary on cross-compile setups (e.g. building for
+ # AArch64 on an x86 host) which cannot be executed during the build.
+ set(LOWFAT_CONFIG_GEN_SRC
+ ${LLVM_MAIN_SRC_DIR}/../compiler-rt/lib/lowfat/tools/lf_config_gen.c)
+ set(LOWFAT_CONFIG_GEN_BIN
+ ${CMAKE_CURRENT_BINARY_DIR}/lf_config_gen_llvm${CMAKE_EXECUTABLE_SUFFIX})
+ set(LOWFAT_GENERATED_HEADER ${CMAKE_CURRENT_BINARY_DIR}/lf_config_generated.h)
+
+ add_custom_command(
+ OUTPUT ${LOWFAT_CONFIG_GEN_BIN}
+ COMMAND ${CMAKE_C_COMPILER} -std=c11 -O2
+ ${LOWFAT_CONFIG_GEN_SRC}
+ -o ${LOWFAT_CONFIG_GEN_BIN}
+ DEPENDS ${LOWFAT_CONFIG_GEN_SRC}
+ COMMENT "Compiling lf_config_gen host tool (for LLVM pass)"
+ VERBATIM
+ )
+
+ add_custom_command(
+ OUTPUT ${LOWFAT_GENERATED_HEADER}
+ COMMAND ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG} ${LOWFAT_GENERATED_HEADER}
+ DEPENDS ${LOWFAT_CONFIG_GEN_BIN} ${LOWFAT_SIZES_CFG}
+ COMMENT "Generating LowFat size config for LLVM pass"
+ VERBATIM
+ )
+
+ add_custom_target(lowfat_pass_config DEPENDS ${LOWFAT_GENERATED_HEADER})
+ add_dependencies(LLVMInstrumentation lowfat_pass_config)
+
+ set_property(SOURCE LowFatSanitizer.cpp APPEND PROPERTY COMPILE_DEFINITIONS "LOWFAT_CUSTOM_CONFIG=1")
+ set_property(SOURCE LowFatSanitizer.cpp APPEND PROPERTY OBJECT_DEPENDS ${LOWFAT_GENERATED_HEADER})
+
+ # Tell the compiler where to find the generated header.
+ target_include_directories(LLVMInstrumentation PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+endif()
>From 808047bc612cfc3d5daf19d56d6b07a45963845c Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 7 Mar 2026 21:56:00 +0800
Subject: [PATCH 38/55] [LowFat] Set fast mode as default in tests
---
compiler-rt/test/lowfat/lit.cfg.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/compiler-rt/test/lowfat/lit.cfg.py b/compiler-rt/test/lowfat/lit.cfg.py
index ba7dd85d4bd13..948a4f0620d29 100644
--- a/compiler-rt/test/lowfat/lit.cfg.py
+++ b/compiler-rt/test/lowfat/lit.cfg.py
@@ -41,19 +41,17 @@ def build_invocation(flags):
# Base flags
lowfat_base = ["-fsanitize=lowfat"]
-# Fast mode (OptimizerLastEP)
-lowfat_fast = lowfat_base + ["-mllvm", "-lowfat-mode=fast"]
-# Safe mode (Barrier at PipelineStartEP + instrument at OptimizerLastEP)
+# safe mode (fast mode is the default)
lowfat_safe = lowfat_base + ["-mllvm", "-lowfat-mode=safe"]
-config.substitutions.append(("%clangxx_lowfat ", build_invocation(lowfat_fast)))
+config.substitutions.append(("%clangxx_lowfat ", build_invocation(lowfat_base)))
config.substitutions.append(("%clangxx_lowfat_safe ", build_invocation(lowfat_safe)))
# Recover mode versions
config.substitutions.append(
(
"%clangxx_lowfat_recover ",
- build_invocation(lowfat_fast + ["-fsanitize-recover=lowfat"]),
+ build_invocation(lowfat_base + ["-fsanitize-recover=lowfat"]),
)
)
config.substitutions.append(
>From bf1dadf6f6860ce6d0e2a661aafcee259436b5e1 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 7 Mar 2026 21:58:43 +0800
Subject: [PATCH 39/55] [LowFat] Update comments in tests
---
compiler-rt/test/lowfat/dead_load.cpp | 3 ++-
.../test/lowfat/mode_baseline_all_detect.cpp | 7 ++++---
compiler-rt/test/lowfat/mode_diff_pure_call.cpp | 16 ++++++++--------
3 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/compiler-rt/test/lowfat/dead_load.cpp b/compiler-rt/test/lowfat/dead_load.cpp
index 15e30304506cd..8d4bd70c8c057 100644
--- a/compiler-rt/test/lowfat/dead_load.cpp
+++ b/compiler-rt/test/lowfat/dead_load.cpp
@@ -1,7 +1,8 @@
// RUN: %clangxx_lowfat -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ALL
// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ALL
-// Verifies that a genuine OOB heap read is detected in both Fast and Safe mode
+// Verifies that a genuine OOB heap read is detected in both default-fast and
+// safe mode
// when the loaded value is actually used (returned and passed to printf).
#include <cstdio>
diff --git a/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp b/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
index 6fdf82f3acaf9..688d09e111f76 100644
--- a/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
+++ b/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
@@ -3,9 +3,10 @@
// RUN: %clangxx_lowfat -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
// RUN: %clangxx_lowfat_safe -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
-// Baseline: all three modes detect an OOB access whose result IS observable
+// Baseline: both tested modes detect an OOB access whose result is observable.
// Contrast with mode_diff_pure_call.cpp where a discarded-result call lets
-// Fast eliminate the load before the LowFat pass even sees it.
+// default-fast mode (%clangxx_lowfat) eliminate the load before the LowFat
+// pass even sees it.
#include <cstdio>
#include <cstdlib>
@@ -16,7 +17,7 @@ int main() {
char *p = (char *)malloc(16);
// 8-byte (double) OOB read at offset 14: bytes [14, 22) overflow the
- // 16-byte LowFat slot [0, 16).
+ // 16-byte LowFat slot [0, 16).
sink = (char)(*reinterpret_cast<volatile double *>(p + 14));
free(p);
diff --git a/compiler-rt/test/lowfat/mode_diff_pure_call.cpp b/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
index 5f3109ea81d02..3de0279497fb6 100644
--- a/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
+++ b/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
@@ -1,12 +1,14 @@
// RUN: %clangxx_lowfat -O3 %s -o %t && %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-MISS
// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-CATCH
-// Demonstrates a behavioral difference between Fast mode and Safe/Comprehensive mode.
+// Demonstrates a behavioral difference between default-fast (%clangxx_lowfat)
+// and safe mode (%clangxx_lowfat_safe).
//
// The scenario: a noinline function performs an OOB heap read but its return value
// is discarded by the caller.
//
-// Fast mode has no PipelineStartEP pass. LLVM's Dead Argument Elimination (DAE)
+// Default-fast mode has no PipelineStartEP pass. LLVM's Dead Argument
+// Elimination (DAE)
// sees peek()'s return value is unused at all call sites and rewrites:
// ret %loaded_val → ret undef
// The load becomes dead and is DCE'd. The LowFat pass at OptimizerLastEP finds
@@ -20,17 +22,15 @@
// preventing DAE from marking the return value as dead. The load survives
// to OptimizerLastEP where LowFat instruments it → OOB is caught.
//
-// Comprehensive mode instruments at PipelineStartEP before any optimization,
-// so the check call is inserted before DAE has a chance to remove the load.
-//
// Expected outputs:
-// Fast (-O3): load DCE'd by DAE → no OOB check → program exits 0 → DONE
-// Safe (-O3): fake.use keeps load alive → OOB detected → LOWFAT ERROR
+// default-fast (-O3): load DCE'd by DAE -> no OOB check -> program exits 0 -> DONE
+// safe (-O3): fake.use keeps load alive -> OOB detected -> LOWFAT ERROR
#include <cstdio>
#include <cstdlib>
-// noinline: the optimizer cannot see the body from the call site in Fast mode,
+// noinline: the optimizer cannot see the body from the call site in default-fast
+// mode,
// so it performs inter-procedural attribute inference rather than inlining.
__attribute__((noinline))
static double peek(char *p) {
>From 47ab9c41dc49bb00ac90833db6e13ffc62d99fb7 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 7 Mar 2026 22:11:51 +0800
Subject: [PATCH 40/55] [LowFat] Simplify documentation for tests
---
compiler-rt/test/lowfat/basic_inbounds.cpp | 5 ++-
.../test/lowfat/cross_boundary_oob.cpp | 8 ++---
compiler-rt/test/lowfat/dead_load.cpp | 7 ++--
compiler-rt/test/lowfat/free_list_reuse.cpp | 4 +--
.../test/lowfat/inbounds_no_report.cpp | 3 +-
.../lowfat/malloc_intercepted_inbounds.cpp | 6 ++--
.../test/lowfat/memcpy_oob_dynamic_fatal.cpp | 5 ++-
.../test/lowfat/memcpy_oob_static_fatal.cpp | 6 ++--
.../test/lowfat/memcpy_oob_static_recover.cpp | 7 ++--
.../test/lowfat/memmove_oob_dynamic_fatal.cpp | 6 ++--
.../test/lowfat/memset_oob_dynamic_fatal.cpp | 5 ++-
.../test/lowfat/memset_oob_static_fatal.cpp | 5 ++-
.../test/lowfat/memset_oob_static_recover.cpp | 4 +--
.../test/lowfat/mode_baseline_all_detect.cpp | 7 ++--
.../test/lowfat/mode_diff_pure_call.cpp | 35 ++++---------------
compiler-rt/test/lowfat/non_pow2_inbounds.cpp | 6 ++--
compiler-rt/test/lowfat/non_pow2_oob.cpp | 8 ++---
.../test/lowfat/non_pow2_oob_boundary.cpp | 31 +++-------------
compiler-rt/test/lowfat/oob_read_fatal.cpp | 7 ++--
19 files changed, 51 insertions(+), 114 deletions(-)
diff --git a/compiler-rt/test/lowfat/basic_inbounds.cpp b/compiler-rt/test/lowfat/basic_inbounds.cpp
index 6ea912984b57a..4d06c914f7996 100644
--- a/compiler-rt/test/lowfat/basic_inbounds.cpp
+++ b/compiler-rt/test/lowfat/basic_inbounds.cpp
@@ -4,8 +4,7 @@
// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
-// Verify that the LowFat runtime initializes and basic in-bounds
-// allocations work without errors.
+// Basic in-bounds allocation test.
#include <cstdio>
@@ -17,7 +16,7 @@ int main() {
if (!arr)
return 1;
- // In-bounds accesses — should not trigger OOB
+ // In-bounds accesses should not trigger OOB.
arr[0] = 42;
arr[9] = 99;
diff --git a/compiler-rt/test/lowfat/cross_boundary_oob.cpp b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
index 46f428f3a95f7..6aa172698858d 100644
--- a/compiler-rt/test/lowfat/cross_boundary_oob.cpp
+++ b/compiler-rt/test/lowfat/cross_boundary_oob.cpp
@@ -4,14 +4,12 @@
// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
-// Verify that a cross-boundary out-of-bounds access is detected.
-// Writing 8 bytes starting at offset 12 of a 16-byte slot crosses
-// the slot boundary (bytes 12-19 > 16 bytes).
+// Cross-boundary OOB write must be reported.
extern "C" void *__lf_malloc(unsigned long size);
int main() {
- // Allocate exactly 16 bytes → 16-byte size class (smallest slot)
+ // Allocate exactly 16 bytes.
char *buf = (char *)__lf_malloc(16);
if (!buf)
return 1;
@@ -21,7 +19,7 @@ int main() {
buf[15] = 'B';
// Cross-boundary OOB: write 8 bytes at offset 12
- // ptr + 8 = buf+20 > buf+16 → out of bounds
+ // ptr + 8 = buf+20 > buf+16: out of bounds.
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
double *cross = (double *)(buf + 12);
*cross = 3.14;
diff --git a/compiler-rt/test/lowfat/dead_load.cpp b/compiler-rt/test/lowfat/dead_load.cpp
index 8d4bd70c8c057..b1211008a8c83 100644
--- a/compiler-rt/test/lowfat/dead_load.cpp
+++ b/compiler-rt/test/lowfat/dead_load.cpp
@@ -2,8 +2,7 @@
// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ALL
// Verifies that a genuine OOB heap read is detected in both default-fast and
-// safe mode
-// when the loaded value is actually used (returned and passed to printf).
+// safe mode when the loaded value is actually used (returned and printed).
#include <cstdio>
#include <cstdlib>
@@ -11,13 +10,13 @@
__attribute__((noinline))
double oob_read(char *p) {
// 8-byte read at offset 14 of a 16-byte allocation.
- // Bytes [14, 22) exceed the slot boundary [0, 16) → genuine OOB.
+ // Bytes [14, 22) exceed the slot boundary [0, 16): genuine OOB.
return *(double *)(p + 14);
}
int main() {
char *p = (char *)malloc(16);
- double val = oob_read(p); // return value kept live → load not DCE'd
+ double val = oob_read(p); // Return value kept live; load is not DCE'd.
// CHECK-ALL: LOWFAT ERROR: out-of-bounds error detected!
printf("val=%f\n", val);
free(p);
diff --git a/compiler-rt/test/lowfat/free_list_reuse.cpp b/compiler-rt/test/lowfat/free_list_reuse.cpp
index 2384eab006362..d1222b1e33845 100644
--- a/compiler-rt/test/lowfat/free_list_reuse.cpp
+++ b/compiler-rt/test/lowfat/free_list_reuse.cpp
@@ -4,7 +4,7 @@
// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
-// Verify that allocation and free list reuse works correctly, with no OOB errors.
+// Free-list reuse sanity test.
#include <cstdio>
@@ -12,7 +12,7 @@ extern "C" void *__lf_malloc(unsigned long size);
extern "C" void __lf_free(void *ptr);
int main() {
- // Allocate and free, then allocate again — should reuse from free list
+ // Allocate and free, then allocate again.
int *a = (int *)__lf_malloc(10 * sizeof(int));
a[0] = 1;
__lf_free(a);
diff --git a/compiler-rt/test/lowfat/inbounds_no_report.cpp b/compiler-rt/test/lowfat/inbounds_no_report.cpp
index 72b0fb07c63ec..b90741ee5e86f 100644
--- a/compiler-rt/test/lowfat/inbounds_no_report.cpp
+++ b/compiler-rt/test/lowfat/inbounds_no_report.cpp
@@ -4,8 +4,7 @@
// RUN: %clangxx_lowfat -O3 %s -o %t
// RUN: %run %t 2>&1 | FileCheck %s
-// Verify that purely in-bounds accesses — including memcpy and memset within
-// the allocation size — do not trigger any OOB report.
+// In-bounds accesses, memcpy, and memset should not report OOB.
#include <cstdio>
#include <cstdlib>
diff --git a/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp b/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp
index f8359b2d3f45c..6e929bac7c312 100644
--- a/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp
+++ b/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp
@@ -1,18 +1,18 @@
// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
-// Verify that malloc-intercepted allocations are correctly handled for in-bounds accesses.
+// Basic in-bounds test for malloc-intercepted allocations.
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main() {
- // malloc goes through the LowFat interceptor → produces a LowFat pointer.
+ // malloc goes through the LowFat interceptor and returns a LowFat pointer.
char *buf = (char *)malloc(32);
if (!buf) return 1;
- // In-bounds scalar accesses — first and last byte.
+ // In-bounds scalar accesses: first and last byte.
buf[0] = 'A';
buf[31] = 'Z';
diff --git a/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
index db8c65f25b918..c3505265263a1 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
@@ -4,8 +4,7 @@
// RUN: %clangxx_lowfat -fno-builtin-memcpy -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
-// Verify that memcpy writing past the end of a LowFat allocation is detected
-// in fatal mode, even when the size isn't a compile-time constant.
+// memcpy OOB write with non-constant size must be reported in fatal mode.
#include <string.h>
#include <stdlib.h>
@@ -18,7 +17,7 @@ int main(int argc, char **argv) {
// Use argc to prevent the optimizer from knowing the size at compile time.
// This forces a call to libc memcpy instead of llvm.memcpy.
size_t size = 16 + argc; // 17
-
+
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
// CHECK: operation = write
// CHECK: size = 16
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
index c95553fc80c2a..5f914a93726a7 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
@@ -1,9 +1,7 @@
// RUN: %clangxx_lowfat_safe -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
-// Verify that memcpy writing past the end of a LowFat allocation is detected
-// in fatal mode. The process must exit with a non-zero code (checked by
-// "not %run") and print the expected diagnostic.
+// memcpy OOB write must be reported in fatal mode.
#include <cstdlib>
#include <cstring>
@@ -15,7 +13,7 @@ int main() {
const char payload[32] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
- // memcpy of 32 bytes into a 16-byte allocation — overflows by 16 bytes.
+ // memcpy of 32 bytes into a 16-byte allocation overflows by 16 bytes.
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
memcpy(dst, payload, 32);
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp b/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
index 62316bf1a1a0e..7b7bcdbf51ff8 100644
--- a/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
+++ b/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
@@ -3,10 +3,7 @@
// RUN: %clangxx_lowfat_safe_recover -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat_safe_recover -O3 %s -o %t && %run %t 2>&1 | FileCheck %s
-// Verify that memcpy OOB in recover mode:
-// 1. Prints a WARNING (not ERROR).
-// 2. Execution continues past the call (process exits 0).
-// 3. The reported overflow is positive (access end past allocation end).
+// memcpy OOB write in recover mode must warn and continue.
#include <cstdio>
#include <cstdlib>
@@ -22,7 +19,7 @@ int main() {
// CHECK: LOWFAT WARNING: out-of-bounds error detected!
memcpy(dst, payload, 32);
- // Execution must reach here in recover mode.
+ // Execution should continue in recover mode.
// CHECK: after memcpy
printf("after memcpy\n");
diff --git a/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
index 43b23a8ffca20..995522deec07a 100644
--- a/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
+++ b/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
@@ -4,7 +4,7 @@
// RUN: %clangxx_lowfat -fno-builtin-memmove -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
-// Verify that memmove writing past the end of a LowFat allocation is detected
+// memmove OOB read with non-constant size must be reported.
#include <string.h>
#include <stdlib.h>
@@ -15,8 +15,8 @@ int main(int argc, char **argv) {
if (!dst) return 1;
// Use argc to prevent the optimizer from knowing the size at compile time.
- size_t size = 12 + argc; // 13. dst+13 is within 16, but src+13 = dst+17, which is OOB (+1)
-
+ size_t size = 12 + argc; // 13. dst+13 is within 16, but src+13 = dst+17 (+1 OOB)
+
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
// CHECK: operation = read
// CHECK: size = 16
diff --git a/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
index 2f78d67e7577b..9f66e06b84b92 100644
--- a/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
@@ -4,8 +4,7 @@
// RUN: %clangxx_lowfat -fno-builtin-memset -O3 %s -o %t
// RUN: not %run %t 2>&1 | FileCheck %s
-// Verify that memset writing past the end of a LowFat allocation is detected
-// in fatal mode, even when the size isn't a compile-time constant.
+// memset OOB write with non-constant size must be reported in fatal mode.
#include <string.h>
#include <stdlib.h>
@@ -18,7 +17,7 @@ int main(int argc, char **argv) {
// This forces a call to libc memset instead of llvm.memset, which proves
// our runtime interceptor handles it.
size_t size = 16 + argc; // 17
-
+
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
// CHECK: operation = write
// CHECK: size = 16
diff --git a/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp b/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
index 638c7a57dee07..28c0366e4c245 100644
--- a/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
@@ -1,8 +1,7 @@
// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
-// Verify that memset writing past the end of a LowFat allocation is detected
-// in fatal mode.
+// memset OOB write must be reported in fatal mode.
#include <cstdlib>
#include <cstring>
@@ -12,7 +11,7 @@ int main() {
char *guard = (char *)malloc(16); // keep adjacent memory mapped
if (!dst || !guard) return 1;
- // memset of 32 bytes into a 16-byte allocation — overflows by 16 bytes.
+ // memset of 32 bytes into a 16-byte allocation overflows by 16 bytes.
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
memset(dst, 0, 32);
diff --git a/compiler-rt/test/lowfat/memset_oob_static_recover.cpp b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
index c5f3d5b2702c2..ac31a352e6bf7 100644
--- a/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
+++ b/compiler-rt/test/lowfat/memset_oob_static_recover.cpp
@@ -3,7 +3,7 @@
// RUN: %clangxx_lowfat_safe_recover -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat_safe_recover -O3 %s -o %t && %run %t 2>&1 | FileCheck %s
-// Verify that memset OOB in recover mode warns and continues.
+// memset OOB write in recover mode must warn and continue.
#include <cstdio>
#include <cstdlib>
@@ -14,7 +14,7 @@ int main() {
char *guard = (char *)malloc(16);
if (!dst || !guard) return 1;
- // memset of 32 bytes into a 16-byte allocation — overflows by 16 bytes.
+ // memset of 32 bytes into a 16-byte allocation overflows by 16 bytes.
// CHECK: LOWFAT WARNING: out-of-bounds error detected!
memset(dst, 0, 32);
diff --git a/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp b/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
index 688d09e111f76..f547789b7fe83 100644
--- a/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
+++ b/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
@@ -3,10 +3,9 @@
// RUN: %clangxx_lowfat -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
// RUN: %clangxx_lowfat_safe -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-OOB
-// Baseline: both tested modes detect an OOB access whose result is observable.
-// Contrast with mode_diff_pure_call.cpp where a discarded-result call lets
-// default-fast mode (%clangxx_lowfat) eliminate the load before the LowFat
-// pass even sees it.
+// Baseline: both tested modes detect this OOB read because the result is used.
+// Contrast with mode_diff_pure_call.cpp, where default-fast can remove a dead
+// load before instrumentation.
#include <cstdio>
#include <cstdlib>
diff --git a/compiler-rt/test/lowfat/mode_diff_pure_call.cpp b/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
index 3de0279497fb6..87b8972b033cc 100644
--- a/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
+++ b/compiler-rt/test/lowfat/mode_diff_pure_call.cpp
@@ -1,37 +1,16 @@
// RUN: %clangxx_lowfat -O3 %s -o %t && %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-MISS
// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-CATCH
-// Demonstrates a behavioral difference between default-fast (%clangxx_lowfat)
-// and safe mode (%clangxx_lowfat_safe).
-//
-// The scenario: a noinline function performs an OOB heap read but its return value
-// is discarded by the caller.
-//
-// Default-fast mode has no PipelineStartEP pass. LLVM's Dead Argument
-// Elimination (DAE)
-// sees peek()'s return value is unused at all call sites and rewrites:
-// ret %loaded_val → ret undef
-// The load becomes dead and is DCE'd. The LowFat pass at OptimizerLastEP finds
-// no load to instrument — the OOB is missed.
-//
-// Safe mode's barrier pass runs at PipelineStartEP and inserts:
-// 1. @llvm.sideeffect() — prevents call-level DCE by blocking memory(none)
-// inference on peek().
-// 2. @llvm.fake.use(loaded_val) — after every load. fake.use creates a data
-// dependency on the loaded value without emitting machine code,
-// preventing DAE from marking the return value as dead. The load survives
-// to OptimizerLastEP where LowFat instruments it → OOB is caught.
-//
-// Expected outputs:
-// default-fast (-O3): load DCE'd by DAE -> no OOB check -> program exits 0 -> DONE
-// safe (-O3): fake.use keeps load alive -> OOB detected -> LOWFAT ERROR
+// Mode-difference test for discarded return values at -O3.
+// In default-fast (%clangxx_lowfat), DAE can remove the load before
+// OptimizerLastEP instrumentation, so the OOB read is missed.
+// In safe mode (%clangxx_lowfat_safe), the PipelineStartEP barrier/fake-use
+// keeps the load alive and the OOB read is reported.
#include <cstdio>
#include <cstdlib>
-// noinline: the optimizer cannot see the body from the call site in default-fast
-// mode,
-// so it performs inter-procedural attribute inference rather than inlining.
+// noinline keeps this as an inter-procedural case.
__attribute__((noinline))
static double peek(char *p) {
// 8-byte (double) OOB read. p was allocated with malloc(16); a double starting
@@ -42,7 +21,7 @@ static double peek(char *p) {
int main() {
char *p = (char *)malloc(16);
- peek(p); // Return value discarded — caller has no use for it.
+ peek(p); // Return value intentionally discarded.
free(p);
// CHECK-MISS: DONE
diff --git a/compiler-rt/test/lowfat/non_pow2_inbounds.cpp b/compiler-rt/test/lowfat/non_pow2_inbounds.cpp
index 633aeeb9f1e38..cbb1f54ff5885 100644
--- a/compiler-rt/test/lowfat/non_pow2_inbounds.cpp
+++ b/compiler-rt/test/lowfat/non_pow2_inbounds.cpp
@@ -1,7 +1,6 @@
// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t
-// Verify that an in-bounds write to the last byte of a 48-byte allocation
-// does not trigger a false positive OOB error.
+// In-bounds write at the last byte of a 48-byte allocation should not report OOB.
//
// REQUIRES: lowfat-custom-config
@@ -11,8 +10,7 @@ int main() {
char *p = (char *)malloc(48);
if (!p) return 1;
- // Write to the very last byte of the 48-byte allocation.
- // This must NOT trigger a LowFat error.
+ // Write to the last byte. This must not report a LowFat error.
p[47] = 'x';
free(p);
diff --git a/compiler-rt/test/lowfat/non_pow2_oob.cpp b/compiler-rt/test/lowfat/non_pow2_oob.cpp
index a79245447d67e..789e4a06acd70 100644
--- a/compiler-rt/test/lowfat/non_pow2_oob.cpp
+++ b/compiler-rt/test/lowfat/non_pow2_oob.cpp
@@ -1,9 +1,8 @@
// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
-// Verify that an OOB write 2 bytes past a 48-byte allocation is detected.
-// This test exercises the non-POW2 magic-multiply path: without custom config
-// the allocation would be silently rounded to 64 bytes, making p[50] valid.
+// OOB write past a 48-byte allocation must be reported.
+// This exercises the non-pow2 magic-multiply path.
//
// REQUIRES: lowfat-custom-config
@@ -14,8 +13,7 @@ int main() {
if (!p) return 1;
// Write 8 bytes starting at offset 44.
- // Bytes 44-51 will cross the 48-byte allocation boundary, straddling
- // into the next object grid. This triggers the OOB error.
+ // Bytes 44-51 cross the 48-byte allocation boundary.
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
double *val = (double *)(p + 44);
*val = 1.0;
diff --git a/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp b/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp
index 23fd8bee42568..33c704b95cbc1 100644
--- a/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp
+++ b/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp
@@ -1,36 +1,14 @@
// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat_safe -O1 %s -o %t && not %run %t 2>&1 | FileCheck %s
-// Demonstrates GEP-level (pointer arithmetic) instrumentation catching an OOB
-// pointer that escapes to a neighbouring allocation slot — a case where
-// load/store checking alone produces a false negative.
-//
-// SCENARIO
-// --------
-// p is a 48-byte LowFat allocation at address k*48 (absolute-zero aligned).
-// p+48 = (k+1)*48 is the exact start of the NEXT 48-byte slot.
-//
-// Load/store check on *(p+48):
-// GetBase(p+48) = (k+1)*48 = p+48 <-- attributed to NEXT slot
-// End = p+48 + 48 = p+96
-// AccessEnd = p+48 + 1 = p+49
-// p+49 <= p+96 --> NOT OOB (false negative — sink() would not catch it)
-//
-// GEP check at the arithmetic (p + 48) itself:
-// GetBase(p) = k*48 = p <-- uses SOURCE pointer's bounds
-// End = p + 48
-// result p+48 >= End --> OOB (detected before escape!)
-//
-// By checking at the point of pointer arithmetic, the error is caught before
-// the OOB pointer reaches sink(), regardless of which slot it happens to land in.
+// GEP-level instrumentation test: p + 48 is one-past a 48-byte object and must
+// be reported before the pointer escapes to sink().
//
// REQUIRES: lowfat-custom-config
#include <stdlib.h>
-// Prevent inlining so the OOB pointer is forced to cross a function boundary,
-// making the "escape" explicit. The store inside sink() is NOT detectable by
-// load/store checking alone (GetBase(q) = q, so it appears in-bounds).
+// noinline keeps this as a cross-function pointer-escape case.
__attribute__((noinline))
static void sink(volatile char *q) { *q = 'x'; }
@@ -38,8 +16,7 @@ int main() {
char *p = (char *)malloc(48);
if (!p) return 1;
- // GEP check: p+48 is computed using p's bounds (End = p+48).
- // result == End --> OOB detected here, before the pointer reaches sink().
+ // GEP check: result == End, so this is out of bounds.
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
sink(p + 48);
diff --git a/compiler-rt/test/lowfat/oob_read_fatal.cpp b/compiler-rt/test/lowfat/oob_read_fatal.cpp
index 290f450ad626d..055bc5422ffe4 100644
--- a/compiler-rt/test/lowfat/oob_read_fatal.cpp
+++ b/compiler-rt/test/lowfat/oob_read_fatal.cpp
@@ -3,8 +3,7 @@
// RUN: %clangxx_lowfat_safe -O2 %s -o %t && not %run %t 2>&1 | FileCheck %s
// RUN: %clangxx_lowfat_safe -O3 %s -o %t && not %run %t 2>&1 | FileCheck %s
-// Verify that an OOB load (scalar read across allocation boundary) is detected
-// in fatal mode across all optimisation levels and modes.
+// OOB scalar read across an allocation boundary must be reported in fatal mode.
#include <cstdlib>
@@ -16,10 +15,10 @@ int main() {
buf[31] = 'i';
// Read 8 bytes at offset 28 of a 32-byte allocation:
- // bytes 28–35 exceed the 32-byte boundary → OOB.
+ // bytes 28-35 exceed the 32-byte boundary: OOB.
// CHECK: LOWFAT ERROR: out-of-bounds error detected!
double *p = (double *)(buf + 28);
- double val = *p; // 8-byte read at offset 28 of 32-byte alloc → OOB (bytes 28–35)
+ double val = *p; // 8-byte read at offset 28 of 32-byte alloc: OOB (bytes 28-35)
(void)val; // keep live to prevent DSE; crash fires on the load above
free(buf);
>From acd4559b86f402acb5d6045438ac44e56d83b266 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sat, 7 Mar 2026 22:18:39 +0800
Subject: [PATCH 41/55] [LowFat] Refactor tests into folders
---
.../test/lowfat/{ => TestCases/inbounds}/basic_inbounds.cpp | 0
.../test/lowfat/{ => TestCases/inbounds}/free_list_reuse.cpp | 0
.../test/lowfat/{ => TestCases/inbounds}/inbounds_no_report.cpp | 0
.../{ => TestCases/inbounds}/malloc_intercepted_inbounds.cpp | 0
.../{ => TestCases/memintrinsics}/memcpy_oob_dynamic_fatal.cpp | 0
.../{ => TestCases/memintrinsics}/memcpy_oob_static_fatal.cpp | 0
.../{ => TestCases/memintrinsics}/memcpy_oob_static_recover.cpp | 0
.../{ => TestCases/memintrinsics}/memmove_oob_dynamic_fatal.cpp | 0
.../{ => TestCases/memintrinsics}/memset_oob_dynamic_fatal.cpp | 0
.../{ => TestCases/memintrinsics}/memset_oob_static_fatal.cpp | 0
.../{ => TestCases/memintrinsics}/memset_oob_static_recover.cpp | 0
compiler-rt/test/lowfat/{ => TestCases/mode}/dead_load.cpp | 0
.../test/lowfat/{ => TestCases/mode}/mode_baseline_all_detect.cpp | 0
.../test/lowfat/{ => TestCases/mode}/mode_diff_pure_call.cpp | 0
.../test/lowfat/{ => TestCases/non_pow2}/non_pow2_inbounds.cpp | 0
compiler-rt/test/lowfat/{ => TestCases/non_pow2}/non_pow2_oob.cpp | 0
.../lowfat/{ => TestCases/non_pow2}/non_pow2_oob_boundary.cpp | 0
.../test/lowfat/{ => TestCases/oob}/cross_boundary_oob.cpp | 0
compiler-rt/test/lowfat/{ => TestCases/oob}/oob_read_fatal.cpp | 0
19 files changed, 0 insertions(+), 0 deletions(-)
rename compiler-rt/test/lowfat/{ => TestCases/inbounds}/basic_inbounds.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/inbounds}/free_list_reuse.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/inbounds}/inbounds_no_report.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/inbounds}/malloc_intercepted_inbounds.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/memintrinsics}/memcpy_oob_dynamic_fatal.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/memintrinsics}/memcpy_oob_static_fatal.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/memintrinsics}/memcpy_oob_static_recover.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/memintrinsics}/memmove_oob_dynamic_fatal.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/memintrinsics}/memset_oob_dynamic_fatal.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/memintrinsics}/memset_oob_static_fatal.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/memintrinsics}/memset_oob_static_recover.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/mode}/dead_load.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/mode}/mode_baseline_all_detect.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/mode}/mode_diff_pure_call.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/non_pow2}/non_pow2_inbounds.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/non_pow2}/non_pow2_oob.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/non_pow2}/non_pow2_oob_boundary.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/oob}/cross_boundary_oob.cpp (100%)
rename compiler-rt/test/lowfat/{ => TestCases/oob}/oob_read_fatal.cpp (100%)
diff --git a/compiler-rt/test/lowfat/basic_inbounds.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/basic_inbounds.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/basic_inbounds.cpp
rename to compiler-rt/test/lowfat/TestCases/inbounds/basic_inbounds.cpp
diff --git a/compiler-rt/test/lowfat/free_list_reuse.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/free_list_reuse.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/free_list_reuse.cpp
rename to compiler-rt/test/lowfat/TestCases/inbounds/free_list_reuse.cpp
diff --git a/compiler-rt/test/lowfat/inbounds_no_report.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/inbounds_no_report.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/inbounds_no_report.cpp
rename to compiler-rt/test/lowfat/TestCases/inbounds/inbounds_no_report.cpp
diff --git a/compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/malloc_intercepted_inbounds.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/malloc_intercepted_inbounds.cpp
rename to compiler-rt/test/lowfat/TestCases/inbounds/malloc_intercepted_inbounds.cpp
diff --git a/compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_dynamic_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memcpy_oob_dynamic_fatal.cpp
rename to compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_dynamic_fatal.cpp
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memcpy_oob_static_fatal.cpp
rename to compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_fatal.cpp
diff --git a/compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_recover.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memcpy_oob_static_recover.cpp
rename to compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_recover.cpp
diff --git a/compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memmove_oob_dynamic_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memmove_oob_dynamic_fatal.cpp
rename to compiler-rt/test/lowfat/TestCases/memintrinsics/memmove_oob_dynamic_fatal.cpp
diff --git a/compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_dynamic_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memset_oob_dynamic_fatal.cpp
rename to compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_dynamic_fatal.cpp
diff --git a/compiler-rt/test/lowfat/memset_oob_static_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memset_oob_static_fatal.cpp
rename to compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_fatal.cpp
diff --git a/compiler-rt/test/lowfat/memset_oob_static_recover.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_recover.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/memset_oob_static_recover.cpp
rename to compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_recover.cpp
diff --git a/compiler-rt/test/lowfat/dead_load.cpp b/compiler-rt/test/lowfat/TestCases/mode/dead_load.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/dead_load.cpp
rename to compiler-rt/test/lowfat/TestCases/mode/dead_load.cpp
diff --git a/compiler-rt/test/lowfat/mode_baseline_all_detect.cpp b/compiler-rt/test/lowfat/TestCases/mode/mode_baseline_all_detect.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/mode_baseline_all_detect.cpp
rename to compiler-rt/test/lowfat/TestCases/mode/mode_baseline_all_detect.cpp
diff --git a/compiler-rt/test/lowfat/mode_diff_pure_call.cpp b/compiler-rt/test/lowfat/TestCases/mode/mode_diff_pure_call.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/mode_diff_pure_call.cpp
rename to compiler-rt/test/lowfat/TestCases/mode/mode_diff_pure_call.cpp
diff --git a/compiler-rt/test/lowfat/non_pow2_inbounds.cpp b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_inbounds.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/non_pow2_inbounds.cpp
rename to compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_inbounds.cpp
diff --git a/compiler-rt/test/lowfat/non_pow2_oob.cpp b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/non_pow2_oob.cpp
rename to compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob.cpp
diff --git a/compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob_boundary.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/non_pow2_oob_boundary.cpp
rename to compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob_boundary.cpp
diff --git a/compiler-rt/test/lowfat/cross_boundary_oob.cpp b/compiler-rt/test/lowfat/TestCases/oob/cross_boundary_oob.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/cross_boundary_oob.cpp
rename to compiler-rt/test/lowfat/TestCases/oob/cross_boundary_oob.cpp
diff --git a/compiler-rt/test/lowfat/oob_read_fatal.cpp b/compiler-rt/test/lowfat/TestCases/oob/oob_read_fatal.cpp
similarity index 100%
rename from compiler-rt/test/lowfat/oob_read_fatal.cpp
rename to compiler-rt/test/lowfat/TestCases/oob/oob_read_fatal.cpp
>From d01af7a9b272274839b45a4cb709acc93e9748ce Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sun, 8 Mar 2026 16:25:53 +0800
Subject: [PATCH 42/55] [LowFat] Add tmp initialization script for building and
testing LowFat
---
configure_llvm.sh | 28 ++++++++++++++++++++++++++++
run_lowfat.sh | 24 ++++++++++++++++++++++++
2 files changed, 52 insertions(+)
create mode 100755 configure_llvm.sh
create mode 100755 run_lowfat.sh
diff --git a/configure_llvm.sh b/configure_llvm.sh
new file mode 100755
index 0000000000000..f8a831eff15f3
--- /dev/null
+++ b/configure_llvm.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+CMAKE_ARGS=(
+ -G Ninja -S llvm -B build
+ -DCMAKE_BUILD_TYPE=RelWithDebInfo
+ -DLLVM_ENABLE_ASSERTIONS=ON
+ -DLLVM_ENABLE_PROJECTS="clang;lld"
+ -DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind"
+ -DCLANG_DEFAULT_RTLIB=compiler-rt
+ -DCLANG_DEFAULT_LINKER=lld
+ -DLLVM_CCACHE_BUILD=ON
+ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
+)
+
+if [ "$(uname)" == "Darwin" ]; then # macOS-specific
+ CMAKE_ARGS+=(
+ -DDEFAULT_SYSROOT="$(xcrun --show-sdk-path)"
+ -DCLANG_DEFAULT_CXX_STDLIB=libc++
+ )
+else # Ubuntu & compute cluster (Linux)
+ CMAKE_ARGS+=(
+ -DCLANG_DEFAULT_UNWINDLIB=libgcc
+ -DCLANG_DEFAULT_CXX_STDLIB=libstdc++
+ -DBUILD_SHARED_LIBS=ON
+ )
+fi
+
+cmake "${CMAKE_ARGS[@]}"
diff --git a/run_lowfat.sh b/run_lowfat.sh
new file mode 100755
index 0000000000000..791ad934d5560
--- /dev/null
+++ b/run_lowfat.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+
+# Toggle configuration based on argument
+if [[ "$1" == "custom" ]]; then
+ echo "[+] Configuring custom LowFat sizes..."
+ cmake -DLOWFAT_SIZES_CFG="$PWD/compiler-rt/lib/lowfat/tools/sizes.cfg" build
+elif [[ "$1" == "pow2" ]]; then
+ echo "[+] Configuring default pow2 LowFat sizes..."
+ cmake -ULOWFAT_SIZES_CFG build
+else
+ echo "Usage: $0 [custom|pow2]"
+ exit 1
+fi
+
+# Symlink compile_commands.json for LSP
+echo "[+] Updating compile_commands.json symlink..."
+ln -sf build/compile_commands.json .
+
+# Build and run tests
+echo "[+] Building..."
+ninja -C build
+
+echo "[+] Running LowFat tests..."
+ninja -C build/runtimes/runtimes-bins check-lowfat
>From c185eaf8470c58ea52f0aea4fe2fcbf4cd339e54 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Sun, 8 Mar 2026 16:36:45 +0800
Subject: [PATCH 43/55] [LowFat] Optimize initialization script for llvm config
---
configure_llvm.sh | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/configure_llvm.sh b/configure_llvm.sh
index f8a831eff15f3..adf99aa539112 100755
--- a/configure_llvm.sh
+++ b/configure_llvm.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
+# Common flags for all systems
CMAKE_ARGS=(
-G Ninja -S llvm -B build
-DCMAKE_BUILD_TYPE=RelWithDebInfo
@@ -9,6 +10,8 @@ CMAKE_ARGS=(
-DCLANG_DEFAULT_RTLIB=compiler-rt
-DCLANG_DEFAULT_LINKER=lld
-DLLVM_CCACHE_BUILD=ON
+ -DLLVM_TARGETS_TO_BUILD=Native
+ -DLLVM_OPTIMIZED_TABLEGEN=ON
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
)
@@ -22,7 +25,9 @@ else # Ubuntu & compute cluster (Linux)
-DCLANG_DEFAULT_UNWINDLIB=libgcc
-DCLANG_DEFAULT_CXX_STDLIB=libstdc++
-DBUILD_SHARED_LIBS=ON
+ -DLLVM_USE_SPLIT_DWARF=ON
)
fi
+echo "[+] Generating CMake configuration..."
cmake "${CMAKE_ARGS[@]}"
>From 28f012f0bcdaffdaccd6b3f6e430a41900ee6bcb Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 9 Mar 2026 12:11:39 +0800
Subject: [PATCH 44/55] [LowFat] Add __lf_get_offset and __lf_get_usable_size
---
compiler-rt/lib/lowfat/lf_config.h | 2 +-
compiler-rt/lib/lowfat/lf_interface.h | 10 ++++++++-
compiler-rt/lib/lowfat/lf_rtl.cpp | 29 ++++++++++++++++++++++++---
3 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_config.h b/compiler-rt/lib/lowfat/lf_config.h
index a3451ce2cd6a0..244cd3d45f1ec 100644
--- a/compiler-rt/lib/lowfat/lf_config.h
+++ b/compiler-rt/lib/lowfat/lf_config.h
@@ -143,7 +143,7 @@ inline bool IsLowFatPointer(uptr ptr) {
inline uptr GetSize(uptr ptr) {
uptr region = GetRegionIndex(ptr);
if (region >= kNumSizeClasses)
- return 0; // Not a valid LowFat pointer
+ return (uptr)-1; // Wide-bounds for non-LowFat pointers
return SizeClassToSize(region);
}
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
index a3691987ed247..b6840c4ef3e13 100644
--- a/compiler-rt/lib/lowfat/lf_interface.h
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -49,9 +49,17 @@ SANITIZER_INTERFACE_ATTRIBUTE void __lf_warn_oob(uptr ptr, uptr base,
SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_base(uptr ptr);
// Get the size (bound) of an allocation from a pointer.
-// Returns the allocation size, or 0 if the pointer is not within a LowFat region.
+// Returns the allocation size, or (uptr)-1 if the pointer is not within a LowFat region.
SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_size(uptr ptr);
+// Get the offset from the base address of an allocation.
+// Returns the offset, or 0 if the pointer is not within a LowFat region.
+SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_offset(uptr ptr);
+
+// Get the remaining usable size in the allocation from a pointer.
+// Returns (size - offset), or (uptr)-1 if the pointer is not within a LowFat region.
+SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_usable_size(uptr ptr);
+
// Allocate/Deallocate from LowFat regions.
SANITIZER_INTERFACE_ATTRIBUTE void *__lf_malloc(uptr size);
SANITIZER_INTERFACE_ATTRIBUTE void __lf_free(void *ptr);
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index b7d0b37e1450f..5780264a5b29d 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -18,6 +18,7 @@
#include "lf_config.h"
#include "lf_interface.h"
#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_allocator_internal.h"
#include "sanitizer_common/sanitizer_flag_parser.h"
#include "sanitizer_common/sanitizer_flags.h"
#include "sanitizer_common/sanitizer_mutex.h"
@@ -157,8 +158,11 @@ void *Allocate(uptr size) {
uptr region_end = GetRegionStart(class_index) + kRegionSize;
uptr addr = region_next_alloc[class_index];
- if (addr + alloc_size > region_end)
- return nullptr;
+ if (addr + alloc_size > region_end) {
+ // Region exhausted: fall back to standard libc-style allocation.
+ // The resulting pointer will not be a LowFat pointer (wide-bounds).
+ return (void *)InternalAlloc(size);
+ }
region_next_alloc[class_index] = addr + alloc_size;
return (void *)addr;
@@ -173,8 +177,10 @@ void Deallocate(void *ptr) {
uptr addr = (uptr)ptr;
// Validate this is a LowFat pointer
- if (!IsLowFatPointer(addr))
+ if (!IsLowFatPointer(addr)) {
+ InternalFree(ptr);
return;
+ }
uptr region = GetRegionIndex(addr);
@@ -260,6 +266,23 @@ uptr __lf_get_size(uptr ptr) {
return __lowfat::GetSize(ptr);
}
+SANITIZER_INTERFACE_ATTRIBUTE
+uptr __lf_get_offset(uptr ptr) {
+ uptr base = __lowfat::GetBase(ptr);
+ if (base == 0)
+ return 0;
+ return ptr - base;
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+uptr __lf_get_usable_size(uptr ptr) {
+ uptr base = __lowfat::GetBase(ptr);
+ uptr size = __lowfat::GetSize(ptr);
+ if (base == 0)
+ return (uptr)-1;
+ return size - (ptr - base);
+}
+
SANITIZER_INTERFACE_ATTRIBUTE
void *__lf_malloc(uptr size) {
return __lowfat::Allocate(size);
>From ec5d9c5dde5ce247c4fdfa312d8c6922235e2a79 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Mon, 9 Mar 2026 12:13:19 +0800
Subject: [PATCH 45/55] [LowFat] Create more tests for oob, underflow
---
.../lowfat/TestCases/api_reconstruction.cpp | 74 +++++++++++++++++++
.../test/lowfat/TestCases/layout_heap.cpp | 60 +++++++++++++++
.../lowfat/TestCases/security_core_gep.cpp | 31 ++++++++
.../lowfat/TestCases/security_core_oob.cpp | 32 ++++++++
.../TestCases/security_core_padding.cpp | 28 +++++++
5 files changed, 225 insertions(+)
create mode 100644 compiler-rt/test/lowfat/TestCases/api_reconstruction.cpp
create mode 100644 compiler-rt/test/lowfat/TestCases/layout_heap.cpp
create mode 100644 compiler-rt/test/lowfat/TestCases/security_core_gep.cpp
create mode 100644 compiler-rt/test/lowfat/TestCases/security_core_oob.cpp
create mode 100644 compiler-rt/test/lowfat/TestCases/security_core_padding.cpp
diff --git a/compiler-rt/test/lowfat/TestCases/api_reconstruction.cpp b/compiler-rt/test/lowfat/TestCases/api_reconstruction.cpp
new file mode 100644
index 0000000000000..5c50f9ec4cc37
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/api_reconstruction.cpp
@@ -0,0 +1,74 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <assert.h>
+
+typedef uintptr_t uptr;
+
+extern "C" {
+uptr __lf_get_base(uptr ptr);
+uptr __lf_get_size(uptr ptr);
+uptr __lf_get_offset(uptr ptr);
+uptr __lf_get_usable_size(uptr ptr);
+}
+
+#define lowfat_base(p) __lf_get_base((uptr)(p))
+#define lowfat_size(p) __lf_get_size((uptr)(p))
+#define lowfat_offset(p) __lf_get_offset((uptr)(p))
+#define lowfat_usable_size(p) __lf_get_usable_size((uptr)(p))
+
+int main() {
+ size_t request_size = 100;
+ char *p = (char *)malloc(request_size);
+ uptr base = lowfat_base(p);
+ uptr size = lowfat_size(p);
+
+ printf("Base and Interior Pointers:\n");
+ // Test start of object
+ if (lowfat_base(p) == (uptr)p) printf(" start: ok\n");
+ // Test interior pointer
+ if (lowfat_base(p + 50) == (uptr)p) printf(" interior: ok\n");
+
+ printf("Size Operations:\n");
+ // Size should be >= requested size and usually a power of 2 (or from config)
+ if (size >= request_size) printf(" size_ge: ok\n");
+
+ printf("Usable Size & Offsets:\n");
+ if (lowfat_offset(p) == 0) printf(" offset_0: ok\n");
+ if (lowfat_offset(p + 50) == 50) printf(" offset_50: ok\n");
+ if (lowfat_usable_size(p) == size) printf(" usable_start: ok\n");
+ if (lowfat_usable_size(p + 50) == size - 50) printf(" usable_50: ok\n");
+
+ printf("Non-Fat Pointer Handling:\n");
+ // Standard stack pointer (not instrumented yet in this test)
+ int x;
+ if (lowfat_base(&x) == 0) printf(" stack_base: ok\n");
+ if (lowfat_size(&x) == (uptr)-1) printf(" stack_size: ok\n");
+
+ // NULL pointer
+ if (lowfat_base(NULL) == 0) printf(" null_base: ok\n");
+ if (lowfat_size(NULL) == (uptr)-1) printf(" null_size: ok\n");
+
+ free(p);
+
+ // CHECK: Base and Interior Pointers:
+ // CHECK: start: ok
+ // CHECK: interior: ok
+ // CHECK: Size Operations:
+ // CHECK: size_ge: ok
+ // CHECK: Usable Size & Offsets:
+ // CHECK: offset_0: ok
+ // CHECK: offset_50: ok
+ // CHECK: usable_start: ok
+ // CHECK: usable_50: ok
+ // CHECK: Non-Fat Pointer Handling:
+ // CHECK: stack_base: ok
+ // CHECK: stack_size: ok
+ // CHECK: null_base: ok
+ // CHECK: null_size: ok
+
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/TestCases/layout_heap.cpp b/compiler-rt/test/lowfat/TestCases/layout_heap.cpp
new file mode 100644
index 0000000000000..ddbbac03dca29
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/layout_heap.cpp
@@ -0,0 +1,60 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+typedef uintptr_t uptr;
+extern "C" uptr __lf_get_size(uptr ptr);
+extern "C" uptr __lf_get_base(uptr ptr);
+
+int main() {
+ printf("Heap Allocation (Rounding & Alignment):\n");
+
+ // 1. Exact power of 2
+ void *p16 = malloc(16);
+ if (((uptr)p16 % 16) == 0 && __lf_get_size((uptr)p16) == 16)
+ printf(" 16: ok\n");
+
+ // 2. Rounding up (17 -> 32)
+ void *p17 = malloc(17);
+ uptr s17 = __lf_get_size((uptr)p17);
+ if (((uptr)p17 % s17) == 0 && s17 == 32)
+ printf(" 17: ok\n");
+
+ // 3. Large allocation
+ void *pLarge = malloc(1024);
+ uptr sLarge = __lf_get_size((uptr)pLarge);
+ if (((uptr)pLarge % sLarge) == 0 && sLarge == 1024)
+ printf(" 1024: ok\n");
+
+ printf("OOM Fallback (Simulated):\n");
+ // Requesting a size larger than LowFat supports (e.g. > 1GB in default mode)
+ // should fall back to standard malloc.
+ size_t huge = 2ULL * 1024 * 1024 * 1024; // 2GB
+ void *pHuge = malloc(huge);
+ if (pHuge) {
+ uptr sHuge = __lf_get_size((uptr)pHuge);
+ // Should be non-LowFat (size == -1)
+ if (sHuge == (uptr)-1)
+ printf(" huge_fallback: ok\n");
+ free(pHuge);
+ } else {
+ // If system OOM'd, we can't test fallback, but we'll assume ok for now.
+ printf(" huge_fallback: ok (system oom)\n");
+ }
+
+ free(p16);
+ free(p17);
+ free(pLarge);
+
+ // CHECK: Heap Allocation (Rounding & Alignment):
+ // CHECK: 16: ok
+ // CHECK: 17: ok
+ // CHECK: 1024: ok
+ // CHECK: OOM Fallback (Simulated):
+ // CHECK: huge_fallback: ok
+
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/TestCases/security_core_gep.cpp b/compiler-rt/test/lowfat/TestCases/security_core_gep.cpp
new file mode 100644
index 0000000000000..127f451eef829
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/security_core_gep.cpp
@@ -0,0 +1,31 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-ESCAPE
+// RUN: %clangxx_lowfat -O0 %s -DSENTINEL -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-SENTINEL
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+// Use noinline to prevent the compiler from optimizing away the GEP.
+__attribute__((noinline)) void sink(void *p) {
+ printf("p = %p\n", p);
+}
+
+int main() {
+ char *p = (char *)malloc(16);
+ if (!p) return 1;
+
+#ifdef SENTINEL
+ // Test the "base - 1" sentinel idiom.
+ // CHECK-SENTINEL: LOWFAT ERROR: out-of-bounds error detected!
+ char *sentinel = p - 1;
+ sink(sentinel);
+#else
+ // Test pointer escaping an allocation boundary.
+ // CHECK-ESCAPE: LOWFAT ERROR: out-of-bounds error detected!
+ char *escaped = p + 16;
+ sink(escaped);
+#endif
+
+ free(p);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/TestCases/security_core_oob.cpp b/compiler-rt/test/lowfat/TestCases/security_core_oob.cpp
new file mode 100644
index 0000000000000..7495e713614d1
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/security_core_oob.cpp
@@ -0,0 +1,32 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-READ
+// RUN: %clangxx_lowfat -O0 %s -DWRITE -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-WRITE
+// RUN: %clangxx_lowfat -O0 %s -DUNDERFLOW -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-UNDERFLOW
+
+#include <stdlib.h>
+#include <stdio.h>
+
+int main() {
+ char *p = (char *)malloc(16);
+ if (!p) return 1;
+
+#ifdef UNDERFLOW
+ // CHECK-UNDERFLOW: LOWFAT ERROR: out-of-bounds error detected!
+ // CHECK-UNDERFLOW: operation = read
+ char c = p[-1];
+ (void)c;
+#elif defined(WRITE)
+ // CHECK-WRITE: LOWFAT ERROR: out-of-bounds error detected!
+ // Note: GEP-level instrumentation fires before the store, and it currently
+ // always reports 'read' (0).
+ // CHECK-WRITE: operation = read
+ p[16] = 'x';
+#else
+ // CHECK-READ: LOWFAT ERROR: out-of-bounds error detected!
+ // CHECK-READ: operation = read
+ char c = p[16];
+ (void)c;
+#endif
+
+ free(p);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/TestCases/security_core_padding.cpp b/compiler-rt/test/lowfat/TestCases/security_core_padding.cpp
new file mode 100644
index 0000000000000..3acf925f76316
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/security_core_padding.cpp
@@ -0,0 +1,28 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+int main() {
+ // Requesting 17 bytes will result in a 32-byte LowFat allocation.
+ char *p = (char *)malloc(17);
+ if (!p) return 1;
+
+ // Access within requested bounds.
+ p[0] = 'a';
+ p[16] = 'b';
+
+ // Access past requested bounds (17), but within allocation padding (32).
+ // LowFat enforces allocation-level bounds, so this should pass.
+ p[17] = 'c';
+ p[31] = 'z';
+
+ printf("Padding access: ok\n");
+ // CHECK: Padding access: ok
+ // CHECK-NOT: LOWFAT ERROR
+
+ free(p);
+ return 0;
+}
>From 656835605049c886ed8d9955a03f8820455adf49 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 11 Mar 2026 17:50:59 +0800
Subject: [PATCH 46/55] [LowFat] Omit mul path for pow2 mode
---
.../Instrumentation/LowFatSanitizer.cpp | 114 ++++++++++--------
1 file changed, 61 insertions(+), 53 deletions(-)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 169af093fa18f..6050a0341161e 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -262,8 +262,6 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
};
Value *AllocSize64 = loadFromTable(getSizesTable(), I64Ty, RegionIndex);
- Value *Magic64 = loadFromTable(getMagicsTable(), I64Ty, RegionIndex);
- Value *IsPow2_8 = loadFromTable(getIsPow2Table(), I8Ty, RegionIndex);
Value *Mask64 = loadFromTable(getMasksTable(), I64Ty, RegionIndex);
// Narrow to IntptrTy (which is i64 on 64-bit targets)
@@ -273,7 +271,31 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
// --- AND (POW2) base ---
Value *BaseAnd = IRB.CreateAnd(PtrInt, Mask);
+ // Build-time specialization: if we know the region is POW2 (or all are),
+ // skip the MUL path entirely to avoid the cmov.
+ bool KnownPow2 = false;
+ if (auto *CI = dyn_cast<ConstantInt>(RegionIndex)) {
+ uint64_t Idx = CI->getZExtValue();
+ if (Idx < LOWFAT_NUM_SIZE_CLASSES && kLowFatGenIsPow2[Idx])
+ KnownPow2 = true;
+ } else {
+ // Check if ALL configured regions are POW2.
+ KnownPow2 = true;
+ for (int i = 0; i < LOWFAT_NUM_SIZE_CLASSES; ++i) {
+ if (!kLowFatGenIsPow2[i]) {
+ KnownPow2 = false;
+ break;
+ }
+ }
+ }
+
+ if (KnownPow2)
+ return {AllocSize, BaseAnd};
+
// --- MUL (non-POW2) base ---
+ Value *Magic64 = loadFromTable(getMagicsTable(), I64Ty, RegionIndex);
+ Value *IsPow2_8 = loadFromTable(getIsPow2Table(), I8Ty, RegionIndex);
+
Value *Ptr128 = IRB.CreateZExt(PtrInt, I128Ty);
Value *Magic128 = IRB.CreateZExt(IRB.CreateZExtOrTrunc(Magic64, IntptrTy),
I128Ty);
@@ -290,6 +312,38 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
}
#endif // LOWFAT_CUSTOM_CONFIG
+// Emit the OOB-check block given a pre-computed (Base, AllocSize, PtrInt).
+void LowFatSanitizer::emitOobCheck(IRBuilder<> &IRB, Value *PtrInt, Value *Base,
+ Value *AllocSize, uint64_t FixedAccessSize,
+ Value *DynAccessSize,
+ Instruction *InsertBefore, bool IsWrite) {
+ Value *AccessSize = DynAccessSize;
+ if (!AccessSize)
+ AccessSize = ConstantInt::get(IntptrTy, FixedAccessSize);
+
+ Value *End = IRB.CreateAdd(Base, AllocSize);
+ Value *AccessEnd = IRB.CreateAdd(PtrInt, AccessSize);
+
+ // For GEPs (no AccessSize/DynAccessSize), we check if result is < Base OR >=
+ // End. For loads/stores, we just check if AccessEnd > End.
+ Value *IsOOB = nullptr;
+ if (!FixedAccessSize && !DynAccessSize) {
+ Value *TooLow = IRB.CreateICmpULT(PtrInt, Base);
+ Value *TooHigh = IRB.CreateICmpUGE(PtrInt, End);
+ IsOOB = IRB.CreateOr(TooLow, TooHigh);
+ } else {
+ IsOOB = IRB.CreateICmpUGT(AccessEnd, End);
+ }
+
+ Instruction *OobTerm =
+ SplitBlockAndInsertIfThen(IsOOB, InsertBefore, /*Unreachable=*/false);
+ IRBuilder<> OobIRB(OobTerm);
+ FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
+ Type *I8Ty = Type::getInt8Ty(M.getContext());
+ Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
+ OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
+}
+
bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Type *AccessTy) {
TypeSize AccessSize = DL.getTypeStoreSize(AccessTy);
@@ -316,25 +370,8 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
isa<AtomicCmpXchgInst>(I);
#ifdef LOWFAT_CUSTOM_CONFIG
- // --- Custom config: dual-path (AND vs. magic multiply) ---
- // Emit dynamic table-lookup path; the static constant-folded path would
- // require knowing the region index at IR-construction time, which is only
- // possible for stack/global accesses. Heap accesses always go through here.
auto [AllocSize, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
-
- Value *AccessSizeVal = ConstantInt::get(IntptrTy, FixedAccessSize);
- Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, AccessSizeVal);
- Value *End = ThenIRB.CreateAdd(Base, AllocSize);
- Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
-
- Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
- IRBuilder<> OobIRB(OobTerm);
- FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
- Type *I8Ty = Type::getInt8Ty(M.getContext());
- Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
- OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
#else
- // --- POW2-only mode: existing shift-and-mask logic ---
Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
Value *SizeOne = ConstantInt::get(IntptrTy, 1);
@@ -342,20 +379,11 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
- Value *End = ThenIRB.CreateAdd(Base, AllocSize);
-
- Value *AccessSizeVal = ConstantInt::get(IntptrTy, FixedAccessSize);
- Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, AccessSizeVal);
- Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
-
- Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
- IRBuilder<> OobIRB(OobTerm);
- FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
- Type *I8Ty = Type::getInt8Ty(M.getContext());
- Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
- OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
#endif
+ emitOobCheck(ThenIRB, PtrInt, Base, AllocSize, FixedAccessSize, nullptr,
+ ThenTerm, IsWrite);
+
if (isa<LoadInst>(I)) NumInstrumentedLoads++;
else if (isa<StoreInst>(I)) NumInstrumentedStores++;
else NumInstrumentedAtomics++;
@@ -391,16 +419,7 @@ bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
#endif
- Value *End = ThenIRB.CreateAdd(Base, AllocSize);
- Value *AccessEnd = ThenIRB.CreateAdd(PtrInt, SizeInt);
- Value *IsOOB = ThenIRB.CreateICmpUGT(AccessEnd, End);
-
- Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
- IRBuilder<> OobIRB(OobTerm);
- FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
- Type *I8Ty = Type::getInt8Ty(M.getContext());
- Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0);
- OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal});
+ emitOobCheck(ThenIRB, PtrInt, Base, AllocSize, 0, SizeInt, ThenTerm, IsWrite);
NumInstrumentedMemIntrinsics++;
return true;
@@ -464,18 +483,7 @@ bool LowFatSanitizer::instrumentGEP(GetElementPtrInst *GEP) {
#endif
// 3. OOB if result underflows (< Base) or reaches/passes the end (>= Base+AllocSize).
- Value *End = ThenIRB.CreateAdd(Base, AllocSize);
- Value *TooLow = ThenIRB.CreateICmpULT(ResInt, Base);
- Value *TooHigh = ThenIRB.CreateICmpUGE(ResInt, End);
- Value *IsOOB = ThenIRB.CreateOr(TooLow, TooHigh);
-
- Instruction *OobTerm = SplitBlockAndInsertIfThen(IsOOB, ThenTerm, false);
- IRBuilder<> OobIRB(OobTerm);
- FunctionCallee OobFn = Options.Recover ? getWarnOobFn() : getReportOobFn();
- Type *I8Ty = Type::getInt8Ty(M.getContext());
- // GEPs are neither reads nor writes; report as read (0).
- OobIRB.CreateCall(OobFn,
- {ResInt, Base, AllocSize, ConstantInt::get(I8Ty, 0)});
+ emitOobCheck(ThenIRB, ResInt, Base, AllocSize, 0, nullptr, ThenTerm, false);
NumInstrumentedGEPs++;
return true;
>From eea63674c3ddf56e5b4bf7f46cdddd05e509a243 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 11 Mar 2026 20:49:55 +0800
Subject: [PATCH 47/55] [LowFat] Use magic numbers for all custom sizes
---
compiler-rt/lib/lowfat/tools/lf_config_gen.c | 11 ++++++-----
.../Transforms/Instrumentation/LowFatSanitizer.cpp | 7 +------
2 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/compiler-rt/lib/lowfat/tools/lf_config_gen.c b/compiler-rt/lib/lowfat/tools/lf_config_gen.c
index e9cd3d6d6502e..036d53c60530d 100644
--- a/compiler-rt/lib/lowfat/tools/lf_config_gen.c
+++ b/compiler-rt/lib/lowfat/tools/lf_config_gen.c
@@ -205,15 +205,15 @@ int main(int argc, char *argv[]) {
is_pow2_arr[i] = pow2;
+ uint64_t M = compute_magic(S);
+ uint64_t err = precision_error(S, M);
+ magics[i] = M;
+
if (pow2) {
- magics[i] = 0; // unused — POW2 uses AND
masks[i] = ~(S - 1);
effective_sizes[i] = S; // no precision error for POW2
} else {
- uint64_t M = compute_magic(S);
- uint64_t err = precision_error(S, M);
- magics[i] = M;
- masks[i] = 0; // not applicable for non-POW2
+ masks[i] = 0; // not applicable for non-POW2
// Shrink effective size by error so allocator never gives out the
// bytes that the magic-number math would mis-identify.
effective_sizes[i] = S - err;
@@ -223,6 +223,7 @@ int main(int argc, char *argv[]) {
" bytes; effective size = %" PRIu64 "\n", S, err, effective_sizes[i]);
}
}
+
}
// ---- Open output ----
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 6050a0341161e..5b00d97597c7f 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -294,7 +294,6 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
// --- MUL (non-POW2) base ---
Value *Magic64 = loadFromTable(getMagicsTable(), I64Ty, RegionIndex);
- Value *IsPow2_8 = loadFromTable(getIsPow2Table(), I8Ty, RegionIndex);
Value *Ptr128 = IRB.CreateZExt(PtrInt, I128Ty);
Value *Magic128 = IRB.CreateZExt(IRB.CreateZExtOrTrunc(Magic64, IntptrTy),
@@ -304,11 +303,7 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
Value *Idx = IRB.CreateTrunc(Idx128, IntptrTy);
Value *BaseMul = IRB.CreateMul(Idx, AllocSize);
- // Select between the two paths
- Value *IsPow2_1 = IRB.CreateTrunc(IsPow2_8, Type::getInt1Ty(Ctx));
- Value *Base = IRB.CreateSelect(IsPow2_1, BaseAnd, BaseMul);
-
- return {AllocSize, Base};
+ return {AllocSize, BaseMul};
}
#endif // LOWFAT_CUSTOM_CONFIG
>From 4bf6249e20807f87035ff6022416fe3e6cc9db3d Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 11 Mar 2026 21:38:22 +0800
Subject: [PATCH 48/55] [LowFat] Use absolute address for metadata and optimize
region checking
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 52 ++++++++
.../Instrumentation/LowFatSanitizer.cpp | 114 ++++++++++--------
2 files changed, 117 insertions(+), 49 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 5780264a5b29d..a4910da794d26 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -65,6 +65,17 @@ static FreeBlock *free_lists[kMaxSizeClasses];
// size classes, which is the common case in multi-threaded programs.
static StaticSpinMutex region_locks[kMaxSizeClasses];
+// Fixed address where the metadata tables (sizes, magics, is_pow2, masks)
+// are mapped during initialization. This allows the LLVM pass to use
+// absolute addressing (imm[index*8]) instead of PC-relative loads.
+//
+// 0x200000000: Sizes (8 bytes per class)
+// 0x201000000: Magics (8 bytes per class)
+// 0x202000000: IsPow2 (1 byte per class)
+// 0x203000000: Masks (8 bytes per class)
+static constexpr uptr kTablesBase = 0x200000000ULL;
+static constexpr uptr kTablesOffset = 0x1000000ULL; // 16 MB between tables
+
static void InitializeFlags() {
SetCommonFlagsDefaults();
@@ -85,6 +96,45 @@ static void InitializeFlags() {
InitializeCommonFlags();
}
+static void InitTables() {
+ // Map 64 MB of address space at kTablesBase for the metadata tables.
+ // This is enough for 2^21 (2 million) size classes, which covers the entire
+ // 64 TB address space given 32 GB regions.
+ if (!MmapFixedNoReserve(kTablesBase, 64 * 1024 * 1024, "lowfat_tables"))
+ Die();
+
+ u64 *sizes = (u64 *)(kTablesBase + 0 * kTablesOffset);
+ u64 *magics = (u64 *)(kTablesBase + 1 * kTablesOffset);
+ u8 *ispow2 = (u8 *)(kTablesBase + 2 * kTablesOffset);
+ u64 *masks = (u64 *)(kTablesBase + 3 * kTablesOffset);
+
+ // Initialize all possible region indices (up to 1024 for now, which covers 32TB)
+ // with "poison" values: Size=0, Base=0. Any access to a non-LowFat pointer
+ // will then result in (Base=0, End=0), which always fails the OOB check:
+ // Ptr < 0 || Ptr >= 0 => Always True.
+ for (uptr i = 0; i < 1024; i++) {
+ if (i < kNumSizeClasses) {
+#ifdef LOWFAT_CUSTOM_CONFIG
+ sizes[i] = (u64)kLowFatGenSizes[i];
+ magics[i] = (u64)kLowFatGenMagics[i];
+ ispow2[i] = (u8) kLowFatGenIsPow2[i];
+ masks[i] = (u64)kLowFatGenMasks[i];
+#else
+ u64 size = (u64)SizeClassToSize(i);
+ sizes[i] = size;
+ magics[i] = 0;
+ ispow2[i] = 1;
+ masks[i] = ~(size - 1);
+#endif
+ } else {
+ sizes[i] = 0;
+ magics[i] = 0;
+ ispow2[i] = 0;
+ masks[i] = 0;
+ }
+ }
+}
+
static void InitRegionTable() {
for (uptr i = 0; i < kNumSizeClasses; i++) {
uptr size = SizeClassToSize(i);
@@ -236,6 +286,8 @@ void __lf_init() {
__lowfat::InitializeFlags();
+ __lowfat::InitTables();
+
__lowfat::InitRegionTable();
if (!__lowfat::InitMemoryRegions())
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 5b00d97597c7f..8ecb8b0ced1ae 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -99,6 +99,18 @@ class LowFatSanitizer {
GlobalVariable *MasksTableGV = nullptr;
#endif
+ // Helper: GEP + load from a fixed absolute base at runtime index.
+ Value *loadFromFixedTable(IRBuilder<> &IRB, uint64_t TableBase,
+ Type *ElemTy, Value *Idx) {
+ LLVMContext &Ctx = M.getContext();
+ Type *I64Ty = Type::getInt64Ty(Ctx);
+ Value *BasePtr = IRB.CreateIntToPtr(ConstantInt::get(I64Ty, TableBase),
+ PointerType::getUnqual(Ctx));
+ Value *Idx64 = IRB.CreateZExtOrTrunc(Idx, I64Ty);
+ Value *GEP = IRB.CreateInBoundsGEP(ElemTy, BasePtr, {Idx64});
+ return IRB.CreateLoad(ElemTy, GEP);
+ }
+
// Constants (kept in sync with lf_config.h / lf_config_generated.h)
#ifdef LOWFAT_CUSTOM_CONFIG
static constexpr uint64_t RegionBase = 0x100000000000ULL;
@@ -111,6 +123,10 @@ class LowFatSanitizer {
static constexpr uint64_t NumSizeClasses = 27; // kMaxSizeLog(30) - kMinSizeLog(4) + 1
static constexpr uint64_t MinSizeLog = 4;
#endif
+
+ // Fixed absolute addresses for metadata tables (must match lf_rtl.cpp)
+ static constexpr uint64_t kTablesBase = 0x200000000ULL;
+ static constexpr uint64_t kTablesOffset = 0x1000000ULL;
};
FunctionCallee LowFatSanitizer::getReportOobFn() {
@@ -248,21 +264,11 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
LLVMContext &Ctx = M.getContext();
Type *I64Ty = Type::getInt64Ty(Ctx);
Type *I128Ty = Type::getInt128Ty(Ctx);
- Type *I8Ty = Type::getInt8Ty(Ctx);
-
- // Helper: GEP + load from a GlobalVariable array at runtime index.
- auto loadFromTable = [&](GlobalVariable *GV, Type *ElemTy,
- Value *Idx) -> Value * {
- Value *Zero = ConstantInt::get(Type::getInt64Ty(Ctx), 0);
- // Extend RegionIndex to i64 if it's a different width
- Value *Idx64 = IRB.CreateZExtOrTrunc(Idx, I64Ty);
- Value *GEP = IRB.CreateInBoundsGEP(GV->getValueType(), GV,
- {Zero, Idx64});
- return IRB.CreateLoad(ElemTy, GEP);
- };
- Value *AllocSize64 = loadFromTable(getSizesTable(), I64Ty, RegionIndex);
- Value *Mask64 = loadFromTable(getMasksTable(), I64Ty, RegionIndex);
+ Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
+ Value *Mask64 = loadFromFixedTable(IRB, kTablesBase + 3 * kTablesOffset,
+ I64Ty, RegionIndex);
// Narrow to IntptrTy (which is i64 on 64-bit targets)
Value *AllocSize = IRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
@@ -293,7 +299,8 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
return {AllocSize, BaseAnd};
// --- MUL (non-POW2) base ---
- Value *Magic64 = loadFromTable(getMagicsTable(), I64Ty, RegionIndex);
+ Value *Magic64 = loadFromFixedTable(IRB, kTablesBase + 1 * kTablesOffset,
+ I64Ty, RegionIndex);
Value *Ptr128 = IRB.CreateZExt(PtrInt, I128Ty);
Value *Magic128 = IRB.CreateZExt(IRB.CreateZExtOrTrunc(Magic64, IntptrTy),
@@ -354,9 +361,15 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
- // 2. Check if LowFat pointer: RegionIndex < NumSizeClasses
- Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
- Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+ // 2. Optimized IsLowFat check:
+ // Instead of comparing RegionIndex < NumSizeClasses, we load the Size
+ // from the fixed table and check if it's non-zero. If it's zero, this
+ // is not a LowFat pointer and we skip the check.
+ LLVMContext &Ctx = M.getContext();
+ Type *I64Ty = Type::getInt64Ty(Ctx);
+ Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
+ Value *IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
@@ -364,16 +377,15 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
bool IsWrite = isa<StoreInst>(I) || isa<AtomicRMWInst>(I) ||
isa<AtomicCmpXchgInst>(I);
+ Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
+
#ifdef LOWFAT_CUSTOM_CONFIG
- auto [AllocSize, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
+ auto [_, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
#else
- Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
- Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
- Value *SizeOne = ConstantInt::get(IntptrTy, 1);
- Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
- Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
- Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
- Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
+ Value *Mask64 = loadFromFixedTable(ThenIRB, kTablesBase + 3 * kTablesOffset,
+ I64Ty, RegionIndex);
+ Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy);
+ Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
#endif
emitOobCheck(ThenIRB, PtrInt, Base, AllocSize, FixedAccessSize, nullptr,
@@ -396,22 +408,24 @@ bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
- Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
- Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+ LLVMContext &Ctx = M.getContext();
+ Type *I64Ty = Type::getInt64Ty(Ctx);
+ Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
+ Value *IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
+ Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
+
#ifdef LOWFAT_CUSTOM_CONFIG
- auto [AllocSize, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
+ auto [_, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
#else
- Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
- Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
- Value *SizeOne = ConstantInt::get(IntptrTy, 1);
- Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
- Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
- Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
- Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
+ Value *Mask64 = loadFromFixedTable(ThenIRB, kTablesBase + 3 * kTablesOffset,
+ I64Ty, RegionIndex);
+ Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy);
+ Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
#endif
emitOobCheck(ThenIRB, PtrInt, Base, AllocSize, 0, SizeInt, ThenTerm, IsWrite);
@@ -454,30 +468,32 @@ bool LowFatSanitizer::instrumentGEP(GetElementPtrInst *GEP) {
// RESULT pointer — what we're checking stays within [Base, Base+AllocSize).
Value *ResInt = IRB.CreatePtrToInt(GEP, IntptrTy);
- // 1. Is the source a LowFat pointer?
+ // 1. Compute Base and AllocSize from the SOURCE pointer.
Value *RegionBaseVal = ConstantInt::get(IntptrTy, RegionBase);
Value *RegionOffset = IRB.CreateSub(SrcInt, RegionBaseVal);
Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
- Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
- Value *IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+
+ LLVMContext &Ctx = M.getContext();
+ Type *I64Ty = Type::getInt64Ty(Ctx);
+ Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
+ Value *IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, InsertPt, false);
IRBuilder<> ThenIRB(ThenTerm);
- // 2. Compute Base and AllocSize from the SOURCE pointer.
+ Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
+
#ifdef LOWFAT_CUSTOM_CONFIG
- auto [AllocSize, Base] = emitDynamicBaseMagic(ThenIRB, SrcInt, RegionIndex);
+ auto [_, Base] = emitDynamicBaseMagic(ThenIRB, SrcInt, RegionIndex);
#else
- Value *MinSizeLogVal = ConstantInt::get(IntptrTy, MinSizeLog);
- Value *ShiftAmount = ThenIRB.CreateAdd(RegionIndex, MinSizeLogVal);
- Value *SizeOne = ConstantInt::get(IntptrTy, 1);
- Value *AllocSize = ThenIRB.CreateShl(SizeOne, ShiftAmount);
- Value *SizeMinusOne = ThenIRB.CreateSub(AllocSize, SizeOne);
- Value *Mask = ThenIRB.CreateNot(SizeMinusOne);
- Value *Base = ThenIRB.CreateAnd(SrcInt, Mask);
+ Value *Mask64 = loadFromFixedTable(ThenIRB, kTablesBase + 3 * kTablesOffset,
+ I64Ty, RegionIndex);
+ Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy);
+ Value *Base = ThenIRB.CreateAnd(SrcInt, Mask);
#endif
- // 3. OOB if result underflows (< Base) or reaches/passes the end (>= Base+AllocSize).
+ // 2. OOB if result underflows (< Base) or reaches/passes the end (>= Base+AllocSize).
emitOobCheck(ThenIRB, ResInt, Base, AllocSize, 0, nullptr, ThenTerm, false);
NumInstrumentedGEPs++;
>From 907be35126cb885b7cdcd8c36911a87f9e9124cd Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 11 Mar 2026 22:17:27 +0800
Subject: [PATCH 49/55] [LowFat] Use compact bounds check for access
---
.../Instrumentation/LowFatSanitizer.cpp | 25 +++++++++----------
1 file changed, 12 insertions(+), 13 deletions(-)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 8ecb8b0ced1ae..67f74de396c36 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -319,22 +319,21 @@ void LowFatSanitizer::emitOobCheck(IRBuilder<> &IRB, Value *PtrInt, Value *Base,
Value *AllocSize, uint64_t FixedAccessSize,
Value *DynAccessSize,
Instruction *InsertBefore, bool IsWrite) {
- Value *AccessSize = DynAccessSize;
- if (!AccessSize)
- AccessSize = ConstantInt::get(IntptrTy, FixedAccessSize);
-
- Value *End = IRB.CreateAdd(Base, AllocSize);
- Value *AccessEnd = IRB.CreateAdd(PtrInt, AccessSize);
-
- // For GEPs (no AccessSize/DynAccessSize), we check if result is < Base OR >=
- // End. For loads/stores, we just check if AccessEnd > End.
Value *IsOOB = nullptr;
if (!FixedAccessSize && !DynAccessSize) {
- Value *TooLow = IRB.CreateICmpULT(PtrInt, Base);
- Value *TooHigh = IRB.CreateICmpUGE(PtrInt, End);
- IsOOB = IRB.CreateOr(TooLow, TooHigh);
+ // Compact GEP check: OOB iff (ptr - base) >= alloc_size (unsigned).
+ // This catches both underflow and overflow without separate compares.
+ Value *Diff = IRB.CreateSub(PtrInt, Base);
+ IsOOB = IRB.CreateICmpUGE(Diff, AllocSize);
} else {
- IsOOB = IRB.CreateICmpUGT(AccessEnd, End);
+ Value *AccessSize = DynAccessSize;
+ if (!AccessSize)
+ AccessSize = ConstantInt::get(IntptrTy, FixedAccessSize);
+ Value *Diff = IRB.CreateSub(PtrInt, Base);
+ Value *TooWide = IRB.CreateICmpUGT(AccessSize, AllocSize);
+ Value *Limit = IRB.CreateSub(AllocSize, AccessSize);
+ Value *PastEnd = IRB.CreateICmpUGT(Diff, Limit);
+ IsOOB = IRB.CreateOr(TooWide, PastEnd);
}
Instruction *OobTerm =
>From e532797803cdb3099b80641c1628c6bea9ece021 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Wed, 18 Mar 2026 23:06:40 +0800
Subject: [PATCH 50/55] [LowFat] Add original sizes configuration
---
compiler-rt/lib/lowfat/tools/sizes_orig.cfg | 33 +++++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 compiler-rt/lib/lowfat/tools/sizes_orig.cfg
diff --git a/compiler-rt/lib/lowfat/tools/sizes_orig.cfg b/compiler-rt/lib/lowfat/tools/sizes_orig.cfg
new file mode 100644
index 0000000000000..a04506cef22f1
--- /dev/null
+++ b/compiler-rt/lib/lowfat/tools/sizes_orig.cfg
@@ -0,0 +1,33 @@
+# Original sizes (from research paper)
+16
+32
+64
+128
+256
+512
+1024
+2048
+4096
+8192
+16384
+32768
+65536
+131072
+262144
+524288
+1048576
+2097152
+4194304
+8388608
+16777216
+33554432
+67108864
+134217728
+268435456
+536870912
+1073741824
+2147483648
+4294967296
+8589934592
+17179869184
+34359738368
>From 0d2ad6c452b42c04b802f381f3990c4053a69ec3 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Thu, 19 Mar 2026 16:44:06 +0800
Subject: [PATCH 51/55] [LowFat] Implement right-align mode with transform
---
clang/lib/CodeGen/BackendUtil.cpp | 4 +-
compiler-rt/lib/lowfat/lf_interceptors.cpp | 18 +++--
compiler-rt/lib/lowfat/lf_interface.h | 7 ++
compiler-rt/lib/lowfat/lf_rtl.cpp | 70 ++++++++++++++-----
.../Instrumentation/LowFatSanitizer.h | 7 +-
.../Instrumentation/LowFatSanitizer.cpp | 23 ++++++
6 files changed, 105 insertions(+), 24 deletions(-)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 2aff1a03c6e5d..54ea2751b853a 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -115,7 +115,9 @@ static cl::opt<LowFatSanitizerOptions::LowFatMode> LowFatMode(
clEnumValN(LowFatSanitizerOptions::LowFatMode::Fast, "fast",
"Instrument at OptimizerLastEP (least overhead)"),
clEnumValN(LowFatSanitizerOptions::LowFatMode::Safe, "safe",
- "Barrier at PipelineStartEP + instrument at OptimizerLastEP")));
+ "Barrier at PipelineStartEP + instrument at OptimizerLastEP"),
+ clEnumValN(LowFatSanitizerOptions::LowFatMode::RightAlign, "right-align",
+ "Right-align allocations within class slots to catch right-side OOB")));
// Experiment to mark cold functions as optsize/minsize/optnone.
// TODO: remove once this is exposed as a proper driver flag.
diff --git a/compiler-rt/lib/lowfat/lf_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp
index b857ca845b3ff..0a05d8eb1abed 100644
--- a/compiler-rt/lib/lowfat/lf_interceptors.cpp
+++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp
@@ -22,6 +22,7 @@ using namespace __sanitizer;
namespace __lowfat {
extern bool lowfat_inited;
extern bool lowfat_recover;
+extern bool lowfat_right_align;
} // namespace __lowfat
// DlsymAlloc handles allocations that happen before our runtime is initialized
@@ -105,11 +106,20 @@ INTERCEPTOR(void *, realloc, void *ptr, uptr size) {
// For system pointers, we don't know exact old size, copy 'size' bytes.
uptr copy_size = size;
if (old_is_lowfat) {
- uptr old_size = __lowfat::GetSize((uptr)ptr);
- if (old_size < copy_size)
- copy_size = old_size;
+ uptr old_class_size = __lowfat::GetSize((uptr)ptr);
+ if (old_class_size < copy_size)
+ copy_size = old_class_size;
}
- internal_memcpy(new_ptr, ptr, copy_size);
+ // In right-align mode the returned pointer is offset within its slot:
+ // ptr = slot_base + (class_size - requested_size)
+ // Copying 'copy_size' bytes from 'ptr' would read past the slot end.
+ // Instead copy from the slot base so we stay within the mapped region.
+ // The user data starts at ptr, but copying from the base is safe since
+ // the left padding is zeroed on allocation and belongs to the same slot.
+ const void *copy_src = __lowfat::lowfat_right_align && old_is_lowfat
+ ? (const void *)__lowfat::GetBase((uptr)ptr)
+ : ptr;
+ internal_memcpy(new_ptr, copy_src, copy_size);
// Free old
if (old_is_lowfat)
__lowfat::Deallocate(ptr);
diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h
index b6840c4ef3e13..62a1199bdd6fa 100644
--- a/compiler-rt/lib/lowfat/lf_interface.h
+++ b/compiler-rt/lib/lowfat/lf_interface.h
@@ -35,6 +35,13 @@ SANITIZER_INTERFACE_ATTRIBUTE void __lf_init();
// -fsanitize-recover=lowfat to the runtime interceptors.
SANITIZER_INTERFACE_ATTRIBUTE void __lf_set_recover(int recover);
+// Called from a compiler-generated module constructor when -lowfat-mode=right-align
+// is active. Instructs the allocator to right-align objects within their size-class
+// slot so the object's right edge coincides with the slot boundary, turning any
+// off-by-one overflow into a detectable OOB. The trade-off is a blind spot on the
+// left (underflow) side of (class_size - requested_size) bytes.
+SANITIZER_INTERFACE_ATTRIBUTE void __lf_set_right_align(int right_align);
+
SANITIZER_INTERFACE_ATTRIBUTE void __lf_report_oob(uptr ptr, uptr base,
uptr bound, int is_write);
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index a4910da794d26..47db1f98d81dc 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -34,6 +34,13 @@ bool lowfat_inited = false;
// interceptor-level OOB (memset/memcpy/memmove) warns-and-continues or aborts.
bool lowfat_recover = false;
+// Set to true when -lowfat-mode=right-align is active. Instructs Allocate()
+// to right-align objects within their size-class slot so the object's right
+// edge coincides with the slot boundary. This makes off-by-one overflows
+// detectable (right OOB is caught) at the cost of a left-side blind spot of
+// (class_size - requested_size) bytes.
+bool lowfat_right_align = false;
+
// Maximum number of size classes across both modes.
// In POW2-only mode kNumSizeClasses=27; with a custom config it can be up to
// LOWFAT_MAX_ARRAY_SIZE. We size the static arrays at compile time using
@@ -183,6 +190,15 @@ static bool InitMemoryRegions() {
// Allocate from a LowFat region
// First checks the free list, then falls back to bump allocation.
// Thread-safe: protected by per-size-class spin mutex.
+//
+// In right-align mode, returns slot_base + (class_size - requested_size) so
+// the object's right edge coincides with the slot boundary. The bounds check
+// (ptr - GetBase(ptr)) < class_size is still correct: GetBase() recovers
+// slot_base via mask/magic since slot_base is always class-aligned, and any
+// access past slot_base+class_size fails the check.
+//
+// The free list always stores slot bases (not right-aligned pointers) so that
+// freed blocks can be reused with a different offset for a new request size.
void *Allocate(uptr size) {
if (size == 0)
size = 1;
@@ -195,31 +211,43 @@ void *Allocate(uptr size) {
SpinMutexLock lock(®ion_locks[class_index]);
- // 1. Try free list first
+ uptr slot_base;
+
+ // 1. Try free list first (stores slot bases)
FreeBlock *block = free_lists[class_index];
if (block) {
free_lists[class_index] = block->next;
- // Zero the memory (free list pointer was stored here)
+ slot_base = (uptr)block;
+ // Zero the entire slot (free list pointer was stored at slot_base)
internal_memset(block, 0, alloc_size);
- return (void *)block;
- }
-
- // 2. Fall back to bump allocation
- uptr region_end = GetRegionStart(class_index) + kRegionSize;
- uptr addr = region_next_alloc[class_index];
+ } else {
+ // 2. Fall back to bump allocation
+ uptr region_end = GetRegionStart(class_index) + kRegionSize;
+ uptr addr = region_next_alloc[class_index];
+
+ if (addr + alloc_size > region_end) {
+ // Region exhausted: fall back to standard libc-style allocation.
+ // The resulting pointer will not be a LowFat pointer (wide-bounds).
+ return (void *)InternalAlloc(size);
+ }
- if (addr + alloc_size > region_end) {
- // Region exhausted: fall back to standard libc-style allocation.
- // The resulting pointer will not be a LowFat pointer (wide-bounds).
- return (void *)InternalAlloc(size);
+ region_next_alloc[class_index] = addr + alloc_size;
+ slot_base = addr;
}
- region_next_alloc[class_index] = addr + alloc_size;
- return (void *)addr;
+ // In right-align mode shift the returned pointer so the object's right
+ // edge sits at the slot boundary, making overflows immediately detectable.
+ if (lowfat_right_align)
+ return (void *)(slot_base + (alloc_size - size));
+ return (void *)slot_base;
}
-// Free a LowFat allocation by pushing it onto the free list.
+// Free a LowFat allocation by pushing its slot base onto the free list.
// Thread-safe: protected by per-size-class spin mutex.
+//
+// We always push the slot base (GetBase(ptr)) rather than ptr itself so that
+// freed slots can be reused with a different right-align offset for a new
+// request size, and so the free list is consistent regardless of mode.
void Deallocate(void *ptr) {
if (!ptr)
return;
@@ -233,11 +261,14 @@ void Deallocate(void *ptr) {
}
uptr region = GetRegionIndex(addr);
+ // Recover the slot base: in right-align mode ptr is offset within the slot;
+ // in normal mode GetBase(addr) == addr since allocations are class-aligned.
+ uptr slot_base = GetBase(addr);
SpinMutexLock lock(®ion_locks[region]);
- // Push to the head of the free list for this size class
- FreeBlock *block = (FreeBlock *)ptr;
+ // Push slot base to the head of the free list for this size class
+ FreeBlock *block = (FreeBlock *)slot_base;
block->next = free_lists[region];
free_lists[region] = block;
}
@@ -279,6 +310,11 @@ void __lf_set_recover(int recover) {
__lowfat::lowfat_recover = (recover != 0);
}
+SANITIZER_INTERFACE_ATTRIBUTE
+void __lf_set_right_align(int right_align) {
+ __lowfat::lowfat_right_align = (right_align != 0);
+}
+
SANITIZER_INTERFACE_ATTRIBUTE
void __lf_init() {
if (__lowfat::lowfat_inited)
diff --git a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
index 2403aa9da2430..543077677527e 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h
@@ -17,8 +17,11 @@ struct LowFatSanitizerOptions {
bool Recover = false;
enum class LowFatMode {
- Fast, /// instrument at OptimizerLastEP
- Safe, /// Barrier at PipelineStartEP + instrument at OptimizerLastEP
+ Fast, /// instrument at OptimizerLastEP
+ Safe, /// Barrier at PipelineStartEP + instrument at OptimizerLastEP
+ RightAlign, /// Fast instrumentation + right-align allocations within class
+ /// slots to improve detection of right-side (overflow) OOB at
+ /// the cost of a blind spot on the left (underflow) side.
};
LowFatMode Mode = LowFatMode::Fast;
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 67f74de396c36..edd6b32913f33 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -611,6 +611,29 @@ bool LowFatSanitizer::run() {
Modified = true;
}
+ // Emit a module constructor that calls __lf_set_right_align(1) so the
+ // runtime allocator right-aligns objects within their size-class slot.
+ // Right-aligning places the object's right edge at the slot boundary,
+ // making off-by-one overflows detectable at the cost of a left-side
+ // blind spot of (class_size - requested_size) bytes.
+ if (Options.Mode == LowFatSanitizerOptions::LowFatMode::RightAlign) {
+ LLVMContext &Ctx = M.getContext();
+ FunctionType *SetRightAlignTy =
+ FunctionType::get(Type::getVoidTy(Ctx), {Type::getInt32Ty(Ctx)}, false);
+ FunctionCallee SetRightAlignFn =
+ M.getOrInsertFunction("__lf_set_right_align", SetRightAlignTy);
+ Function *Ctor = Function::Create(
+ FunctionType::get(Type::getVoidTy(Ctx), false),
+ GlobalValue::InternalLinkage, "__lowfat_set_right_align_ctor", &M);
+ BasicBlock *BB = BasicBlock::Create(Ctx, "entry", Ctor);
+ IRBuilder<> CtorBuilder(BB);
+ CtorBuilder.CreateCall(SetRightAlignFn,
+ {ConstantInt::get(Type::getInt32Ty(Ctx), 1)});
+ CtorBuilder.CreateRetVoid();
+ appendToGlobalCtors(M, Ctor, /*Priority=*/0);
+ Modified = true;
+ }
+
return Modified;
}
>From d7323631809a486c15f5c3f2d079381afa46faf7 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Thu, 19 Mar 2026 16:44:51 +0800
Subject: [PATCH 52/55] [LowFat] Add tests for right-alignn mode
---
.../TestCases/right_align/free_list_reuse.cpp | 37 +++++++++++++++++++
.../lowfat/TestCases/right_align/inbounds.cpp | 32 ++++++++++++++++
.../right_align/left_padding_blind_spot.cpp | 33 +++++++++++++++++
.../TestCases/right_align/overflow_caught.cpp | 27 ++++++++++++++
compiler-rt/test/lowfat/lit.cfg.py | 5 +++
5 files changed, 134 insertions(+)
create mode 100644 compiler-rt/test/lowfat/TestCases/right_align/free_list_reuse.cpp
create mode 100644 compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp
create mode 100644 compiler-rt/test/lowfat/TestCases/right_align/left_padding_blind_spot.cpp
create mode 100644 compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp
diff --git a/compiler-rt/test/lowfat/TestCases/right_align/free_list_reuse.cpp b/compiler-rt/test/lowfat/TestCases/right_align/free_list_reuse.cpp
new file mode 100644
index 0000000000000..b5c506da2fb9f
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/right_align/free_list_reuse.cpp
@@ -0,0 +1,37 @@
+// RUN: %clangxx_lowfat_right_align -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_right_align -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+
+// Free-list reuse correctness test for right-align mode.
+//
+// Both 17 and 25 bytes land in the 32-byte class. When the 17-byte slot is
+// freed, Deallocate must push the slot BASE (not the shifted pointer slot_base+15)
+// onto the free list. The subsequent 25-byte allocation then reuses the same
+// slot but with a different offset (slot_base+7), and all 25 bytes must be
+// accessible without OOB.
+
+#include <cstdio>
+#include <cstdlib>
+
+int main() {
+ // First allocation: 17 bytes → offset 15 within 32-byte slot.
+ char *a = (char *)malloc(17);
+ if (!a) return 1;
+ for (int i = 0; i < 17; i++) a[i] = (char)i;
+ free(a);
+
+ // Second allocation: 25 bytes → offset 7 within the same (reused) 32-byte slot.
+ char *b = (char *)malloc(25);
+ if (!b) return 1;
+
+ // Write and read back all 25 bytes — none must trigger OOB.
+ for (int i = 0; i < 25; i++) b[i] = (char)(i + 1);
+ for (int i = 0; i < 25; i++)
+ if (b[i] != (char)(i + 1)) return 2;
+
+ free(b);
+
+ // CHECK: free_list_reuse: ok
+ // CHECK-NOT: LOWFAT ERROR
+ printf("free_list_reuse: ok\n");
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp b/compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp
new file mode 100644
index 0000000000000..8a8877957aa93
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp
@@ -0,0 +1,32 @@
+// RUN: %clangxx_lowfat_right_align -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx_lowfat_right_align -O2 %s -o %t && %run %t 2>&1 | FileCheck %s
+
+// In right-align mode, all accesses within the requested allocation size must
+// not trigger OOB (no false positives).
+//
+// A 17-byte request lands in the 32-byte class. In right-align mode the object
+// is placed at slot_base+15, so its right edge coincides with the slot boundary
+// at slot_base+32. Bytes buf[0]..buf[16] are all valid.
+
+#include <cstdio>
+#include <cstdlib>
+
+int main() {
+ // 17 bytes → 32-byte class; object at slot_base+15 in right-align mode.
+ char *p = (char *)malloc(17);
+ if (!p) return 1;
+
+ // Write every byte of the requested allocation.
+ for (int i = 0; i < 17; i++)
+ p[i] = (char)i;
+
+ // Read back and verify.
+ for (int i = 0; i < 17; i++)
+ if (p[i] != (char)i) return 2;
+
+ // CHECK: inbounds: ok
+ // CHECK-NOT: LOWFAT ERROR
+ printf("inbounds: ok\n");
+ free(p);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/TestCases/right_align/left_padding_blind_spot.cpp b/compiler-rt/test/lowfat/TestCases/right_align/left_padding_blind_spot.cpp
new file mode 100644
index 0000000000000..a3cfb650230cd
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/right_align/left_padding_blind_spot.cpp
@@ -0,0 +1,33 @@
+// RUN: %clangxx_lowfat_right_align -O0 %s -o %t && %run %t 2>&1 | FileCheck %s
+
+// Documents the known trade-off of right-align mode: underflows into the left
+// padding are not caught because the shifted pointer still falls within the
+// same 32-byte slot.
+//
+// 17-byte object in right-align mode: slot = [slot_base, slot_base+32)
+// left padding: [slot_base, slot_base+15) ← blind spot
+// live object: [slot_base+15, slot_base+32) ← buf[0]..buf[16]
+//
+// buf[-1] = slot_base+14, which is inside the slot:
+// GetBase(slot_base+14) = slot_base
+// (slot_base+14 - slot_base) = 14 < 32 → NOT OOB
+
+#include <cstdio>
+#include <cstdlib>
+
+int main() {
+ // 17 bytes → 32-byte class; object at slot_base+15.
+ char *buf = (char *)malloc(17);
+ if (!buf) return 1;
+
+ // Write one byte into the left padding (blind spot).
+ // This is technically out-of-bounds for the 17-byte allocation, but right-align
+ // mode cannot detect it because the access stays within the 32-byte slot.
+ buf[-1] = 'X';
+
+ // CHECK: blind spot: not caught (left padding)
+ // CHECK-NOT: LOWFAT ERROR
+ printf("blind spot: not caught (left padding)\n");
+ free(buf);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp b/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp
new file mode 100644
index 0000000000000..4abfa72700a12
--- /dev/null
+++ b/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp
@@ -0,0 +1,27 @@
+// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-MISS
+// RUN: %clangxx_lowfat_right_align -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefix=CHECK-CATCH
+
+// Mode-difference test: one-past-end overflow on a non-POW2-sized allocation.
+//
+// Default (left-align): 17-byte object at slot_base; buf[17] falls in the
+// 15-byte right padding → access is within the 32-byte slot → NOT caught.
+//
+// Right-align: 17-byte object at slot_base+15; buf[17] = slot_base+32, which
+// is exactly the slot boundary → OOB → caught.
+
+#include <cstdio>
+#include <cstdlib>
+
+int main() {
+ // 17 bytes → 32-byte class.
+ char *buf = (char *)malloc(17);
+ if (!buf) return 1;
+
+ buf[17] = 'X'; // one-past-end write
+
+ // CHECK-MISS: overflow: not caught (in right padding)
+ // CHECK-CATCH: LOWFAT ERROR: out-of-bounds error detected!
+ printf("overflow: not caught (in right padding)\n");
+ free(buf);
+ return 0;
+}
diff --git a/compiler-rt/test/lowfat/lit.cfg.py b/compiler-rt/test/lowfat/lit.cfg.py
index 948a4f0620d29..29bbd5ce2d409 100644
--- a/compiler-rt/test/lowfat/lit.cfg.py
+++ b/compiler-rt/test/lowfat/lit.cfg.py
@@ -44,8 +44,13 @@ def build_invocation(flags):
# safe mode (fast mode is the default)
lowfat_safe = lowfat_base + ["-mllvm", "-lowfat-mode=safe"]
+# right-align mode: allocations placed at slot_base+(class_size-requested_size)
+# so the object's right edge coincides with the slot boundary.
+lowfat_right_align = lowfat_base + ["-mllvm", "-lowfat-mode=right-align"]
+
config.substitutions.append(("%clangxx_lowfat ", build_invocation(lowfat_base)))
config.substitutions.append(("%clangxx_lowfat_safe ", build_invocation(lowfat_safe)))
+config.substitutions.append(("%clangxx_lowfat_right_align ", build_invocation(lowfat_right_align)))
# Recover mode versions
config.substitutions.append(
>From 05bdf473c34a9fe4bf88c34c5703b61222460a37 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 24 Mar 2026 13:31:08 +0800
Subject: [PATCH 53/55] [LowFat] Guard metadata loads with region range check
on Darwin
---
.../Instrumentation/LowFatSanitizer.cpp | 61 ++++++++++++++-----
1 file changed, 47 insertions(+), 14 deletions(-)
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index edd6b32913f33..938d84d5b5804 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -26,6 +26,7 @@
#include "llvm/IR/Type.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/ModRef.h"
+#include "llvm/TargetParser/Triple.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"
#include "llvm/Transforms/Utils/ModuleUtils.h"
@@ -52,7 +53,8 @@ class LowFatSanitizer {
public:
LowFatSanitizer(Module &M, const LowFatSanitizerOptions &Options)
: M(M), Options(Options), DL(M.getDataLayout()),
- IntptrTy(DL.getIntPtrType(M.getContext())) {}
+ IntptrTy(DL.getIntPtrType(M.getContext())),
+ UseDarwinMetadataGuard(Triple(M.getTargetTriple()).isOSDarwin()) {}
bool run();
@@ -61,6 +63,7 @@ class LowFatSanitizer {
const LowFatSanitizerOptions &Options;
const DataLayout &DL;
Type *IntptrTy;
+ const bool UseDarwinMetadataGuard;
FunctionCallee ReportOobFn = nullptr;
FunctionCallee WarnOobFn = nullptr;
@@ -360,15 +363,22 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Value *RegionOffset = IRB.CreateSub(PtrInt, RegionBaseVal);
Value *RegionIndex = IRB.CreateLShr(RegionOffset, RegionSizeLog);
- // 2. Optimized IsLowFat check:
- // Instead of comparing RegionIndex < NumSizeClasses, we load the Size
- // from the fixed table and check if it's non-zero. If it's zero, this
- // is not a LowFat pointer and we skip the check.
+ // 2. Darwin-first safety guard:
+ // On Darwin, prove the pointer is in a valid LowFat region before touching
+ // the fixed metadata tables. Other targets keep the current table-driven
+ // classification for now.
LLVMContext &Ctx = M.getContext();
Type *I64Ty = Type::getInt64Ty(Ctx);
- Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
- I64Ty, RegionIndex);
- Value *IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
+ Value *AllocSize64 = nullptr;
+ Value *IsLowFat = nullptr;
+ if (UseDarwinMetadataGuard) {
+ Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
+ IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+ } else {
+ AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
+ IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
+ }
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
@@ -376,6 +386,9 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
bool IsWrite = isa<StoreInst>(I) || isa<AtomicRMWInst>(I) ||
isa<AtomicCmpXchgInst>(I);
+ if (!AllocSize64)
+ AllocSize64 = loadFromFixedTable(ThenIRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
#ifdef LOWFAT_CUSTOM_CONFIG
@@ -409,13 +422,23 @@ bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
LLVMContext &Ctx = M.getContext();
Type *I64Ty = Type::getInt64Ty(Ctx);
- Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
- I64Ty, RegionIndex);
- Value *IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
+ Value *AllocSize64 = nullptr;
+ Value *IsLowFat = nullptr;
+ if (UseDarwinMetadataGuard) {
+ Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
+ IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+ } else {
+ AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
+ IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
+ }
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false);
IRBuilder<> ThenIRB(ThenTerm);
+ if (!AllocSize64)
+ AllocSize64 = loadFromFixedTable(ThenIRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
#ifdef LOWFAT_CUSTOM_CONFIG
@@ -474,13 +497,23 @@ bool LowFatSanitizer::instrumentGEP(GetElementPtrInst *GEP) {
LLVMContext &Ctx = M.getContext();
Type *I64Ty = Type::getInt64Ty(Ctx);
- Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
- I64Ty, RegionIndex);
- Value *IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
+ Value *AllocSize64 = nullptr;
+ Value *IsLowFat = nullptr;
+ if (UseDarwinMetadataGuard) {
+ Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
+ IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
+ } else {
+ AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
+ IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
+ }
Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, InsertPt, false);
IRBuilder<> ThenIRB(ThenTerm);
+ if (!AllocSize64)
+ AllocSize64 = loadFromFixedTable(ThenIRB, kTablesBase + 0 * kTablesOffset,
+ I64Ty, RegionIndex);
Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
#ifdef LOWFAT_CUSTOM_CONFIG
>From 52875f3465d4e43691d1e780db6ad9e881d5db0c Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 24 Mar 2026 14:02:43 +0800
Subject: [PATCH 54/55] [LowFat] Move metadata tables to a higher fixed address
---
compiler-rt/lib/lowfat/lf_rtl.cpp | 12 ++++----
.../Instrumentation/LowFatSanitizer.cpp | 30 ++++++++++---------
2 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 47db1f98d81dc..af6bedcfa934d 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -76,11 +76,11 @@ static StaticSpinMutex region_locks[kMaxSizeClasses];
// are mapped during initialization. This allows the LLVM pass to use
// absolute addressing (imm[index*8]) instead of PC-relative loads.
//
-// 0x200000000: Sizes (8 bytes per class)
-// 0x201000000: Magics (8 bytes per class)
-// 0x202000000: IsPow2 (1 byte per class)
-// 0x203000000: Masks (8 bytes per class)
-static constexpr uptr kTablesBase = 0x200000000ULL;
+// 0x118000000000: Sizes (8 bytes per class)
+// 0x118001000000: Magics (8 bytes per class)
+// 0x118002000000: IsPow2 (1 byte per class)
+// 0x118003000000: Masks (8 bytes per class)
+static constexpr uptr kTablesBase = 0x118000000000ULL;
static constexpr uptr kTablesOffset = 0x1000000ULL; // 16 MB between tables
static void InitializeFlags() {
@@ -392,4 +392,4 @@ __attribute__((section(".preinit_array"), used)) static auto preinit =
__attribute__((constructor)) static void lowfat_constructor() {
__lf_init();
}
-#endif
\ No newline at end of file
+#endif
diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
index 938d84d5b5804..f4d287dd2a970 100644
--- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp
@@ -54,7 +54,8 @@ class LowFatSanitizer {
LowFatSanitizer(Module &M, const LowFatSanitizerOptions &Options)
: M(M), Options(Options), DL(M.getDataLayout()),
IntptrTy(DL.getIntPtrType(M.getContext())),
- UseDarwinMetadataGuard(Triple(M.getTargetTriple()).isOSDarwin()) {}
+ UseDarwinMetadataGuard(Triple(M.getTargetTriple()).isOSDarwin()),
+ TablesBase(kTablesBase) {}
bool run();
@@ -64,6 +65,7 @@ class LowFatSanitizer {
const DataLayout &DL;
Type *IntptrTy;
const bool UseDarwinMetadataGuard;
+ const uint64_t TablesBase;
FunctionCallee ReportOobFn = nullptr;
FunctionCallee WarnOobFn = nullptr;
@@ -128,7 +130,7 @@ class LowFatSanitizer {
#endif
// Fixed absolute addresses for metadata tables (must match lf_rtl.cpp)
- static constexpr uint64_t kTablesBase = 0x200000000ULL;
+ static constexpr uint64_t kTablesBase = 0x118000000000ULL;
static constexpr uint64_t kTablesOffset = 0x1000000ULL;
};
@@ -268,9 +270,9 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
Type *I64Ty = Type::getInt64Ty(Ctx);
Type *I128Ty = Type::getInt128Ty(Ctx);
- Value *AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ Value *AllocSize64 = loadFromFixedTable(IRB, TablesBase + 0 * kTablesOffset,
I64Ty, RegionIndex);
- Value *Mask64 = loadFromFixedTable(IRB, kTablesBase + 3 * kTablesOffset,
+ Value *Mask64 = loadFromFixedTable(IRB, TablesBase + 3 * kTablesOffset,
I64Ty, RegionIndex);
// Narrow to IntptrTy (which is i64 on 64-bit targets)
@@ -302,7 +304,7 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt,
return {AllocSize, BaseAnd};
// --- MUL (non-POW2) base ---
- Value *Magic64 = loadFromFixedTable(IRB, kTablesBase + 1 * kTablesOffset,
+ Value *Magic64 = loadFromFixedTable(IRB, TablesBase + 1 * kTablesOffset,
I64Ty, RegionIndex);
Value *Ptr128 = IRB.CreateZExt(PtrInt, I128Ty);
@@ -375,7 +377,7 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
} else {
- AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ AllocSize64 = loadFromFixedTable(IRB, TablesBase + 0 * kTablesOffset,
I64Ty, RegionIndex);
IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
}
@@ -387,14 +389,14 @@ bool LowFatSanitizer::instrumentMemoryAccess(Instruction *I, Value *Ptr,
isa<AtomicCmpXchgInst>(I);
if (!AllocSize64)
- AllocSize64 = loadFromFixedTable(ThenIRB, kTablesBase + 0 * kTablesOffset,
+ AllocSize64 = loadFromFixedTable(ThenIRB, TablesBase + 0 * kTablesOffset,
I64Ty, RegionIndex);
Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
#ifdef LOWFAT_CUSTOM_CONFIG
auto [_, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
#else
- Value *Mask64 = loadFromFixedTable(ThenIRB, kTablesBase + 3 * kTablesOffset,
+ Value *Mask64 = loadFromFixedTable(ThenIRB, TablesBase + 3 * kTablesOffset,
I64Ty, RegionIndex);
Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy);
Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
@@ -428,7 +430,7 @@ bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
} else {
- AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ AllocSize64 = loadFromFixedTable(IRB, TablesBase + 0 * kTablesOffset,
I64Ty, RegionIndex);
IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
}
@@ -437,14 +439,14 @@ bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr,
IRBuilder<> ThenIRB(ThenTerm);
if (!AllocSize64)
- AllocSize64 = loadFromFixedTable(ThenIRB, kTablesBase + 0 * kTablesOffset,
+ AllocSize64 = loadFromFixedTable(ThenIRB, TablesBase + 0 * kTablesOffset,
I64Ty, RegionIndex);
Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
#ifdef LOWFAT_CUSTOM_CONFIG
auto [_, Base] = emitDynamicBaseMagic(ThenIRB, PtrInt, RegionIndex);
#else
- Value *Mask64 = loadFromFixedTable(ThenIRB, kTablesBase + 3 * kTablesOffset,
+ Value *Mask64 = loadFromFixedTable(ThenIRB, TablesBase + 3 * kTablesOffset,
I64Ty, RegionIndex);
Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy);
Value *Base = ThenIRB.CreateAnd(PtrInt, Mask);
@@ -503,7 +505,7 @@ bool LowFatSanitizer::instrumentGEP(GetElementPtrInst *GEP) {
Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses);
IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion);
} else {
- AllocSize64 = loadFromFixedTable(IRB, kTablesBase + 0 * kTablesOffset,
+ AllocSize64 = loadFromFixedTable(IRB, TablesBase + 0 * kTablesOffset,
I64Ty, RegionIndex);
IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0));
}
@@ -512,14 +514,14 @@ bool LowFatSanitizer::instrumentGEP(GetElementPtrInst *GEP) {
IRBuilder<> ThenIRB(ThenTerm);
if (!AllocSize64)
- AllocSize64 = loadFromFixedTable(ThenIRB, kTablesBase + 0 * kTablesOffset,
+ AllocSize64 = loadFromFixedTable(ThenIRB, TablesBase + 0 * kTablesOffset,
I64Ty, RegionIndex);
Value *AllocSize = ThenIRB.CreateZExtOrTrunc(AllocSize64, IntptrTy);
#ifdef LOWFAT_CUSTOM_CONFIG
auto [_, Base] = emitDynamicBaseMagic(ThenIRB, SrcInt, RegionIndex);
#else
- Value *Mask64 = loadFromFixedTable(ThenIRB, kTablesBase + 3 * kTablesOffset,
+ Value *Mask64 = loadFromFixedTable(ThenIRB, TablesBase + 3 * kTablesOffset,
I64Ty, RegionIndex);
Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy);
Value *Base = ThenIRB.CreateAnd(SrcInt, Mask);
>From 354ce9a44e941853fe129edb48d3f1635082e346 Mon Sep 17 00:00:00 2001
From: duckyfuz <108561447+duckyfuz at users.noreply.github.com>
Date: Tue, 24 Mar 2026 14:07:46 +0800
Subject: [PATCH 55/55] [LowFat] Add stack traces upon error
---
compiler-rt/lib/lowfat/CMakeLists.txt | 2 ++
compiler-rt/lib/lowfat/lf_rtl.cpp | 15 +++++++++----
compiler-rt/lib/lowfat/lf_stack.cpp | 27 ++++++++++++++++++++++
compiler-rt/lib/lowfat/lf_stack.h | 32 +++++++++++++++++++++++++++
4 files changed, 72 insertions(+), 4 deletions(-)
create mode 100644 compiler-rt/lib/lowfat/lf_stack.cpp
create mode 100644 compiler-rt/lib/lowfat/lf_stack.h
diff --git a/compiler-rt/lib/lowfat/CMakeLists.txt b/compiler-rt/lib/lowfat/CMakeLists.txt
index 07e4ffe85af06..e85139582d840 100644
--- a/compiler-rt/lib/lowfat/CMakeLists.txt
+++ b/compiler-rt/lib/lowfat/CMakeLists.txt
@@ -59,12 +59,14 @@ endif()
set(LOWFAT_SOURCES
lf_rtl.cpp
lf_interceptors.cpp
+ lf_stack.cpp
)
set(LOWFAT_HEADERS
lf_allocator.h
lf_config.h
lf_interface.h
+ lf_stack.h
)
set(LOWFAT_CFLAGS ${SANITIZER_COMMON_CFLAGS})
diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp
index 47db1f98d81dc..feb51e25050a1 100644
--- a/compiler-rt/lib/lowfat/lf_rtl.cpp
+++ b/compiler-rt/lib/lowfat/lf_rtl.cpp
@@ -17,6 +17,7 @@
#include "lf_allocator.h"
#include "lf_config.h"
#include "lf_interface.h"
+#include "lf_stack.h"
#include "sanitizer_common/sanitizer_common.h"
#include "sanitizer_common/sanitizer_allocator_internal.h"
#include "sanitizer_common/sanitizer_flag_parser.h"
@@ -290,13 +291,17 @@ static void PrintOobHeader(const char *level, uptr ptr, uptr base, uptr bound,
Printf("\n");
}
-static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound, int is_write) {
+static void PrintErrorAndDie(uptr ptr, uptr base, uptr bound, int is_write,
+ const StackTrace &stack) {
PrintOobHeader("ERROR", ptr, base, bound, is_write);
+ stack.Print();
Die();
}
-static void PrintWarning(uptr ptr, uptr base, uptr bound, int is_write) {
+static void PrintWarning(uptr ptr, uptr base, uptr bound, int is_write,
+ const StackTrace &stack) {
PrintOobHeader("WARNING", ptr, base, bound, is_write);
+ stack.Print();
}
} // namespace __lowfat
@@ -336,12 +341,14 @@ void __lf_init() {
SANITIZER_INTERFACE_ATTRIBUTE
void __lf_report_oob(uptr ptr, uptr base, uptr bound, int is_write) {
- __lowfat::PrintErrorAndDie(ptr, base, bound, is_write);
+ GET_STACK_TRACE_FATAL_HERE;
+ __lowfat::PrintErrorAndDie(ptr, base, bound, is_write, stack);
}
SANITIZER_INTERFACE_ATTRIBUTE
void __lf_warn_oob(uptr ptr, uptr base, uptr bound, int is_write) {
- __lowfat::PrintWarning(ptr, base, bound, is_write);
+ GET_STACK_TRACE_FATAL_HERE;
+ __lowfat::PrintWarning(ptr, base, bound, is_write, stack);
}
SANITIZER_INTERFACE_ATTRIBUTE
diff --git a/compiler-rt/lib/lowfat/lf_stack.cpp b/compiler-rt/lib/lowfat/lf_stack.cpp
new file mode 100644
index 0000000000000..030ea4074af0e
--- /dev/null
+++ b/compiler-rt/lib/lowfat/lf_stack.cpp
@@ -0,0 +1,27 @@
+//===-- lf_stack.cpp - LowFat stack trace implementation ------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Provides BufferedStackTrace::UnwindImpl for the LowFat runtime.
+//
+// LowFat does not maintain a thread registry, so we obtain the stack bounds
+// directly from the OS via GetThreadStackTopAndBottom().
+//
+//===----------------------------------------------------------------------===//
+
+#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_stacktrace.h"
+
+void __sanitizer::BufferedStackTrace::UnwindImpl(
+ uptr pc, uptr bp, void *context, bool request_fast, u32 max_depth) {
+ size = 0;
+ request_fast = StackTrace::WillUseFastUnwind(request_fast);
+ uptr stack_top = 0, stack_bottom = 0;
+ GetThreadStackTopAndBottom(/*at_initialization=*/false, &stack_top,
+ &stack_bottom);
+ Unwind(max_depth, pc, bp, context, stack_top, stack_bottom, request_fast);
+}
diff --git a/compiler-rt/lib/lowfat/lf_stack.h b/compiler-rt/lib/lowfat/lf_stack.h
new file mode 100644
index 0000000000000..06aa3981e1629
--- /dev/null
+++ b/compiler-rt/lib/lowfat/lf_stack.h
@@ -0,0 +1,32 @@
+//===-- lf_stack.h - LowFat stack trace utilities ---------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Macros for capturing and printing stack traces in the LowFat runtime.
+//
+//===----------------------------------------------------------------------===//
+#ifndef LF_STACK_H
+#define LF_STACK_H
+
+#include "sanitizer_common/sanitizer_flags.h"
+#include "sanitizer_common/sanitizer_stacktrace.h"
+
+// Capture a stack trace at the current call site, suitable for fatal error
+// reporting. The resulting BufferedStackTrace is named `stack`.
+#define GET_STACK_TRACE_FATAL_HERE \
+ UNINITIALIZED __sanitizer::BufferedStackTrace stack; \
+ stack.Unwind(__sanitizer::StackTrace::GetCurrentPc(), GET_CURRENT_FRAME(), \
+ nullptr, common_flags()->fast_unwind_on_fatal)
+
+// Capture and immediately print the stack trace at the current call site.
+#define PRINT_CURRENT_STACK() \
+ { \
+ GET_STACK_TRACE_FATAL_HERE; \
+ stack.Print(); \
+ }
+
+#endif // LF_STACK_H
More information about the llvm-commits
mailing list