[compiler-rt] [scudo] Add primary option to zero block on dealloc. (PR #142394)

via llvm-commits llvm-commits at lists.llvm.org
Wed Oct 1 07:20:25 PDT 2025


https://github.com/piwicode updated https://github.com/llvm/llvm-project/pull/142394

>From 7cee0509c2437bacb9fdf20492fcc6e18f5d33f7 Mon Sep 17 00:00:00 2001
From: Pierre Labatut <plabatut at google.com>
Date: Mon, 2 Jun 2025 15:19:02 +0200
Subject: [PATCH] [scudo] Add primary option to zero block on dealloc.

When all the blocks of a page are unused, the page will be full of zero
and decommitted on operating systems that scan the memory.

Change-Id: I278055d82057090b0a04d812b49cf93fdf467478
---
 .../lib/scudo/standalone/allocator_config.def |  9 +++
 compiler-rt/lib/scudo/standalone/flags.cpp    |  1 +
 compiler-rt/lib/scudo/standalone/flags.inc    |  5 ++
 compiler-rt/lib/scudo/standalone/primary32.h  |  1 +
 compiler-rt/lib/scudo/standalone/primary64.h  |  1 +
 .../scudo/standalone/size_class_allocator.h   | 31 ++++++++
 .../scudo/standalone/tests/primary_test.cpp   | 76 ++++++++++++++++++-
 7 files changed, 122 insertions(+), 2 deletions(-)

diff --git a/compiler-rt/lib/scudo/standalone/allocator_config.def b/compiler-rt/lib/scudo/standalone/allocator_config.def
index 748530820cd64..b4c48d9331def 100644
--- a/compiler-rt/lib/scudo/standalone/allocator_config.def
+++ b/compiler-rt/lib/scudo/standalone/allocator_config.def
@@ -114,6 +114,15 @@ PRIMARY_OPTIONAL_TYPE(ConditionVariableT, ConditionVariableDummy)
 // to, in increments of a power-of-2 scale. See `CompactPtrScale` also.
 PRIMARY_OPTIONAL_TYPE(CompactPtrT, uptr)
 
+// When enabled, chunk contents are zeroed out on deallocation. This can be
+// beneficial for security (to prevent information leaks) and for memory usage
+// on some systems where pages filled with zeroes can be decommitted by the OS
+// or better compressed by features like zram.
+PRIMARY_OPTIONAL(const bool, EnableZeroOnDealloc, false)
+// Only chunks smaller or equal to this threshold will be zeroed on
+// deallocation. Requires zero on dealloc to be enabled.
+PRIMARY_OPTIONAL(const s32, DefaultZeroOnDeallocMaxSize, INT32_MAX)
+
 // SECONDARY_REQUIRED_TEMPLATE_TYPE(NAME)
 //
 // Defines the type of Secondary Cache to use.
diff --git a/compiler-rt/lib/scudo/standalone/flags.cpp b/compiler-rt/lib/scudo/standalone/flags.cpp
index f498edfbd326a..84c3676c43320 100644
--- a/compiler-rt/lib/scudo/standalone/flags.cpp
+++ b/compiler-rt/lib/scudo/standalone/flags.cpp
@@ -9,6 +9,7 @@
 #include "flags.h"
 #include "common.h"
 #include "flags_parser.h"
+#include <limits.h>
 
 #include "scudo/interface.h"
 
diff --git a/compiler-rt/lib/scudo/standalone/flags.inc b/compiler-rt/lib/scudo/standalone/flags.inc
index ff0c28e1db7c4..4b3525bc3cf09 100644
--- a/compiler-rt/lib/scudo/standalone/flags.inc
+++ b/compiler-rt/lib/scudo/standalone/flags.inc
@@ -32,6 +32,11 @@ SCUDO_FLAG(bool, delete_size_mismatch, true,
            "Terminate on a size mismatch between a sized-delete and the actual "
            "size of a chunk (as provided to new/new[]).")
 
+SCUDO_FLAG(int, zero_on_dealloc_max_size, INT_MAX,
+           "Only chunks smaller or equal to this threshold will be zeroed on "
+           "deallocation. Requires zero on dealloc to be enabled on the "
+           "primary allocator, and has precedence on primary allocator limit.")
+
 SCUDO_FLAG(bool, zero_contents, false, "Zero chunk contents on allocation.")
 
 SCUDO_FLAG(bool, pattern_fill_contents, false,
diff --git a/compiler-rt/lib/scudo/standalone/primary32.h b/compiler-rt/lib/scudo/standalone/primary32.h
index 4385f4aa58450..3499c326243cd 100644
--- a/compiler-rt/lib/scudo/standalone/primary32.h
+++ b/compiler-rt/lib/scudo/standalone/primary32.h
@@ -44,6 +44,7 @@ namespace scudo {
 
 template <typename Config> class SizeClassAllocator32 {
 public:
+  using ConfigType = Config;
   typedef typename Config::CompactPtrT CompactPtrT;
   typedef typename Config::SizeClassMap SizeClassMap;
   static const uptr GroupSizeLog = Config::getGroupSizeLog();
diff --git a/compiler-rt/lib/scudo/standalone/primary64.h b/compiler-rt/lib/scudo/standalone/primary64.h
index 747b1a2233d32..9bb96f896288b 100644
--- a/compiler-rt/lib/scudo/standalone/primary64.h
+++ b/compiler-rt/lib/scudo/standalone/primary64.h
@@ -48,6 +48,7 @@ namespace scudo {
 
 template <typename Config> class SizeClassAllocator64 {
 public:
+  using ConfigType = Config;
   typedef typename Config::CompactPtrT CompactPtrT;
   typedef typename Config::SizeClassMap SizeClassMap;
   typedef typename Config::ConditionVariableT ConditionVariableT;
diff --git a/compiler-rt/lib/scudo/standalone/size_class_allocator.h b/compiler-rt/lib/scudo/standalone/size_class_allocator.h
index 7c7d6307f8f0a..ea7a6de46ace9 100644
--- a/compiler-rt/lib/scudo/standalone/size_class_allocator.h
+++ b/compiler-rt/lib/scudo/standalone/size_class_allocator.h
@@ -9,12 +9,15 @@
 #ifndef SCUDO_SIZE_CLASS_ALLOCATOR_H_
 #define SCUDO_SIZE_CLASS_ALLOCATOR_H_
 
+#include "common.h"
+#include "flags.h"
 #include "internal_defs.h"
 #include "list.h"
 #include "platform.h"
 #include "report.h"
 #include "stats.h"
 #include "string_utils.h"
+#include <limits.h>
 
 namespace scudo {
 
@@ -28,6 +31,12 @@ template <class SizeClassAllocator> struct SizeClassAllocatorLocalCache {
     if (LIKELY(S))
       S->link(&Stats);
     Allocator = A;
+
+    ZeroOnDeallocMaxSize = static_cast<uptr>(
+        Max(0, (getFlags()->zero_on_dealloc_max_size != INT_MAX)
+                   ? getFlags()->zero_on_dealloc_max_size
+                   : SizeClassAllocator::ConfigType::
+                         getDefaultZeroOnDeallocMaxSize()));
     initAllocator();
   }
 
@@ -59,6 +68,14 @@ template <class SizeClassAllocator> struct SizeClassAllocatorLocalCache {
 
   bool deallocate(uptr ClassId, void *P) {
     CHECK_LT(ClassId, NumClasses);
+
+    if (SizeClassAllocator::ConfigType::getEnableZeroOnDealloc()) {
+      const uptr ClassSize = PerClassArray[ClassId].ClassSize;
+      if (ClassSize <= ZeroOnDeallocMaxSize) {
+        memset(P, 0, ClassSize);
+      }
+    }
+
     PerClass *C = &PerClassArray[ClassId];
 
     // If the cache is full, drain half of blocks back to the main allocator.
@@ -145,6 +162,7 @@ template <class SizeClassAllocator> struct SizeClassAllocatorLocalCache {
   PerClass PerClassArray[NumClasses] = {};
   LocalStats Stats;
   SizeClassAllocator *Allocator = nullptr;
+  uptr ZeroOnDeallocMaxSize = 0;
 
   NOINLINE void initAllocator() {
     for (uptr I = 0; I < NumClasses; I++) {
@@ -188,6 +206,11 @@ template <class SizeClassAllocator> struct SizeClassAllocatorNoCache {
     if (LIKELY(S))
       S->link(&Stats);
     Allocator = A;
+    ZeroOnDeallocMaxSize = static_cast<uptr>(
+        Max(0, (getFlags()->zero_on_dealloc_max_size != INT_MAX)
+                   ? getFlags()->zero_on_dealloc_max_size
+                   : SizeClassAllocator::ConfigType::
+                         getDefaultZeroOnDeallocMaxSize()));
     initAllocator();
   }
 
@@ -211,6 +234,13 @@ template <class SizeClassAllocator> struct SizeClassAllocatorNoCache {
   bool deallocate(uptr ClassId, void *P) {
     CHECK_LT(ClassId, NumClasses);
 
+    if (SizeClassAllocator::ConfigType::getEnableZeroOnDealloc()) {
+      const uptr ClassSize = PerClassArray[ClassId].ClassSize;
+      if (ClassSize <= ZeroOnDeallocMaxSize) {
+        memset(P, 0, ClassSize);
+      }
+    }
+
     if (ClassId == BatchClassId)
       return deallocateBatchClassBlock(P);
 
@@ -288,6 +318,7 @@ template <class SizeClassAllocator> struct SizeClassAllocatorNoCache {
   CompactPtrT BatchClassStorage[SizeClassMap::MaxNumCachedHint] = {};
   LocalStats Stats;
   SizeClassAllocator *Allocator = nullptr;
+  uptr ZeroOnDeallocMaxSize = 0;
 
   bool deallocateBatchClassBlock(void *P) {
     PerClass *C = &PerClassArray[BatchClassId];
diff --git a/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp b/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp
index 1f5df28fd7771..6ea1475f2ea7d 100644
--- a/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp
+++ b/compiler-rt/lib/scudo/standalone/tests/primary_test.cpp
@@ -150,6 +150,28 @@ template <typename SizeClassMapT> struct TestConfig5 {
   };
 };
 
+// Enable `ZeroOnDealloc`
+template <typename SizeClassMapT> struct TestConfig6 {
+  static const bool MaySupportMemoryTagging = false;
+  template <typename> using TSDRegistryT = void;
+  template <typename> using PrimaryT = void;
+  template <typename> using SecondaryT = void;
+
+  struct Primary {
+    using SizeClassMap = SizeClassMapT;
+    static const scudo::uptr RegionSizeLog = 23U;
+    static const scudo::uptr GroupSizeLog = 20U;
+    static const scudo::s32 MinReleaseToOsIntervalMs = INT32_MIN;
+    static const scudo::s32 MaxReleaseToOsIntervalMs = INT32_MAX;
+    typedef scudo::uptr CompactPtrT;
+    static const scudo::uptr CompactPtrScale = 0;
+    static const bool EnableRandomOffset = true;
+    static const scudo::uptr MapSizeIncrement = 1UL << 18;
+    static const bool EnableZeroOnDealloc = true;
+    static const scudo::s32 DefaultZeroOnDeallocMaxSize = 1 << 12;
+  };
+};
+
 template <template <typename> class BaseConfig, typename SizeClassMapT>
 struct Config : public BaseConfig<SizeClassMapT> {};
 
@@ -191,7 +213,8 @@ struct ScudoPrimaryTest : public Test {};
   SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TestConfig2)                            \
   SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TestConfig3)                            \
   SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TestConfig4)                            \
-  SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TestConfig5)
+  SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TestConfig5)                            \
+  SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TestConfig6)
 #endif
 
 #define SCUDO_TYPED_TEST_TYPE(FIXTURE, NAME, TYPE)                             \
@@ -296,6 +319,54 @@ TEST(ScudoPrimaryTest, Primary64OOM) {
   Allocator.unmapTestOnly();
 }
 
+TEST(ScudoPrimaryTest, ZeroOnDeallocFlagLimit) {
+  for (scudo::s32 flag_value :
+       {INT_MAX, INT_MAX - 1, 1 << 12, 1 << 10,
+        static_cast<scudo::s32>(
+            scudo::DefaultSizeClassMap::getSizeByClassId(6)),
+        static_cast<scudo::s32>(
+            scudo::DefaultSizeClassMap::getSizeByClassId(6) - 1)}) {
+    // Override the flag value.
+    scudo::getFlags()->zero_on_dealloc_max_size = flag_value;
+    // INT_MAX flag_value stands for unset, then the static parameter is used.
+    const scudo::uptr threshold =
+        flag_value == INT_MAX ? TestConfig6<scudo::DefaultSizeClassMap>::
+                                    Primary::DefaultZeroOnDeallocMaxSize
+                              : static_cast<scudo::uptr>(flag_value);
+
+    using Primary = TestAllocator<TestConfig6, scudo::DefaultSizeClassMap>;
+    Primary Allocator;
+    Allocator.init(/*ReleaseToOsInterval=*/-1);
+    typename Primary::SizeClassAllocatorT SizeClassAllocator;
+    scudo::GlobalStats Stats;
+    Stats.init();
+    SizeClassAllocator.init(&Stats, &Allocator);
+    for (scudo::uptr ClassId = 1;
+         ClassId < Primary::SizeClassMap::LargestClassId; ClassId++) {
+      void *Ptr = SizeClassAllocator.allocate(ClassId);
+      EXPECT_NE(Ptr, nullptr);
+      const scudo::uptr Size = Primary::getSizeByClassId(ClassId);
+      memset(Ptr, 'B', Size);
+
+      SizeClassAllocator.deallocate(ClassId, Ptr);
+      if (Size <= threshold) {
+        // Verify the block is full of zeros.
+        for (scudo::uptr I = 1; I < Size; ++I) {
+          ASSERT_TRUE(static_cast<char *>(Ptr)[I] == 0);
+        }
+      } else {
+        // Verify the block is full of data.
+        for (scudo::uptr I = 1; I < Size; ++I) {
+          ASSERT_TRUE(static_cast<char *>(Ptr)[I] != 0);
+        }
+      }
+    }
+    SizeClassAllocator.destroy(nullptr);
+    Allocator.releaseToOS(scudo::ReleaseToOS::Force);
+    Allocator.unmapTestOnly();
+  }
+}
+
 SCUDO_TYPED_TEST(ScudoPrimaryTest, PrimaryIterate) {
   using Primary = TestAllocator<TypeParam, scudo::DefaultSizeClassMap>;
   std::unique_ptr<Primary> Allocator(new Primary);
@@ -334,7 +405,8 @@ SCUDO_TYPED_TEST(ScudoPrimaryTest, PrimaryIterate) {
 }
 
 SCUDO_TYPED_TEST(ScudoPrimaryTest, PrimaryThreaded) {
-  using Primary = TestAllocator<TypeParam, scudo::Config::Primary::SizeClassMap>;
+  using Primary =
+      TestAllocator<TypeParam, scudo::Config::Primary::SizeClassMap>;
   std::unique_ptr<Primary> Allocator(new Primary);
   Allocator->init(/*ReleaseToOsInterval=*/-1);
   std::mutex Mutex;



More information about the llvm-commits mailing list