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/include/clang/Driver/SanitizerArgs.h b/clang/include/clang/Driver/SanitizerArgs.h index ed2eb6852b124..e3d088d0b2f0e 100644 --- a/clang/include/clang/Driver/SanitizerArgs.h +++ b/clang/include/clang/Driver/SanitizerArgs.h @@ -122,6 +122,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/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp index 94257fb96fc7f..54ea2751b853a 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" @@ -107,6 +108,17 @@ 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"), + 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. static cl::opt ClPGOColdFuncAttr( @@ -767,6 +779,12 @@ static void addSanitizers(const Triple &TargetTriple, MPM.addPass(DataFlowSanitizerPass(LangOpts.NoSanitizeFiles, PB.getVirtualFileSystemPtr())); } + 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( @@ -784,6 +802,23 @@ static void addSanitizers(const Triple &TargetTriple, // LastEP does not need GlobalsAA. PB.registerOptimizerLastEPCallback(SanitizersCallback); } + + if (LangOpts.Sanitize.has(SanitizerKind::LowFat)) { + LowFatSanitizerOptions LFOpts; + LFOpts.Recover = CodeGenOpts.SanitizeRecover.has(SanitizerKind::LowFat); + 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)); + }); + } + } } void addLowerAllowCheckPass(const CodeGenOptions &CodeGenOpts, diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp index 294c9ad2705dc..c299fbee29692 100644 --- a/clang/lib/Driver/SanitizerArgs.cpp +++ b/clang/lib/Driver/SanitizerArgs.cpp @@ -672,6 +672,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/CommonArgs.cpp b/clang/lib/Driver/ToolChains/CommonArgs.cpp index 8bb271d27a3c4..06bd7f71a5488 100644 --- a/clang/lib/Driver/ToolChains/CommonArgs.cpp +++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp @@ -1728,6 +1728,8 @@ collectSanitizerRuntimes(const ToolChain &TC, const ArgList &Args, if (SanArgs.linkCXXRuntimes()) StaticRuntimes.push_back("scudo_standalone_cxx"); } + if (SanArgs.needsLowFatRt()) + StaticRuntimes.push_back("lowfat"); if (SanArgs.needsUbsanLoopDetectRt()) NonWholeStaticRuntimes.push_back("ubsan_loop_detect"); } diff --git a/clang/lib/Driver/ToolChains/Darwin.cpp b/clang/lib/Driver/ToolChains/Darwin.cpp index 1c95a79a52a9c..c788106643edc 100644 --- a/clang/lib/Driver/ToolChains/Darwin.cpp +++ b/clang/lib/Driver/ToolChains/Darwin.cpp @@ -1670,6 +1670,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()) @@ -3917,6 +3920,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 a5277dcac1747..1886c3c970b3d 100644 --- a/clang/lib/Driver/ToolChains/Linux.cpp +++ b/clang/lib/Driver/ToolChains/Linux.cpp @@ -975,6 +975,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 c2de0d0f652e8..0015b0fffce81 100644 --- a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake +++ b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake @@ -122,6 +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}) +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.*") 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() diff --git a/compiler-rt/lib/lowfat/CMakeLists.txt b/compiler-rt/lib/lowfat/CMakeLists.txt new file mode 100644 index 0000000000000..e85139582d840 --- /dev/null +++ b/compiler-rt/lib/lowfat/CMakeLists.txt @@ -0,0 +1,113 @@ +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 + lf_stack.cpp +) + +set(LOWFAT_HEADERS + lf_allocator.h + lf_config.h + lf_interface.h + lf_stack.h +) + +set(LOWFAT_CFLAGS ${SANITIZER_COMMON_CFLAGS}) +append_rtti_flag(OFF LOWFAT_CFLAGS) + +# Static runtime library. +add_compiler_rt_component(lowfat) + +if(COMPILER_RT_HAS_LOWFAT) + 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 RTInterception + RTSanitizerCommon + RTSanitizerCommonLibc + RTSanitizerCommonSymbolizer + 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 + STATIC + ARCHS ${LOWFAT_SUPPORTED_ARCH} + SOURCES ${LOWFAT_SOURCES} + ADDITIONAL_HEADERS ${LOWFAT_HEADERS} + OBJECT_LIBS RTInterception + RTSanitizerCommon + RTSanitizerCommonLibc + RTSanitizerCommonSymbolizer + RTSanitizerCommonSymbolizerInternal + CFLAGS ${LOWFAT_CFLAGS} + DEFS ${LOWFAT_COMMON_DEFINITIONS} + DEPS ${LOWFAT_DEPS} + PARENT_TARGET lowfat) + endif() +endif() 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_config.h b/compiler-rt/lib/lowfat/lf_config.h new file mode 100644 index 0000000000000..8d9b18481469a --- /dev/null +++ b/compiler-rt/lib/lowfat/lf_config.h @@ -0,0 +1,222 @@ +//===-- 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 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): +// 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): +// 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. +// Base recovery uses generated reciprocal tables for every size class. +// +//===----------------------------------------------------------------------===// + +#ifndef LF_CONFIG_H +#define LF_CONFIG_H + +#include "sanitizer_common/sanitizer_internal_defs.h" + +#ifdef LOWFAT_CUSTOM_CONFIG +#include "lf_config_generated.h" +#endif + +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; + +#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 arbitrary configured sizes. +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; + +// 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); +} + +#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 +// 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 allocation size from a LowFat pointer +inline uptr GetSize(uptr ptr) { + uptr region = GetRegionIndex(ptr); + if (region >= kNumSizeClasses) + return (uptr)-1; // Wide-bounds for non-LowFat pointers + return SizeClassToSize(region); +} + +#ifdef LOWFAT_CUSTOM_CONFIG + +// 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. +// +// base = ((u128)ptr * magic >> 64) * size +inline uptr GetBase(uptr ptr) { + uptr region = GetRegionIndex(ptr); + if (region >= kNumSizeClasses) + return 0; + 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) { + 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; +} + +// 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; +} + +#endif // LOWFAT_CUSTOM_CONFIG + +//===----------------------------------------------------------------------===// +// Region Table (for lookup by region index) +//===----------------------------------------------------------------------===// + +struct RegionInfo { + uptr size; // Allocation size for this region + uptr alignment; // Alignment (same as size for LowFat) +}; + +// 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_interceptors.cpp b/compiler-rt/lib/lowfat/lf_interceptors.cpp new file mode 100644 index 0000000000000..1b4f579d12f48 --- /dev/null +++ b/compiler-rt/lib/lowfat/lf_interceptors.cpp @@ -0,0 +1,218 @@ +//===-- 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 "lf_interface.h" +#include "sanitizer_common/sanitizer_allocator_dlsym.h" + +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 +// (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, 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); + 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; + } + 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_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; + } + + 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); +} + +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); + 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); + } +} + +// 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)) + return DlsymAlloc::GetSize(ptr); + if (__lowfat::IsLowFatPointer((uptr)ptr)) + return __lowfat::GetSize((uptr)ptr); + return REAL(malloc_size)(ptr); +} +#endif + +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); + INTERCEPT_FUNCTION(memset); + INTERCEPT_FUNCTION(memcpy); + INTERCEPT_FUNCTION(memmove); +#if SANITIZER_APPLE + INTERCEPT_FUNCTION(malloc_size); +#endif + inited = 1; +} +} // namespace __lowfat diff --git a/compiler-rt/lib/lowfat/lf_interface.h b/compiler-rt/lib/lowfat/lf_interface.h new file mode 100644 index 0000000000000..d951d8a4521c0 --- /dev/null +++ b/compiler-rt/lib/lowfat/lf_interface.h @@ -0,0 +1,79 @@ +//===-- 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(); + +// 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 +// 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); + +// 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, + uptr bound, int is_write); + +// Warn about an out-of-bounds error without terminating. +// 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, 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. +SANITIZER_INTERFACE_ATTRIBUTE uptr __lf_get_base(uptr ptr); + +// Get the size (bound) of an allocation from a pointer. +// 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); + + +} // 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..f047a12f92b04 --- /dev/null +++ b/compiler-rt/lib/lowfat/lf_rtl.cpp @@ -0,0 +1,401 @@ +//===-- 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_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" +#include "sanitizer_common/sanitizer_flags.h" +#include "sanitizer_common/sanitizer_mutex.h" +#include + +using namespace __sanitizer; + +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; + +// Set to true when -lowfat-mode=right-align is active. Instructs Allocate() +// 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 +// 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 +RegionInfo kRegions[kMaxSizeClasses]; + +// Pointers to the start of each mapped region +static uptr region_bases[kMaxSizeClasses]; + +// Bump pointer: next fresh address to allocate from in each region +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[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[kMaxSizeClasses]; + +// 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, custom mode) +// 0x118003000000: Masks (8 bytes per class, pow2 mode) +static constexpr uptr kTablesBase = 0x118000000000ULL; +static constexpr uptr kTablesOffset = 0x1000000ULL; // 16 MB between tables + +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 InitTables() { + // 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); +#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 + // 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]; +#else + u64 size = (u64)SizeClassToSize(i); + sizes[i] = size; + masks[i] = ~(size - 1); +#endif + } else { + sizes[i] = 0; +#ifdef LOWFAT_CUSTOM_CONFIG + magics[i] = 0; +#else + masks[i] = 0; +#endif + } + } +} + +static void InitRegionTable() { + for (uptr i = 0; i < kNumSizeClasses; i++) { + uptr size = SizeClassToSize(i); + kRegions[i].size = size; + kRegions[i].alignment = size; + free_lists[i] = nullptr; + } +} + +// 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 + bool success = MmapFixedNoReserve(region_start, kRegionSize, "lowfat_region"); + + if (!success) + return false; + + region_bases[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; + } + + return true; +} + +// 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 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 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 +// freed blocks can be reused with a different offset for a new request size. +void *Allocate(uptr size) { + if (size == 0) + size = 1; + + uptr class_index = SizeClassIndex(size); + if (class_index >= kNumSizeClasses) + return nullptr; + + uptr alloc_size = SizeClassToSize(class_index); + + SpinMutexLock lock(®ion_locks[class_index]); + + 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; + slot_base = (uptr)block; + // Zero the entire slot (free list pointer was stored at slot_base) + internal_memset(block, 0, alloc_size); + } 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); + } + + region_next_alloc[class_index] = addr + alloc_size; + slot_base = addr; + } + + // 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; +} + +// 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; + + uptr addr = (uptr)ptr; + + // Validate this is a LowFat pointer + if (!IsLowFatPointer(addr)) { + InternalFree(ptr); + return; + } + + 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 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; +} + +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, 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, + const StackTrace &stack) { + PrintOobHeader("WARNING", ptr, base, bound, is_write); + stack.Print(); +} + +} // namespace __lowfat + +// ---------------------- Interface Functions ---------------------- + +extern "C" { + +SANITIZER_INTERFACE_ATTRIBUTE +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) + return; + + __lowfat::InitializeFlags(); + + __lowfat::InitTables(); + + __lowfat::InitRegionTable(); + + if (!__lowfat::InitMemoryRegions()) + Die(); + + __lowfat::lowfat_inited = true; + + __lowfat::InitializeInterceptors(); +} + +SANITIZER_INTERFACE_ATTRIBUTE +void __lf_report_oob(uptr ptr, uptr base, uptr bound, int 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) { + GET_STACK_TRACE_FATAL_HERE; + __lowfat::PrintWarning(ptr, base, bound, is_write, stack); +} + +SANITIZER_INTERFACE_ATTRIBUTE +uptr __lf_get_base(uptr ptr) { + return __lowfat::GetBase(ptr); +} + +SANITIZER_INTERFACE_ATTRIBUTE +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); +} + +SANITIZER_INTERFACE_ATTRIBUTE +void __lf_free(void *ptr) { + __lowfat::Deallocate(ptr); +} + +} // extern "C" + +#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 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 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..20b86a897bfed --- /dev/null +++ b/compiler-rt/lib/lowfat/tools/lf_config_gen.c @@ -0,0 +1,327 @@ +//===-- 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 ceil(2^64/S) reciprocals for all sizes +// - 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 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; + 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]; + 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); + + uint64_t M = compute_magic(S); + uint64_t err = precision_error(S, M); + magics[i] = M; + + if (pow2) { + effective_sizes[i] = S; // no precision error for POW2 + } else { + // 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 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(sizes[i]) ? " (POW2)" : ""); + } + 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\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 "\n", + i, sizes[i], + is_pow2(sizes[i]) ? "yes" : "no", + effective_sizes[i], + magics[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..e7e753ff84397 --- /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) +# +# 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. +# + +# 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/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 diff --git a/compiler-rt/test/lowfat/CMakeLists.txt b/compiler-rt/test/lowfat/CMakeLists.txt new file mode 100644 index 0000000000000..f1670497c1dca --- /dev/null +++ b/compiler-rt/test/lowfat/CMakeLists.txt @@ -0,0 +1,41 @@ +set(LOWFAT_LIT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + +set(LOWFAT_TESTSUITES) +set(LOWFAT_TEST_DEPS ${SANITIZER_COMMON_LIT_TEST_DEPS}) +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() + +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) + + # 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 + 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/TestCases/core/api_reconstruction.cpp b/compiler-rt/test/lowfat/TestCases/core/api_reconstruction.cpp new file mode 100644 index 0000000000000..5c50f9ec4cc37 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/core/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/core/layout_heap.cpp b/compiler-rt/test/lowfat/TestCases/core/layout_heap.cpp new file mode 100644 index 0000000000000..ddbbac03dca29 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/core/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/inbounds/basic_inbounds.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/basic_inbounds.cpp new file mode 100644 index 0000000000000..4d06c914f7996 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/inbounds/basic_inbounds.cpp @@ -0,0 +1,28 @@ +// 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 + +// Basic in-bounds allocation test. + +#include + +extern "C" void *__lf_malloc(unsigned long size); +extern "C" void __lf_free(void *ptr); + +int main() { + 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: basic_inbounds: ok + // CHECK-NOT: LOWFAT ERROR + printf("basic_inbounds: ok\n"); + return 0; +} diff --git a/compiler-rt/test/lowfat/TestCases/inbounds/free_list_reuse.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/free_list_reuse.cpp new file mode 100644 index 0000000000000..d1222b1e33845 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/inbounds/free_list_reuse.cpp @@ -0,0 +1,29 @@ +// 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 + +// Free-list reuse sanity test. + +#include + +extern "C" void *__lf_malloc(unsigned long size); +extern "C" void __lf_free(void *ptr); + +int main() { + // Allocate and free, then allocate again. + int *a = (int *)__lf_malloc(10 * sizeof(int)); + a[0] = 1; + __lf_free(a); + + int *b = (int *)__lf_malloc(10 * sizeof(int)); + b[0] = 2; + b[9] = 3; + __lf_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/inbounds/inbounds_no_report.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/inbounds_no_report.cpp new file mode 100644 index 0000000000000..b90741ee5e86f --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/inbounds/inbounds_no_report.cpp @@ -0,0 +1,35 @@ +// 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 + +// In-bounds accesses, memcpy, and memset should not report OOB. + +#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/TestCases/inbounds/malloc_intercepted_inbounds.cpp b/compiler-rt/test/lowfat/TestCases/inbounds/malloc_intercepted_inbounds.cpp new file mode 100644 index 0000000000000..6e929bac7c312 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/inbounds/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 + +// Basic in-bounds test for malloc-intercepted allocations. + +#include +#include +#include + +int main() { + // 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. + 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/TestCases/memintrinsics/memcpy_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_dynamic_fatal.cpp new file mode 100644 index 0000000000000..c3505265263a1 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_dynamic_fatal.cpp @@ -0,0 +1,30 @@ +// 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 + +// memcpy OOB write with non-constant size must be reported in fatal mode. + +#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/TestCases/memintrinsics/memcpy_oob_static_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_fatal.cpp new file mode 100644 index 0000000000000..5f914a93726a7 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_fatal.cpp @@ -0,0 +1,23 @@ +// 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 + +// memcpy OOB write must be reported 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; + + 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/TestCases/memintrinsics/memcpy_oob_static_recover.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_recover.cpp new file mode 100644 index 0000000000000..7b7bcdbf51ff8 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/memintrinsics/memcpy_oob_static_recover.cpp @@ -0,0 +1,29 @@ +// 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 + +// memcpy OOB write in recover mode must warn and continue. + +#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 should continue in recover mode. + // CHECK: after memcpy + printf("after memcpy\n"); + + free(guard); + free(dst); + return 0; +} diff --git a/compiler-rt/test/lowfat/TestCases/memintrinsics/memmove_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memmove_oob_dynamic_fatal.cpp new file mode 100644 index 0000000000000..995522deec07a --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/memintrinsics/memmove_oob_dynamic_fatal.cpp @@ -0,0 +1,28 @@ +// 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 + +// memmove OOB read with non-constant size must be reported. + +#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 (+1 OOB) + + // 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/TestCases/memintrinsics/memset_oob_dynamic_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_dynamic_fatal.cpp new file mode 100644 index 0000000000000..9f66e06b84b92 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_dynamic_fatal.cpp @@ -0,0 +1,29 @@ +// 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 + +// memset OOB write with non-constant size must be reported in fatal mode. + +#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/TestCases/memintrinsics/memset_oob_static_fatal.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_fatal.cpp new file mode 100644 index 0000000000000..28c0366e4c245 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_fatal.cpp @@ -0,0 +1,21 @@ +// 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 + +// memset OOB write must be reported 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/TestCases/memintrinsics/memset_oob_static_recover.cpp b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_recover.cpp new file mode 100644 index 0000000000000..ac31a352e6bf7 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/memintrinsics/memset_oob_static_recover.cpp @@ -0,0 +1,27 @@ +// 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 + +// memset OOB write in recover mode must warn and continue. + +#include +#include +#include + +int main() { + char *dst = (char *)malloc(16); + char *guard = (char *)malloc(16); + if (!dst || !guard) return 1; + + // 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); + + // CHECK: after memset + printf("after memset\n"); + + free(guard); + free(dst); + return 0; +} diff --git a/compiler-rt/test/lowfat/TestCases/mode/dead_load.cpp b/compiler-rt/test/lowfat/TestCases/mode/dead_load.cpp new file mode 100644 index 0000000000000..b1211008a8c83 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/mode/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 default-fast and +// safe mode when the loaded value is actually used (returned and printed). + +#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 is 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/TestCases/mode/mode_baseline_all_detect.cpp b/compiler-rt/test/lowfat/TestCases/mode/mode_baseline_all_detect.cpp new file mode 100644 index 0000000000000..f547789b7fe83 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/mode/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: 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 + +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/TestCases/mode/mode_diff_pure_call.cpp b/compiler-rt/test/lowfat/TestCases/mode/mode_diff_pure_call.cpp new file mode 100644 index 0000000000000..87b8972b033cc --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/mode/mode_diff_pure_call.cpp @@ -0,0 +1,31 @@ +// 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 + +// 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 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 + // 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 intentionally discarded. + free(p); + + // CHECK-MISS: DONE + // CHECK-CATCH: LOWFAT ERROR: out-of-bounds error detected! + printf("DONE\n"); + return 0; +} diff --git a/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_inbounds.cpp b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_inbounds.cpp new file mode 100644 index 0000000000000..cbb1f54ff5885 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_inbounds.cpp @@ -0,0 +1,18 @@ +// RUN: %clangxx_lowfat -O0 %s -o %t && %run %t + +// In-bounds write at the last byte of a 48-byte allocation should not report OOB. +// +// REQUIRES: lowfat-custom-config + +#include + +int main() { + char *p = (char *)malloc(48); + if (!p) return 1; + + // Write to the last byte. This must not report a LowFat error. + p[47] = 'x'; + + free(p); + return 0; +} diff --git a/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob.cpp b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob.cpp new file mode 100644 index 0000000000000..789e4a06acd70 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob.cpp @@ -0,0 +1,23 @@ +// 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 + +// OOB write past a 48-byte allocation must be reported. +// This exercises the non-pow2 magic-multiply path. +// +// 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 cross the 48-byte allocation boundary. + // 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/TestCases/non_pow2/non_pow2_oob_boundary.cpp b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob_boundary.cpp new file mode 100644 index 0000000000000..33c704b95cbc1 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/non_pow2/non_pow2_oob_boundary.cpp @@ -0,0 +1,26 @@ +// 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 + +// 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 + +// noinline keeps this as a cross-function pointer-escape case. +__attribute__((noinline)) +static void sink(volatile char *q) { *q = 'x'; } + +int main() { + char *p = (char *)malloc(48); + if (!p) return 1; + + // GEP check: result == End, so this is out of bounds. + // CHECK: LOWFAT ERROR: out-of-bounds error detected! + sink(p + 48); + + free(p); + return 0; +} + diff --git a/compiler-rt/test/lowfat/TestCases/oob/cross_boundary_oob.cpp b/compiler-rt/test/lowfat/TestCases/oob/cross_boundary_oob.cpp new file mode 100644 index 0000000000000..6aa172698858d --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/oob/cross_boundary_oob.cpp @@ -0,0 +1,28 @@ +// 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 + +// Cross-boundary OOB write must be reported. + +extern "C" void *__lf_malloc(unsigned long size); + +int main() { + // Allocate exactly 16 bytes. + 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: LOWFAT ERROR: out-of-bounds error detected! + double *cross = (double *)(buf + 12); + *cross = 3.14; + + return 0; +} diff --git a/compiler-rt/test/lowfat/TestCases/oob/oob_read_fatal.cpp b/compiler-rt/test/lowfat/TestCases/oob/oob_read_fatal.cpp new file mode 100644 index 0000000000000..055bc5422ffe4 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/oob/oob_read_fatal.cpp @@ -0,0 +1,26 @@ +// 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 + +// OOB scalar read across an allocation boundary must be reported 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! + 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; +} 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 new file mode 100644 index 0000000000000..6c41d599ec70d --- /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 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: 48 bytes -> offset 16 within a 64-byte slot. + char *a = (char *)malloc(48); + if (!a) return 1; + for (int i = 0; i < 48; i++) a[i] = (char)i; + free(a); + + // 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 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); + + // 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..ee551219d3543 --- /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 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() { + // 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 < 48; i++) + p[i] = (char)i; + + // Read back and verify. + for (int i = 0; i < 48; 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..537d44fb53f65 --- /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 slot. +// +// 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+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 < 128 -> NOT OOB + +#include +#include + +int main() { + // 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 112-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) + // 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..aad680ac00ca8 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/right_align/overflow_caught.cpp @@ -0,0 +1,31 @@ +// 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 an allocation where aligned +// right-biasing still leaves a non-zero shift 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 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() { + // 112 bytes -> 128-byte class in both POW2 and custom-config mode. + char *buf = (char *)malloc(112); + if (!buf) return 1; + + buf[112] = '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/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; +} 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; +} diff --git a/compiler-rt/test/lowfat/TestCases/security/security_core_gep.cpp b/compiler-rt/test/lowfat/TestCases/security/security_core_gep.cpp new file mode 100644 index 0000000000000..127f451eef829 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/security/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/security_core_oob.cpp b/compiler-rt/test/lowfat/TestCases/security/security_core_oob.cpp new file mode 100644 index 0000000000000..7495e713614d1 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/security/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/security_core_padding.cpp b/compiler-rt/test/lowfat/TestCases/security/security_core_padding.cpp new file mode 100644 index 0000000000000..3acf925f76316 --- /dev/null +++ b/compiler-rt/test/lowfat/TestCases/security/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; +} diff --git a/compiler-rt/test/lowfat/lit.cfg.py b/compiler-rt/test/lowfat/lit.cfg.py new file mode 100644 index 0000000000000..b963407c6d12f --- /dev/null +++ b/compiler-rt/test/lowfat/lit.cfg.py @@ -0,0 +1,81 @@ +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) + " " + + +# Base flags +lowfat_base = ["-fsanitize=lowfat"] + +# safe mode (fast mode is the default) +lowfat_safe = lowfat_base + ["-mllvm", "-lowfat-mode=safe"] + +# 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))) +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( + ( + "%clangxx_lowfat_recover ", + build_invocation(lowfat_base + ["-fsanitize-recover=lowfat"]), + ) +) +config.substitutions.append( + ( + "%clangxx_lowfat_safe_recover ", + build_invocation(lowfat_safe + ["-fsanitize-recover=lowfat"]), + ) +) + +# 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 + +# 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 new file mode 100644 index 0000000000000..c89b8d682c688 --- /dev/null +++ b/compiler-rt/test/lowfat/lit.site.cfg.py.in @@ -0,0 +1,15 @@ +@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@" +# 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") + +# 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/configure_llvm.sh b/configure_llvm.sh new file mode 100755 index 0000000000000..adf99aa539112 --- /dev/null +++ b/configure_llvm.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Common flags for all systems +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 + -DLLVM_TARGETS_TO_BUILD=Native + -DLLVM_OPTIMIZED_TABLEGEN=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 + -DLLVM_USE_SPLIT_DWARF=ON + ) +fi + +echo "[+] Generating CMake configuration..." +cmake "${CMAKE_ARGS[@]}" diff --git a/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h new file mode 100644 index 0000000000000..543077677527e --- /dev/null +++ b/llvm/include/llvm/Transforms/Instrumentation/LowFatSanitizer.h @@ -0,0 +1,44 @@ +//===- 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 { + bool Recover = false; + + enum class LowFatMode { + 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; + + bool InternalBarrierOnly_ = false; +}; + +class LowFatSanitizerPass : public PassInfoMixin { +public: + LLVM_ABI + LowFatSanitizerPass(const LowFatSanitizerOptions &Options); + LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM); + static bool isRequired() { return true; } + +private: + LowFatSanitizerOptions Options; +}; + +} // namespace llvm + +#endif // LLVM_TRANSFORMS_INSTRUMENTATION_LOWFATSANITIZER_H diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp index 45955426d66a0..ad5d78435617e 100644 --- a/llvm/lib/Passes/PassBuilder.cpp +++ b/llvm/lib/Passes/PassBuilder.cpp @@ -248,6 +248,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" @@ -979,6 +980,21 @@ Expected parseHWASanPassOptions(StringRef Params) { return Result; } +Expected parseLowFatPassOptions(StringRef Params) { + LowFatSanitizerOptions Result; + if (!Params.empty()) { + 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; +} + 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..023d00d410c92 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 @@ -44,3 +45,5 @@ add_llvm_component_library(LLVMInstrumentation TransformUtils ProfileData ) + +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() diff --git a/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp new file mode 100644 index 0000000000000..3e6138160121e --- /dev/null +++ b/llvm/lib/Transforms/Instrumentation/LowFatSanitizer.cpp @@ -0,0 +1,629 @@ +//===- 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 +// +//===----------------------------------------------------------------------===// +// +// 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/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/TargetParser/Triple.h" +#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" + +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 { + +/// 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())), + UseDarwinMetadataGuard(Triple(M.getTargetTriple()).isOSDarwin()), + TablesBase(kTablesBase) {} + + bool run(); + +private: + Module &M; + const LowFatSanitizerOptions &Options; + const DataLayout &DL; + Type *IntptrTy; + const bool UseDarwinMetadataGuard; + const uint64_t TablesBase; + + FunctionCallee ReportOobFn = nullptr; + FunctionCallee WarnOobFn = nullptr; + + FunctionCallee getReportOobFn(); + FunctionCallee getWarnOobFn(); + + bool instrumentFunction(Function &F); + bool instrumentMemoryAccess(Instruction *I, Value *Ptr, Type *AccessTy); + bool instrumentMemoryRange(Instruction *I, Value *Ptr, Value *Size, + bool IsWrite); + 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 *SizesTableGV = nullptr; + GlobalVariable *MagicsTableGV = 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; + 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; +#endif + + // Fixed absolute addresses for metadata tables (must match lf_rtl.cpp) + static constexpr uint64_t kTablesBase = 0x118000000000ULL; + static constexpr uint64_t kTablesOffset = 0x1000000ULL; +}; + +FunctionCallee LowFatSanitizer::getReportOobFn() { + if (!ReportOobFn) { + // 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, I8Ty}, false)); + 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; +} + +FunctionCallee LowFatSanitizer::getWarnOobFn() { + if (!WarnOobFn) { + // 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, I8Ty}, false)); + if (auto *F = dyn_cast(WarnOobFn.getCallee())) { + F->addFnAttr(Attribute::NoUnwind); + F->setMemoryEffects(MemoryEffects::inaccessibleMemOnly()); + } + } + return WarnOobFn; +} + +#ifdef LOWFAT_CUSTOM_CONFIG +// --------------------------------------------------------------------------- +// Custom-config pass helpers: table-accessor lazy initializers +// --------------------------------------------------------------------------- +// +// 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. + +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; +} + +// --------------------------------------------------------------------------- +// 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) +// +// ; 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 +// --------------------------------------------------------------------------- +std::pair +LowFatSanitizer::emitDynamicBaseMagic(IRBuilder<> &IRB, Value *PtrInt, + Value *RegionIndex) { + LLVMContext &Ctx = M.getContext(); + Type *I64Ty = Type::getInt64Ty(Ctx); + Type *I128Ty = Type::getInt128Ty(Ctx); + + Value *AllocSize64 = loadFromFixedTable(IRB, TablesBase + 0 * kTablesOffset, + I64Ty, RegionIndex); + + // Narrow to IntptrTy (which is i64 on 64-bit targets) + Value *AllocSize = IRB.CreateZExtOrTrunc(AllocSize64, IntptrTy); + + // 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); + + 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); + + return {AllocSize, BaseMul}; +} +#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 *IsOOB = nullptr; + if (!FixedAccessSize && !DynAccessSize) { + // 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 { + 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 = + 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); + 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); + + // 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 = nullptr; + Value *IsLowFat = nullptr; + if (UseDarwinMetadataGuard) { + Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses); + IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion); + } else { + AllocSize64 = loadFromFixedTable(IRB, TablesBase + 0 * kTablesOffset, + I64Ty, RegionIndex); + IsLowFat = IRB.CreateICmpNE(AllocSize64, ConstantInt::get(I64Ty, 0)); + } + + Instruction *ThenTerm = SplitBlockAndInsertIfThen(IsLowFat, I, false); + IRBuilder<> ThenIRB(ThenTerm); + + bool IsWrite = isa(I) || isa(I) || + isa(I); + + if (!AllocSize64) + 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, TablesBase + 3 * kTablesOffset, + I64Ty, RegionIndex); + Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy); + Value *Base = ThenIRB.CreateAnd(PtrInt, Mask); +#endif + + emitOobCheck(ThenIRB, PtrInt, Base, AllocSize, FixedAccessSize, nullptr, + ThenTerm, IsWrite); + + if (isa(I)) NumInstrumentedLoads++; + else if (isa(I)) NumInstrumentedStores++; + else NumInstrumentedAtomics++; + + return true; +} + +bool LowFatSanitizer::instrumentMemoryRange(Instruction *I, Value *Ptr, + Value *Size, bool IsWrite) { + IRBuilder<> IRB(I); + 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); + + LLVMContext &Ctx = M.getContext(); + Type *I64Ty = Type::getInt64Ty(Ctx); + Value *AllocSize64 = nullptr; + Value *IsLowFat = nullptr; + if (UseDarwinMetadataGuard) { + Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses); + IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion); + } else { + AllocSize64 = loadFromFixedTable(IRB, TablesBase + 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, 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, TablesBase + 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); + + NumInstrumentedMemIntrinsics++; + 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. 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); + + LLVMContext &Ctx = M.getContext(); + Type *I64Ty = Type::getInt64Ty(Ctx); + Value *AllocSize64 = nullptr; + Value *IsLowFat = nullptr; + if (UseDarwinMetadataGuard) { + Value *MaxRegion = ConstantInt::get(IntptrTy, NumSizeClasses); + IsLowFat = IRB.CreateICmpULT(RegionIndex, MaxRegion); + } else { + AllocSize64 = loadFromFixedTable(IRB, TablesBase + 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, 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, TablesBase + 3 * kTablesOffset, + I64Ty, RegionIndex); + Value *Mask = ThenIRB.CreateZExtOrTrunc(Mask64, IntptrTy); + Value *Base = ThenIRB.CreateAnd(SrcInt, Mask); +#endif + + // 2. OOB if result underflows (< Base) or reaches/passes the end (>= Base+AllocSize). + emitOobCheck(ThenIRB, ResInt, Base, AllocSize, 0, nullptr, ThenTerm, false); + + NumInstrumentedGEPs++; + return true; +} + +bool LowFatSanitizer::instrumentFunction(Function &F) { + bool Modified = false; + 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); + else if (isa(&I)) + ToInstrument.push_back(&I); + } + } + + 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); + } else if (auto *GEP = dyn_cast(I)) + Modified |= instrumentGEP(GEP); + } + 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"); + + // 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); + } + + // 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; + } + + // 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; +} + +} // anonymous namespace + +LowFatSanitizerPass::LowFatSanitizerPass(const LowFatSanitizerOptions &Options) + : Options(Options) {} + +PreservedAnalyses LowFatSanitizerPass::run(Module &M, + ModuleAnalysisManager &AM) { + LowFatSanitizer Sanitizer(M, Options); + if (!Sanitizer.run()) + return PreservedAnalyses::all(); + + return PreservedAnalyses::none(); +} 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) diff --git a/llvm/test/Instrumentation/LowFatSanitizer/basic.ll b/llvm/test/Instrumentation/LowFatSanitizer/basic.ll new file mode 100644 index 0000000000000..97588bb23bbc5 --- /dev/null +++ b/llvm/test/Instrumentation/LowFatSanitizer/basic.ll @@ -0,0 +1,102 @@ +; 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 inline checks +define i32 @test_load(ptr %p) { +; CHECK-LABEL: @test_load +; 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 +} + +; Test 2: Store should be instrumented +define void @test_store(ptr %p, i32 %v) { +; CHECK-LABEL: @test_store +; 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 +} + +; 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_report_oob +; 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_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_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: %[[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 +} + +; Test 6: i64 store should use size 8 +define void @test_store_i64(ptr %p, i64 %v) { +; CHECK-LABEL: @test_store_i64 +; 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 +} + +; Test 7: AtomicRMW should be instrumented +define i32 @test_atomic_rmw(ptr %p) { +; CHECK-LABEL: @test_atomic_rmw +; CHECK: call void @__lf_report_oob +; 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_report_oob +; 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_report_oob +; CHECK: load i32 +; CHECK: call void @__lf_report_oob +; CHECK: store i32 + %val = load i32, ptr %p, align 4 + store i32 %val, ptr %q, align 4 + ret void +} 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