From d92871e642d01eeece130221150770a6cb887250 Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:03:44 +0800 Subject: [PATCH 01/62] [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 { +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 parseHWASanPassOptions(StringRef Params) { return Result; } +Expected parseLowFatPassOptions(StringRef Params) { + LowFatSanitizerOptions Result; + if (!Params.empty()) { + return make_error( + formatv("invalid LowFatSanitizer pass parameter '{}'", Params).str(), + inconvertibleErrorCode()); + } + return Result; +} + Expected 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 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@users.noreply.github.com> Date: Wed, 21 Jan 2026 23:25:30 +0800 Subject: [PATCH 02/62] [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(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@users.noreply.github.com> Date: Wed, 21 Jan 2026 23:41:48 +0800 Subject: [PATCH 03/62] [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@users.noreply.github.com> Date: Wed, 21 Jan 2026 23:45:51 +0800 Subject: [PATCH 04/62] [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@users.noreply.github.com> Date: Wed, 21 Jan 2026 23:58:05 +0800 Subject: [PATCH 05/62] [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@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:35:10 +0800 Subject: [PATCH 06/62] [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>, 16> ToInstrument; + + for (Instruction &I : instructions(F)) { + Value *Ptr = nullptr; + Type *AccessTy = nullptr; + + if (auto *LI = dyn_cast(&I)) { + if (!LI->isVolatile()) { + Ptr = LI->getPointerOperand(); + AccessTy = LI->getType(); + } + } else if (auto *SI = dyn_cast(&I)) { + if (!SI->isVolatile()) { + Ptr = SI->getPointerOperand(); + AccessTy = SI->getValueOperand()->getType(); + } + } else if (auto *AI = dyn_cast(&I)) { + if (!AI->isVolatile()) { + Ptr = AI->getPointerOperand(); + AccessTy = AI->getValOperand()->getType(); + } + } else if (auto *AI = dyn_cast(&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(I)) + ++NumInstrumentedLoads; + else if (isa(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@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:39:58 +0800 Subject: [PATCH 07/62] [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@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:52:15 +0800 Subject: [PATCH 08/62] [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@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:37:33 +0800 Subject: [PATCH 09/62] [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@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:45:26 +0800 Subject: [PATCH 10/62] [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@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:49:01 +0800 Subject: [PATCH 11/62] [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@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:42:47 +0800 Subject: [PATCH 12/62] [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 { + 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@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:00:16 +0800 Subject: [PATCH 13/62] [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@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:01:10 +0800 Subject: [PATCH 14/62] [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@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:02:22 +0800 Subject: [PATCH 15/62] [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@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:02:25 +0800 Subject: [PATCH 16/62] [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 { 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 parseHWASanPassOptions(StringRef Params) { Expected parseLowFatPassOptions(StringRef Params) { LowFatSanitizerOptions Result; if (!Params.empty()) { - return make_error( - formatv("invalid LowFatSanitizer pass parameter '{}'", Params).str(), - inconvertibleErrorCode()); + for (StringRef Param : llvm::split(Params, ';')) { + if (Param == "recover") + Result.Recover = true; + else + return make_error( + 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(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(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@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:50:06 +0800 Subject: [PATCH 17/62] [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@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:25:39 +0800 Subject: [PATCH 18/62] [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(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(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(I) || isa(I) || + isa(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@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:54:33 +0800 Subject: [PATCH 19/62] [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@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:17:45 +0800 Subject: [PATCH 20/62] [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>, 16> ToInstrument; + // Each MemRange entry is {I, Ptr, SizeVal, IsWrite} + struct MemRange { Instruction *I; Value *Ptr; Value *Size; bool IsWrite; }; + SmallVector 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(&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(&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@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:20:23 +0800 Subject: [PATCH 21/62] [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@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:38:51 +0800 Subject: [PATCH 22/62] [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@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:20:29 +0800 Subject: [PATCH 23/62] [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@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:22:07 +0800 Subject: [PATCH 24/62] [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@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:27:32 +0800 Subject: [PATCH 25/62] [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 + 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 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@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:30:26 +0800 Subject: [PATCH 26/62] [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 +#include +#include + +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 @@ +@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 +#include + +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 +#include +#include + +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 +#include + +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 +#include +#include + +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 + +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@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:42:44 +0800 Subject: [PATCH 27/62] [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@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:02:45 +0800 Subject: [PATCH 28/62] [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 +#include + +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 +#include + +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 +#include + +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@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:11:49 +0800 Subject: [PATCH 29/62] [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@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:13:25 +0800 Subject: [PATCH 30/62] [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@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:52:34 +0800 Subject: [PATCH 31/62] [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 ClSanitizeOnOptimizerEarlyEP( "sanitizer-early-opt-ep", cl::Optional, cl::desc("Insert sanitizers on OptimizerEarlyEP.")); +static cl::opt 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 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 { 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(ReportOobFn.getCallee())) + if (auto *F = dyn_cast(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(WarnOobFn.getCallee())) + if (auto *F = dyn_cast(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(I) || isa(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(I)) NumInstrumentedLoads++; + else if (isa(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>, 16> ToInstrument; - - // Each MemRange entry is {I, Ptr, SizeVal, IsWrite} - struct MemRange { Instruction *I; Value *Ptr; Value *Size; bool IsWrite; }; - SmallVector MemRanges; - - for (Instruction &I : instructions(F)) { - Value *Ptr = nullptr; - Type *AccessTy = nullptr; - - if (auto *LI = dyn_cast(&I)) { - if (!LI->isVolatile()) { - Ptr = LI->getPointerOperand(); - AccessTy = LI->getType(); - } - } else if (auto *SI = dyn_cast(&I)) { - if (!SI->isVolatile()) { - Ptr = SI->getPointerOperand(); - AccessTy = SI->getValueOperand()->getType(); - } - } else if (auto *AI = dyn_cast(&I)) { - if (!AI->isVolatile()) { - Ptr = AI->getPointerOperand(); - AccessTy = AI->getValOperand()->getType(); - } - } else if (auto *AI = dyn_cast(&I)) { - if (!AI->isVolatile()) { - Ptr = AI->getPointerOperand(); - AccessTy = AI->getCompareOperand()->getType(); - } - } else if (auto *MI = dyn_cast(&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(&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(I)) - ++NumInstrumentedLoads; - else if (isa(I)) - ++NumInstrumentedStores; - else - ++NumInstrumentedAtomics; + SmallVector ToInstrument; + + for (auto &BB : F) { + for (auto &I : BB) { + if (isa(&I) || isa(&I) || isa(&I) || + isa(&I)) + ToInstrument.push_back(&I); + else if (isa(&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(I)) + Modified |= instrumentMemoryAccess(I, LI->getPointerOperand(), LI->getType()); + else if (auto *SI = dyn_cast(I)) + Modified |= instrumentMemoryAccess(I, SI->getPointerOperand(), SI->getValueOperand()->getType()); + else if (auto *RMW = dyn_cast(I)) + Modified |= instrumentMemoryAccess(I, RMW->getPointerOperand(), RMW->getValOperand()->getType()); + else if (auto *CmpXchg = dyn_cast(I)) + Modified |= instrumentMemoryAccess(I, CmpXchg->getPointerOperand(), CmpXchg->getNewValOperand()->getType()); + else if (auto *MS = dyn_cast(I)) + Modified |= instrumentMemoryRange(I, MS->getDest(), MS->getLength(), true); + else if (auto *MT = dyn_cast(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 Loads; + for (BasicBlock &BB : F) + for (Instruction &I : BB) + if (auto *LI = dyn_cast(&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@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:53:57 +0800 Subject: [PATCH 32/62] [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 @@ -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@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:54:34 +0800 Subject: [PATCH 33/62] [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 +#include + +__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 +#include +#include + +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 +#include + +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(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 +#include + +// 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(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@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:58:43 +0800 Subject: [PATCH 34/62] [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@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:41:05 +0800 Subject: [PATCH 35/62] [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 +// +// 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 +#include +#include +#include +#include +#include + +// -------------------------------------------------------------------------- +// 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 \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 \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 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 Data, + Type *ElemTy) { + SmallVector 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 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 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 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 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 +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(I) || isa(I) || + isa(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(I) || isa(I) || - isa(I); Value *IsWriteVal = ConstantInt::get(I8Ty, IsWrite ? 1 : 0); OobIRB.CreateCall(OobFn, {PtrInt, Base, AllocSize, IsWriteVal}); +#endif - if (isa(I)) NumInstrumentedLoads++; - else if (isa(I)) NumInstrumentedStores++; - else NumInstrumentedAtomics++; + if (isa(I)) NumInstrumentedLoads++; + else if (isa(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 ToInstrument; @@ -211,6 +492,8 @@ bool LowFatSanitizer::instrumentFunction(Function &F) { ToInstrument.push_back(&I); else if (isa(&I)) ToInstrument.push_back(&I); + else if (isa(&I)) + ToInstrument.push_back(&I); } } @@ -228,7 +511,8 @@ bool LowFatSanitizer::instrumentFunction(Function &F) { else if (auto *MT = dyn_cast(I)) { Modified |= instrumentMemoryRange(I, MT->getDest(), MT->getLength(), true); Modified |= instrumentMemoryRange(I, MT->getSource(), MT->getLength(), false); - } + } else if (auto *GEP = dyn_cast(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@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:42:07 +0800 Subject: [PATCH 36/62] [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 + +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 + +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 + +// 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@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:48:25 +0800 Subject: [PATCH 37/62] [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@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:56:00 +0800 Subject: [PATCH 38/62] [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@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:58:43 +0800 Subject: [PATCH 39/62] [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 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 #include @@ -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(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 #include -// 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@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:11:51 +0800 Subject: [PATCH 40/62] [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 @@ -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 #include @@ -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 @@ -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 #include 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 #include #include 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 #include @@ -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 #include @@ -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 #include @@ -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 #include @@ -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 #include @@ -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 #include @@ -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 #include @@ -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 #include 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 #include -// 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 -// 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 @@ -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@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:18:39 +0800 Subject: [PATCH 41/62] [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@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:25:53 +0800 Subject: [PATCH 42/62] [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@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:36:45 +0800 Subject: [PATCH 43/62] [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@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:11:39 +0800 Subject: [PATCH 44/62] [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@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:13:19 +0800 Subject: [PATCH 45/62] [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 +#include +#include +#include + +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 +#include +#include + +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 +#include +#include + +// 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 +#include + +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 +#include +#include + +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@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:50:59 +0800 Subject: [PATCH 46/62] [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(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(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(I)) NumInstrumentedLoads++; else if (isa(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@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:49:55 +0800 Subject: [PATCH 47/62] [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@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:38:22 +0800 Subject: [PATCH 48/62] [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(I) || isa(I) || isa(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@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:17:27 +0800 Subject: [PATCH 49/62] [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@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:06:40 +0800 Subject: [PATCH 50/62] [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@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:44:06 +0800 Subject: [PATCH 51/62] [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 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@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:44:51 +0800 Subject: [PATCH 52/62] [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 +#include + +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 +#include + +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 +#include + +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 +#include + +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@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:31:08 +0800 Subject: [PATCH 53/62] [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(I) || isa(I) || isa(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@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:02:43 +0800 Subject: [PATCH 54/62] [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(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@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:07:46 +0800 Subject: [PATCH 55/62] [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 From 7aa7e883f39ef6b4bc330fe3be932a8c624a6b25 Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:13:12 +0800 Subject: [PATCH 56/62] [compiler-rt][FlexFat] Preserve malloc alignment in right-align mode --- compiler-rt/lib/lowfat/lf_interceptors.cpp | 4 +-- compiler-rt/lib/lowfat/lf_interface.h | 12 +++++---- compiler-rt/lib/lowfat/lf_rtl.cpp | 27 ++++++++++++------- .../TestCases/right_align/alignment.cpp | 27 +++++++++++++++++++ .../TestCases/right_align/free_list_reuse.cpp | 26 +++++++++--------- .../lowfat/TestCases/right_align/inbounds.cpp | 14 +++++----- .../right_align/left_padding_blind_spot.cpp | 22 +++++++-------- .../TestCases/right_align/overflow_caught.cpp | 19 +++++++------ compiler-rt/test/lowfat/lit.cfg.py | 4 +-- 9 files changed, 97 insertions(+), 58 deletions(-) create mode 100644 compiler-rt/test/lowfat/TestCases/right_align/alignment.cpp diff --git a/compiler-rt/lib/lowfat/lf_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp index 0a05d8eb1abed..2df00fbbed31d 100644 --- a/compiler-rt/lib/lowfat/lf_interceptors.cpp +++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp @@ -110,8 +110,8 @@ INTERCEPTOR(void *, realloc, void *ptr, uptr size) { if (old_class_size < copy_size) copy_size = old_class_size; } - // In right-align mode the returned pointer is offset within its slot: - // ptr = slot_base + (class_size - requested_size) + // In right-align mode the returned pointer may be shifted within its slot + // to the highest malloc-aligned address that still fits the object. // 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 diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h index 62a1199bdd6fa..d951d8a4521c0 100644 --- a/compiler-rt/lib/lowfat/lf_interface.h +++ b/compiler-rt/lib/lowfat/lf_interface.h @@ -35,11 +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. +// Called from a compiler-generated module constructor when +// -lowfat-mode=right-align is active. Instructs the allocator to bias objects +// toward the high end of their size-class slot while preserving the default +// malloc alignment. This can improve detection of some small rightward +// overflows, but the alignment constraint means the object will not always end +// exactly at the slot boundary. The trade-off is a possible blind spot on the +// left side when the shifted pointer still remains within the same slot. SANITIZER_INTERFACE_ATTRIBUTE void __lf_set_right_align(int right_align); SANITIZER_INTERFACE_ATTRIBUTE void __lf_report_oob(uptr ptr, uptr base, diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp index cec4a6b60d21f..d1536a5193911 100644 --- a/compiler-rt/lib/lowfat/lf_rtl.cpp +++ b/compiler-rt/lib/lowfat/lf_rtl.cpp @@ -23,6 +23,7 @@ #include "sanitizer_common/sanitizer_flag_parser.h" #include "sanitizer_common/sanitizer_flags.h" #include "sanitizer_common/sanitizer_mutex.h" +#include using namespace __sanitizer; @@ -36,12 +37,15 @@ bool lowfat_inited = false; 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. +// to bias objects toward the high end of their size-class slot while still +// preserving the default malloc alignment guarantee. This can improve detection +// of some right-side overflows, but the object's right edge does not always +// coincide exactly with the slot boundary once alignment is enforced. bool lowfat_right_align = false; +// malloc() must return a pointer suitably aligned for any object type. +static constexpr uptr kMallocAlignment = alignof(max_align_t); + // 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 @@ -192,8 +196,8 @@ static bool InitMemoryRegions() { // 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 +// In right-align mode, returns the highest malloc-aligned address within the +// slot that still leaves room for the requested object. 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. @@ -236,10 +240,13 @@ void *Allocate(uptr size) { slot_base = 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)); + // In right-align mode, preserve malloc alignment by rounding the available + // slack down to the nearest alignment boundary before shifting the pointer. + if (lowfat_right_align) { + uptr slack = alloc_size - size; + uptr aligned_offset = RoundDownTo(slack, kMallocAlignment); + return (void *)(slot_base + aligned_offset); + } return (void *)slot_base; } diff --git a/compiler-rt/test/lowfat/TestCases/right_align/alignment.cpp b/compiler-rt/test/lowfat/TestCases/right_align/alignment.cpp new file mode 100644 index 0000000000000..e8c1466605fd1 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/right_align/alignment.cpp @@ -0,0 +1,27 @@ +// 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 + +// Right-align mode must still satisfy the default malloc alignment guarantee. + +#include +#include +#include +#include + +int main() { + constexpr size_t kAlign = alignof(max_align_t); + + void *p17 = malloc(17); + void *p48 = malloc(48); + if (!p17 || !p48) return 1; + + if ((reinterpret_cast(p17) % kAlign) != 0) return 2; + if ((reinterpret_cast(p48) % kAlign) != 0) return 3; + + // CHECK: alignment: ok + printf("alignment: ok\n"); + + free(p48); + free(p17); + return 0; +} 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 index b5c506da2fb9f..6c41d599ec70d 100644 --- a/compiler-rt/test/lowfat/TestCases/right_align/free_list_reuse.cpp +++ b/compiler-rt/test/lowfat/TestCases/right_align/free_list_reuse.cpp @@ -3,29 +3,29 @@ // 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. +// Both 48 and 63 bytes land in the 64-byte class. When the 48-byte slot is +// freed, Deallocate must push the slot BASE (not the shifted pointer +// slot_base+16) onto the free list. The subsequent 63-byte allocation then +// reuses the same slot with a different aligned offset, and all 63 bytes must +// be accessible without OOB. #include #include int main() { - // First allocation: 17 bytes → offset 15 within 32-byte slot. - char *a = (char *)malloc(17); + // First allocation: 48 bytes -> offset 16 within a 64-byte slot. + char *a = (char *)malloc(48); if (!a) return 1; - for (int i = 0; i < 17; i++) a[i] = (char)i; + for (int i = 0; i < 48; 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); + // Second allocation: 63 bytes -> offset 0 within the same reused 64-byte slot. + char *b = (char *)malloc(63); 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++) + // Write and read back all 63 bytes — none must trigger OOB. + for (int i = 0; i < 63; i++) b[i] = (char)(i + 1); + for (int i = 0; i < 63; i++) if (b[i] != (char)(i + 1)) return 2; free(b); diff --git a/compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp b/compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp index 8a8877957aa93..ee551219d3543 100644 --- a/compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp +++ b/compiler-rt/test/lowfat/TestCases/right_align/inbounds.cpp @@ -4,24 +4,24 @@ // 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. +// A 48-byte request lands in the 64-byte class. In right-align mode on +// platforms with 16-byte malloc alignment, the object is placed at slot_base+16. +// Bytes buf[0]..buf[47] are all valid. #include #include int main() { - // 17 bytes → 32-byte class; object at slot_base+15 in right-align mode. - char *p = (char *)malloc(17); + // 48 bytes -> 64-byte class; object shifted by an aligned 16-byte offset. + char *p = (char *)malloc(48); if (!p) return 1; // Write every byte of the requested allocation. - for (int i = 0; i < 17; i++) + for (int i = 0; i < 48; i++) p[i] = (char)i; // Read back and verify. - for (int i = 0; i < 17; i++) + for (int i = 0; i < 48; i++) if (p[i] != (char)i) return 2; // CHECK: inbounds: ok 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 index a3cfb650230cd..f45147c88b143 100644 --- 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 @@ -2,27 +2,27 @@ // 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. +// same 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] +// 48-byte object in a 64-byte slot with 16-byte malloc alignment: +// left padding: [slot_base, slot_base+16) <- blind spot +// live object: [slot_base+16, slot_base+64) <- buf[0]..buf[47] // -// 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 +// buf[-1] = slot_base+15, which is inside the slot: +// GetBase(slot_base+15) = slot_base +// (slot_base+15 - slot_base) = 15 < 64 -> NOT OOB #include #include int main() { - // 17 bytes → 32-byte class; object at slot_base+15. - char *buf = (char *)malloc(17); + // 48 bytes -> 64-byte class; object at slot_base+16. + char *buf = (char *)malloc(48); 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. + // This is technically out-of-bounds for the 48-byte allocation, but + // right-align mode cannot detect it because the access stays within the same slot. buf[-1] = 'X'; // CHECK: blind spot: not caught (left padding) diff --git a/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp b/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp index 4abfa72700a12..f70365d2d1271 100644 --- a/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp +++ b/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp @@ -1,23 +1,26 @@ // 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. +// Mode-difference test: one-past-end overflow on an allocation where aligned +// right-biasing still leaves a non-zero shift within the slot. // -// 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. +// Default (left-align): a 48-byte object lives at the start of a 64-byte slot; +// buf[48] falls in the 16-byte right padding -> access is within the 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. +// Right-align: the same 48-byte object is shifted to slot_base+16 to preserve +// malloc alignment; buf[48] = slot_base+64, which is exactly the slot boundary +// -> OOB -> caught. #include #include int main() { - // 17 bytes → 32-byte class. - char *buf = (char *)malloc(17); + // 48 bytes -> 64-byte class. + char *buf = (char *)malloc(48); if (!buf) return 1; - buf[17] = 'X'; // one-past-end write + buf[48] = 'X'; // one-past-end write // CHECK-MISS: overflow: not caught (in right padding) // CHECK-CATCH: LOWFAT ERROR: out-of-bounds error detected! diff --git a/compiler-rt/test/lowfat/lit.cfg.py b/compiler-rt/test/lowfat/lit.cfg.py index 29bbd5ce2d409..b963407c6d12f 100644 --- a/compiler-rt/test/lowfat/lit.cfg.py +++ b/compiler-rt/test/lowfat/lit.cfg.py @@ -44,8 +44,8 @@ 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. +# right-align mode: allocations are biased toward the high end of the slot +# while preserving the platform's default malloc alignment. lowfat_right_align = lowfat_base + ["-mllvm", "-lowfat-mode=right-align"] config.substitutions.append(("%clangxx_lowfat ", build_invocation(lowfat_base))) From 808c41b73e9a468377cf137e1e555562c4392220 Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:13:59 +0800 Subject: [PATCH 57/62] [compiler-rt][FlexFat] Add right-align realloc regression test --- .../right_align/realloc_preserves_data.cpp | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 compiler-rt/test/lowfat/TestCases/right_align/realloc_preserves_data.cpp diff --git a/compiler-rt/test/lowfat/TestCases/right_align/realloc_preserves_data.cpp b/compiler-rt/test/lowfat/TestCases/right_align/realloc_preserves_data.cpp new file mode 100644 index 0000000000000..121c8d9cba1fa --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/right_align/realloc_preserves_data.cpp @@ -0,0 +1,40 @@ +// 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 + +// Regression test for right-align mode realloc semantics. +// +// A 48-byte allocation lands in a 64-byte slot and, with 16-byte malloc +// alignment, is returned at slot_base+16. realloc() must preserve the 48 bytes +// of user data starting at the returned pointer, not copy from slot_base. + +#include +#include + +int main() { + constexpr int OldSize = 48; + constexpr int NewSize = 64; + + unsigned char *p = (unsigned char *)malloc(OldSize); + if (!p) return 1; + + for (int i = 0; i < OldSize; ++i) + p[i] = (unsigned char)(0xA0 + i); + + unsigned char *q = (unsigned char *)realloc(p, NewSize); + if (!q) return 2; + + for (int i = 0; i < OldSize; ++i) { + if (q[i] != (unsigned char)(0xA0 + i)) { + printf("realloc mismatch at %d: got 0x%02x expected 0x%02x\n", + i, (unsigned)q[i], (unsigned)(0xA0 + i)); + free(q); + return 3; + } + } + + // CHECK: realloc_preserves_data: ok + // CHECK-NOT: realloc mismatch + printf("realloc_preserves_data: ok\n"); + free(q); + return 0; +} From 164010556becc580a91de325d79bbd8140f59da0 Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:30:23 +0800 Subject: [PATCH 58/62] [compiler-rt][FlexFat] Preserve data on right align realloc --- compiler-rt/lib/lowfat/lf_interceptors.cpp | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/compiler-rt/lib/lowfat/lf_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp index 2df00fbbed31d..142d41ef74a9a 100644 --- a/compiler-rt/lib/lowfat/lf_interceptors.cpp +++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp @@ -102,24 +102,20 @@ INTERCEPTOR(void *, realloc, void *ptr, uptr 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. + // Copy old data. For LowFat pointers, cap the copy to the bytes reachable + // from the returned pointer to the end of the slot. In right-align mode + // the user pointer may be shifted within the slot, so copying from the + // slot base would corrupt the preserved contents. uptr copy_size = size; if (old_is_lowfat) { uptr old_class_size = __lowfat::GetSize((uptr)ptr); - if (old_class_size < copy_size) - copy_size = old_class_size; + uptr old_base = __lowfat::GetBase((uptr)ptr); + uptr old_offset = (uptr)ptr - old_base; + uptr old_usable = old_class_size - old_offset; + if (old_usable < copy_size) + copy_size = old_usable; } - // In right-align mode the returned pointer may be shifted within its slot - // to the highest malloc-aligned address that still fits the object. - // 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); + internal_memcpy(new_ptr, ptr, copy_size); // Free old if (old_is_lowfat) __lowfat::Deallocate(ptr); From e86bcbfb5be96b2f1c8a3b44b97bbe7344549be0 Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:37:24 +0800 Subject: [PATCH 59/62] [compiler-rt][FlexFat] Fix right-align tests for custom size classes --- .../right_align/left_padding_blind_spot.cpp | 12 ++++++------ .../TestCases/right_align/overflow_caught.cpp | 17 +++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) 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 index f45147c88b143..537d44fb53f65 100644 --- 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 @@ -4,24 +4,24 @@ // padding are not caught because the shifted pointer still falls within the // same slot. // -// 48-byte object in a 64-byte slot with 16-byte malloc alignment: +// 112-byte object in a 128-byte slot with 16-byte malloc alignment: // left padding: [slot_base, slot_base+16) <- blind spot -// live object: [slot_base+16, slot_base+64) <- buf[0]..buf[47] +// live object: [slot_base+16, slot_base+128) <- buf[0]..buf[111] // // buf[-1] = slot_base+15, which is inside the slot: // GetBase(slot_base+15) = slot_base -// (slot_base+15 - slot_base) = 15 < 64 -> NOT OOB +// (slot_base+15 - slot_base) = 15 < 128 -> NOT OOB #include #include int main() { - // 48 bytes -> 64-byte class; object at slot_base+16. - char *buf = (char *)malloc(48); + // 112 bytes -> 128-byte class in both POW2 and custom-config mode. + char *buf = (char *)malloc(112); if (!buf) return 1; // Write one byte into the left padding (blind spot). - // This is technically out-of-bounds for the 48-byte allocation, but + // This is technically out-of-bounds for the 112-byte allocation, but // right-align mode cannot detect it because the access stays within the same slot. buf[-1] = 'X'; diff --git a/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp b/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp index f70365d2d1271..aad680ac00ca8 100644 --- a/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp +++ b/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp @@ -4,23 +4,24 @@ // Mode-difference test: one-past-end overflow on an allocation where aligned // right-biasing still leaves a non-zero shift within the slot. // -// Default (left-align): a 48-byte object lives at the start of a 64-byte slot; -// buf[48] falls in the 16-byte right padding -> access is within the slot -> +// Default (left-align): a 112-byte object lives at the start of a 128-byte +// slot; buf[112] falls in the 16-byte right padding -> access is within the +// slot -> // NOT caught. // -// Right-align: the same 48-byte object is shifted to slot_base+16 to preserve -// malloc alignment; buf[48] = slot_base+64, which is exactly the slot boundary -// -> OOB -> caught. +// Right-align: the same 112-byte object is shifted to slot_base+16 to preserve +// malloc alignment; buf[112] = slot_base+128, which is exactly the slot +// boundary -> OOB -> caught. #include #include int main() { - // 48 bytes -> 64-byte class. - char *buf = (char *)malloc(48); + // 112 bytes -> 128-byte class in both POW2 and custom-config mode. + char *buf = (char *)malloc(112); if (!buf) return 1; - buf[48] = 'X'; // one-past-end write + buf[112] = 'X'; // one-past-end write // CHECK-MISS: overflow: not caught (in right padding) // CHECK-CATCH: LOWFAT ERROR: out-of-bounds error detected! From 695b61203fd35674fbc51c6276eb1b6d2b11096b Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:42:48 +0800 Subject: [PATCH 60/62] [compiler-rt][FlexFat] Reorganize lowfat tests by category --- .../test/lowfat/TestCases/{ => core}/api_reconstruction.cpp | 0 compiler-rt/test/lowfat/TestCases/{ => core}/layout_heap.cpp | 0 .../test/lowfat/TestCases/{ => security}/security_core_gep.cpp | 0 .../test/lowfat/TestCases/{ => security}/security_core_oob.cpp | 0 .../lowfat/TestCases/{ => security}/security_core_padding.cpp | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename compiler-rt/test/lowfat/TestCases/{ => core}/api_reconstruction.cpp (100%) rename compiler-rt/test/lowfat/TestCases/{ => core}/layout_heap.cpp (100%) rename compiler-rt/test/lowfat/TestCases/{ => security}/security_core_gep.cpp (100%) rename compiler-rt/test/lowfat/TestCases/{ => security}/security_core_oob.cpp (100%) rename compiler-rt/test/lowfat/TestCases/{ => security}/security_core_padding.cpp (100%) diff --git a/compiler-rt/test/lowfat/TestCases/api_reconstruction.cpp b/compiler-rt/test/lowfat/TestCases/core/api_reconstruction.cpp similarity index 100% rename from compiler-rt/test/lowfat/TestCases/api_reconstruction.cpp rename to compiler-rt/test/lowfat/TestCases/core/api_reconstruction.cpp diff --git a/compiler-rt/test/lowfat/TestCases/layout_heap.cpp b/compiler-rt/test/lowfat/TestCases/core/layout_heap.cpp similarity index 100% rename from compiler-rt/test/lowfat/TestCases/layout_heap.cpp rename to compiler-rt/test/lowfat/TestCases/core/layout_heap.cpp diff --git a/compiler-rt/test/lowfat/TestCases/security_core_gep.cpp b/compiler-rt/test/lowfat/TestCases/security/security_core_gep.cpp similarity index 100% rename from compiler-rt/test/lowfat/TestCases/security_core_gep.cpp rename to compiler-rt/test/lowfat/TestCases/security/security_core_gep.cpp diff --git a/compiler-rt/test/lowfat/TestCases/security_core_oob.cpp b/compiler-rt/test/lowfat/TestCases/security/security_core_oob.cpp similarity index 100% rename from compiler-rt/test/lowfat/TestCases/security_core_oob.cpp rename to compiler-rt/test/lowfat/TestCases/security/security_core_oob.cpp diff --git a/compiler-rt/test/lowfat/TestCases/security_core_padding.cpp b/compiler-rt/test/lowfat/TestCases/security/security_core_padding.cpp similarity index 100% rename from compiler-rt/test/lowfat/TestCases/security_core_padding.cpp rename to compiler-rt/test/lowfat/TestCases/security/security_core_padding.cpp From 78b89886a782304bb732c658b23a029f5dce7b27 Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:45:43 +0800 Subject: [PATCH 61/62] [compiler-rt][FlexFat] Fix right-align realloc migration copy size --- compiler-rt/lib/lowfat/lf_interceptors.cpp | 7 ++- ...o_system_does_not_copy_neighbor_prefix.cpp | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 compiler-rt/test/lowfat/TestCases/right_align/realloc_to_system_does_not_copy_neighbor_prefix.cpp diff --git a/compiler-rt/lib/lowfat/lf_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp index 142d41ef74a9a..1b4f579d12f48 100644 --- a/compiler-rt/lib/lowfat/lf_interceptors.cpp +++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp @@ -130,8 +130,11 @@ INTERCEPTOR(void *, realloc, void *ptr, uptr size) { 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; + uptr old_class_size = __lowfat::GetSize((uptr)ptr); + uptr old_base = __lowfat::GetBase((uptr)ptr); + uptr old_offset = (uptr)ptr - old_base; + uptr old_usable = old_class_size - old_offset; + uptr copy_size = old_usable < size ? old_usable : size; internal_memcpy(new_ptr, ptr, copy_size); __lowfat::Deallocate(ptr); return new_ptr; diff --git a/compiler-rt/test/lowfat/TestCases/right_align/realloc_to_system_does_not_copy_neighbor_prefix.cpp b/compiler-rt/test/lowfat/TestCases/right_align/realloc_to_system_does_not_copy_neighbor_prefix.cpp new file mode 100644 index 0000000000000..6fcac6d5789f2 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/right_align/realloc_to_system_does_not_copy_neighbor_prefix.cpp @@ -0,0 +1,59 @@ +// RUN: %clangxx_lowfat_right_align -O0 %s -o %t && %run %t 2>&1 | FileCheck %s + +#include +#include +#include +#include +#include +#include + +extern "C" std::uintptr_t __lf_get_base(std::uintptr_t ptr); + +int main() { + // 112 bytes maps to the 128-byte class. In right-align mode the returned + // pointer is shifted 16 bytes into the slot, so copying a full 128 bytes + // from that pointer incorrectly reads the first 16 bytes of the next slot. + char *p = static_cast(malloc(112)); + char *neighbor = static_cast(malloc(112)); + if (!p || !neighbor) + return 1; + + uintptr_t delta = static_cast(neighbor - p); + assert(delta == 128 && "expected adjacent 128-byte-class slots"); + + memset(p, 'A', 112); + memset(neighbor, 0, 112); + char *neighbor_slot_base = + reinterpret_cast(__lf_get_base(reinterpret_cast(neighbor))); + memset(neighbor_slot_base, 0x5A, 16); + + // Force the LowFat -> system-malloc migration path in realloc(). + constexpr size_t HugeSize = (1ULL << 30) + 1; + char *q = static_cast(realloc(p, HugeSize)); + if (!q) { + // Extremely unlikely on the supported 64-bit targets because the system + // allocator usually overcommits here, but avoid a spurious hard failure. + free(neighbor); + puts("SKIP"); + return 0; + } + + bool copied_neighbor_prefix = true; + for (size_t i = 112; i < 128; ++i) { + if (static_cast(q[i]) != 0x5A) { + copied_neighbor_prefix = false; + break; + } + } + + // CHECK: OK: neighbor prefix not copied + // CHECK-NOT: BUG: copied neighbor prefix + if (copied_neighbor_prefix) + puts("BUG: copied neighbor prefix"); + else + puts("OK: neighbor prefix not copied"); + + free(neighbor); + free(q); + return 0; +} From 5c83ed0615f3eb4b5ee14f81311ee9d008643c56 Mon Sep 17 00:00:00 2001 From: duckyfuz <108561447+duckyfuz@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:18:50 +0800 Subject: [PATCH 62/62] [compiler-rt][LowFat] Split pow2 and custom base-recovery paths --- compiler-rt/lib/lowfat/lf_config.h | 33 ++++----- compiler-rt/lib/lowfat/lf_rtl.cpp | 38 ++++------ compiler-rt/lib/lowfat/tools/lf_config_gen.c | 57 ++++---------- compiler-rt/lib/lowfat/tools/sizes.cfg | 4 +- .../Instrumentation/LowFatSanitizer.cpp | 74 ++----------------- 5 files changed, 51 insertions(+), 155 deletions(-) diff --git a/compiler-rt/lib/lowfat/lf_config.h b/compiler-rt/lib/lowfat/lf_config.h index 244cd3d45f1ec..8d9b18481469a 100644 --- a/compiler-rt/lib/lowfat/lf_config.h +++ b/compiler-rt/lib/lowfat/lf_config.h @@ -11,8 +11,8 @@ // 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 (POW2) -// or via fixed-point magic-number math (non-POW2) +// - Given a pointer, the base can be computed either by masking off low bits +// (POW2-only mode) or via fixed-point magic-number math (custom mode) // - The size can be looked up from a table using the region index // // Default Memory Layout (POW2-only mode, kRegionSizeLog=32): @@ -23,11 +23,11 @@ // 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. +// Arbitrary configured 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. +// Base recovery uses generated reciprocal tables for every size class. // //===----------------------------------------------------------------------===// @@ -63,7 +63,7 @@ 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. +// SizeClassToSize: direct table lookup — works for arbitrary configured sizes. inline uptr SizeClassToSize(uptr class_index) { if (class_index >= kNumSizeClasses) return 0; @@ -149,25 +149,19 @@ inline uptr GetSize(uptr ptr) { #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. +// GetBase override for custom config: use reciprocal fixed-point +// multiplication for every configured size class, including power-of-two +// classes. This keeps base recovery uniform across custom layouts. // -// For POW2 sizes: base = ptr & ~(size - 1) [fast path] -// For non-POW2: base = ((u128)ptr * magic >> 64) * size [magic path] +// base = ((u128)ptr * magic >> 64) * size 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]; - } + 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. @@ -217,7 +211,6 @@ inline bool CheckBounds(uptr ptr, uptr access_size) { 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 diff --git a/compiler-rt/lib/lowfat/lf_rtl.cpp b/compiler-rt/lib/lowfat/lf_rtl.cpp index d1536a5193911..f047a12f92b04 100644 --- a/compiler-rt/lib/lowfat/lf_rtl.cpp +++ b/compiler-rt/lib/lowfat/lf_rtl.cpp @@ -77,14 +77,14 @@ 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. +// Fixed address where the metadata tables are mapped during initialization. +// Pow2 mode uses the size and mask tables. Custom mode uses the size and magic +// tables. This allows the LLVM pass to use absolute addressing (imm[index*8]) +// instead of PC-relative loads. // // 0x118000000000: Sizes (8 bytes per class) -// 0x118001000000: Magics (8 bytes per class) -// 0x118002000000: IsPow2 (1 byte per class) -// 0x118003000000: Masks (8 bytes per class) +// 0x118001000000: Magics (8 bytes per class, custom mode) +// 0x118003000000: Masks (8 bytes per class, pow2 mode) static constexpr uptr kTablesBase = 0x118000000000ULL; static constexpr uptr kTablesOffset = 0x1000000ULL; // 16 MB between tables @@ -109,16 +109,16 @@ static void InitializeFlags() { } 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. + // Reserve enough address space for the fixed table offsets used by the pass. if (!MmapFixedNoReserve(kTablesBase, 64 * 1024 * 1024, "lowfat_tables")) Die(); u64 *sizes = (u64 *)(kTablesBase + 0 * kTablesOffset); +#ifdef LOWFAT_CUSTOM_CONFIG u64 *magics = (u64 *)(kTablesBase + 1 * kTablesOffset); - u8 *ispow2 = (u8 *)(kTablesBase + 2 * kTablesOffset); +#else u64 *masks = (u64 *)(kTablesBase + 3 * kTablesOffset); +#endif // 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 @@ -129,20 +129,18 @@ static void InitTables() { #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; +#ifdef LOWFAT_CUSTOM_CONFIG magics[i] = 0; - ispow2[i] = 0; +#else masks[i] = 0; +#endif } } } @@ -152,13 +150,6 @@ static void InitRegionTable() { uptr size = SizeClassToSize(i); 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; } } @@ -199,7 +190,8 @@ static bool InitMemoryRegions() { // In right-align mode, returns the highest malloc-aligned address within the // slot that still leaves room for the requested object. 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 +// slot_base via reciprocal multiplication 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 diff --git a/compiler-rt/lib/lowfat/tools/lf_config_gen.c b/compiler-rt/lib/lowfat/tools/lf_config_gen.c index 036d53c60530d..20b86a897bfed 100644 --- a/compiler-rt/lib/lowfat/tools/lf_config_gen.c +++ b/compiler-rt/lib/lowfat/tools/lf_config_gen.c @@ -10,9 +10,7 @@ // 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) +// - kLowFatGenMagics[] : precomputed ceil(2^64/S) reciprocals for all sizes // - lowfat_size_to_class(): binary-search mapping from alloc size → region index // // sizes.cfg format: @@ -62,8 +60,9 @@ 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). +// This is the magic number M used by custom-config base recovery: +// floor(P / S) ~= (P * M) >> 64 +// The precision checker below verifies the usable range inside a region. static uint64_t compute_magic(uint64_t S) { if (S == 0) return 0; u128 two64 = (u128)1 << 64; @@ -195,25 +194,19 @@ int main(int argc, char *argv[]) { // ---- 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; - uint64_t M = compute_magic(S); uint64_t err = precision_error(S, M); magics[i] = M; if (pow2) { - masks[i] = ~(S - 1); effective_sizes[i] = S; // no precision error for POW2 } else { - 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; @@ -276,39 +269,16 @@ int main(int argc, char *argv[]) { // 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" + "// Magic numbers for all sizes: M = ceil(2^64 / S).\n" + "// Custom-config base recovery uses this reciprocal-multiply path for\n" + "// both power-of-two and non-power-of-two classes.\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]); + sizes[i], is_pow2(sizes[i]) ? " (POW2)" : ""); } fprintf(out, "};\n\n"); @@ -342,16 +312,15 @@ int main(int argc, char *argv[]) { // 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(" %-5s %-10s %-6s %-20s %-20s\n", + "Idx", "ReqSize", "POW2?", "EffectiveSize", "Magic"); printf(" %s\n", "---------------------------------------------------------------------"); for (int i = 0; i < num_sizes; i++) { - printf(" %-5d %-10" PRIu64 " %-6s %-20" PRIu64 " %#-20" PRIx64 " %#-10" PRIx64 "\n", + printf(" %-5d %-10" PRIu64 " %-6s %-20" PRIu64 " %#-20" PRIx64 "\n", i, sizes[i], - is_pow2_arr[i] ? "yes" : "no", + is_pow2(sizes[i]) ? "yes" : "no", effective_sizes[i], - magics[i], - masks[i]); + magics[i]); } return 0; diff --git a/compiler-rt/lib/lowfat/tools/sizes.cfg b/compiler-rt/lib/lowfat/tools/sizes.cfg index 19036ab03aa68..e7e753ff84397 100644 --- a/compiler-rt/lib/lowfat/tools/sizes.cfg +++ b/compiler-rt/lib/lowfat/tools/sizes.cfg @@ -6,8 +6,8 @@ # - 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. +# Custom-config mode uses the generated reciprocal-multiply path for all +# classes. Non-POW2 sizes are the main reason this configuration exists. # # This default config demonstrates a mix of POW2 and non-POW2 sizes. # Edit to your application's allocation profile for best results. diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp index f4d287dd2a970..3e6138160121e 100644 --- a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp +++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp @@ -95,13 +95,9 @@ class LowFatSanitizer { // 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 // Helper: GEP + load from a fixed absolute base at runtime index. @@ -173,9 +169,10 @@ FunctionCallee LowFatSanitizer::getWarnOobFn() { // 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. +// We mirror the generated size and reciprocal tables 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. @@ -214,27 +211,6 @@ GlobalVariable *LowFatSanitizer::getMagicsTable() { return MagicsTableGV; } -GlobalVariable *LowFatSanitizer::getIsPow2Table() { - if (!IsPow2TableGV) { - SmallVector 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 D(kLowFatGenMasks, - kLowFatGenMasks + LOWFAT_NUM_SIZE_CLASSES); - MasksTableGV = makeConstantArray(M, "__lf_gen_masks", D, - Type::getInt64Ty(M.getContext())); - } - return MasksTableGV; -} - // --------------------------------------------------------------------------- // emitDynamicBaseMagic // @@ -245,23 +221,14 @@ GlobalVariable *LowFatSanitizer::getMasksTable() { // // %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) +// ; Reciprocal fixed-point base recovery for every class // %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 LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt, @@ -272,38 +239,13 @@ LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt, Value *AllocSize64 = loadFromFixedTable(IRB, TablesBase + 0 * kTablesOffset, I64Ty, RegionIndex); - Value *Mask64 = loadFromFixedTable(IRB, TablesBase + 3 * kTablesOffset, - 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); - - // 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(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 --- + // In custom-config mode we deliberately use the reciprocal-multiply path + // for every class, including power-of-two sizes, so runtime and + // instrumentation recover bases the same way. Value *Magic64 = loadFromFixedTable(IRB, TablesBase + 1 * kTablesOffset, I64Ty, RegionIndex);