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

via llvm-commits llvm-commits at lists.llvm.org
Mon Nov 17 06:55:24 PST 2025


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

>From 40b656e047834b96bae51da94e798fd03e77fd1c 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 |   5 +
 compiler-rt/lib/scudo/standalone/combined.h   |  31 ++--
 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/tests/combined_test.cpp  | 147 ++++++++++++++++--
 7 files changed, 169 insertions(+), 22 deletions(-)

diff --git a/compiler-rt/lib/scudo/standalone/allocator_config.def b/compiler-rt/lib/scudo/standalone/allocator_config.def
index 0aea7b8f2fb9a..c3498607afe0a 100644
--- a/compiler-rt/lib/scudo/standalone/allocator_config.def
+++ b/compiler-rt/lib/scudo/standalone/allocator_config.def
@@ -61,6 +61,11 @@ BASE_OPTIONAL(const bool, QuarantineDisabled, false)
 // If set to false, return the total available size in the allocation.
 BASE_OPTIONAL(const bool, ExactUsableSize, true)
 
+// Only chunks smaller or equal to this threshold will be zeroed on
+// deallocation. Requires zero on dealloc to be enabled.
+BASE_OPTIONAL(const bool, EnableZeroOnDealloc, false)
+
+
 // PRIMARY_REQUIRED_TYPE(NAME)
 //
 // SizeClassMap to use with the Primary.
diff --git a/compiler-rt/lib/scudo/standalone/combined.h b/compiler-rt/lib/scudo/standalone/combined.h
index ffe9554203241..d39aaa293c184 100644
--- a/compiler-rt/lib/scudo/standalone/combined.h
+++ b/compiler-rt/lib/scudo/standalone/combined.h
@@ -177,6 +177,8 @@ class Allocator {
 
     QuarantineMaxChunkSize =
         static_cast<u32>(getFlags()->quarantine_max_chunk_size);
+    ZeroOnDeallocMaxSize =
+        static_cast<u32>(getFlags()->zero_on_dealloc_max_size);
 
     Stats.init();
     // TODO(chiahungduan): Given that we support setting the default value in
@@ -1035,6 +1037,7 @@ class Allocator {
 
   u32 Cookie = 0;
   u32 QuarantineMaxChunkSize = 0;
+  u32 ZeroOnDeallocMaxSize = 0;
 
   GlobalStats Stats;
   PrimaryT Primary;
@@ -1334,17 +1337,25 @@ class Allocator {
 
     Chunk::storeHeader(Cookie, Ptr, Header);
 
-    if (BypassQuarantine) {
-      void *BlockBegin;
-      if (LIKELY(!useMemoryTagging<AllocatorConfig>(Options))) {
-        // Must do this after storeHeader because loadHeader uses a tagged ptr.
-        if (allocatorSupportsMemoryTagging<AllocatorConfig>())
-          Ptr = untagPointer(Ptr);
-        BlockBegin = getBlockBegin(Ptr, Header);
-      } else {
-        BlockBegin = retagBlock(Options, TaggedPtr, Ptr, Header, Size, true);
+    void *BlockBegin;
+    if (LIKELY(!useMemoryTagging<AllocatorConfig>(Options))) {
+      // Must do this after storeHeader because loadHeader uses a tagged ptr.
+      if (allocatorSupportsMemoryTagging<AllocatorConfig>())
+        Ptr = untagPointer(Ptr);
+      BlockBegin = getBlockBegin(Ptr, Header);
+    } else {
+      BlockBegin = retagBlock(Options, TaggedPtr, Ptr, Header, Size, true);
+    }
+
+    if (AllocatorConfig::getEnableZeroOnDealloc()) {
+      uptr length = reinterpret_cast<uptr>(Ptr) + Size -
+                    reinterpret_cast<uptr>(BlockBegin);
+      if (length <= ZeroOnDeallocMaxSize) {
+        memset(BlockBegin, 0, length);
       }
+    }
 
+    if (BypassQuarantine) {
       const uptr ClassId = Header->ClassId;
       if (LIKELY(ClassId)) {
         bool CacheDrained;
@@ -1363,8 +1374,6 @@ class Allocator {
         Secondary.deallocate(Options, BlockBegin);
       }
     } else {
-      if (UNLIKELY(useMemoryTagging<AllocatorConfig>(Options)))
-        retagBlock(Options, TaggedPtr, Ptr, Header, Size, false);
       typename TSDRegistryT::ScopedTSD TSD(TSDRegistry);
       Quarantine.put(&TSD->getQuarantineCache(),
                      QuarantineCallback(*this, TSD->getSizeClassAllocator()),
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/tests/combined_test.cpp b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
index 4837ac96b9b26..92db9a36ee087 100644
--- a/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
+++ b/compiler-rt/lib/scudo/standalone/tests/combined_test.cpp
@@ -19,6 +19,7 @@
 
 #include <algorithm>
 #include <condition_variable>
+#include <cstdio>
 #include <memory>
 #include <mutex>
 #include <set>
@@ -89,26 +90,24 @@ template <typename Config> struct TestAllocator : scudo::Allocator<Config> {
   void operator delete(void *ptr);
 };
 
-constexpr size_t kMaxAlign = std::max({
-  alignof(scudo::Allocator<scudo::DefaultConfig>),
+constexpr size_t kMaxAlign =
+    std::max({alignof(scudo::Allocator<scudo::DefaultConfig>),
 #if SCUDO_CAN_USE_PRIMARY64
-      alignof(scudo::Allocator<scudo::FuchsiaConfig>),
+              alignof(scudo::Allocator<scudo::FuchsiaConfig>),
 #endif
-      alignof(scudo::Allocator<scudo::AndroidConfig>)
-});
+              alignof(scudo::Allocator<scudo::AndroidConfig>)});
 
 #if SCUDO_RISCV64
 // The allocator is over 4MB large. Rather than creating an instance of this on
 // the heap, keep it in a global storage to reduce fragmentation from having to
 // mmap this at the start of every test.
 struct TestAllocatorStorage {
-  static constexpr size_t kMaxSize = std::max({
-    sizeof(scudo::Allocator<scudo::DefaultConfig>),
+  static constexpr size_t kMaxSize =
+      std::max({sizeof(scudo::Allocator<scudo::DefaultConfig>),
 #if SCUDO_CAN_USE_PRIMARY64
-        sizeof(scudo::Allocator<scudo::FuchsiaConfig>),
+                sizeof(scudo::Allocator<scudo::FuchsiaConfig>),
 #endif
-        sizeof(scudo::Allocator<scudo::AndroidConfig>)
-  });
+                sizeof(scudo::Allocator<scudo::AndroidConfig>)});
 
   // To alleviate some problem, let's skip the thread safety analysis here.
   static void *get(size_t size) NO_THREAD_SAFETY_ANALYSIS {
@@ -838,7 +837,8 @@ SCUDO_TYPED_TEST(ScudoCombinedTest, ReleaseToOS) {
 
 SCUDO_TYPED_TEST(ScudoCombinedTest, OddEven) {
   auto *Allocator = this->Allocator.get();
-  Allocator->setOption(scudo::Option::MemtagTuning, M_MEMTAG_TUNING_BUFFER_OVERFLOW);
+  Allocator->setOption(scudo::Option::MemtagTuning,
+                       M_MEMTAG_TUNING_BUFFER_OVERFLOW);
 
   if (!Allocator->useMemoryTaggingTestOnly())
     return;
@@ -1077,6 +1077,131 @@ struct TestQuarantineSizeClassConfig {
   static const scudo::uptr SizeDelta = 0;
 };
 
+struct TestZeroOnDeallocConfig {
+  static const bool MaySupportMemoryTagging = false;
+  static const bool EnableZeroOnDealloc = true;
+
+  template <class A> using TSDRegistryT = scudo::TSDRegistrySharedT<A, 1U, 1U>;
+  struct Primary {
+    // Tiny allocator, its Primary only serves chunks of four sizes.
+    using SizeClassMap =
+        scudo::FixedSizeClassMap<TestQuarantineSizeClassConfig>;
+    static const scudo::uptr RegionSizeLog = DeathRegionSizeLog;
+    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 scudo::uptr GroupSizeLog = 18;
+  };
+
+  template <typename Config>
+  using PrimaryT = scudo::SizeClassAllocator64<Config>;
+
+  struct Secondary {
+    template <typename Config>
+    using CacheT = scudo::MapAllocatorNoCache<Config>;
+  };
+
+  template <typename Config> using SecondaryT = scudo::MapAllocator<Config>;
+};
+
+struct TestNoZeroOnDeallocConfig : public TestZeroOnDeallocConfig {
+  static const bool EnableZeroOnDealloc = false;
+};
+
+TEST(ScudoCombinedTest, ZeroOnDeallocDisabled) {
+  // When option `EnableZeroOnDealloc` is false the memeory is not cleared on deallocation.
+  using AllocatorT = TestAllocator<TestNoZeroOnDeallocConfig>;
+  auto Allocator = std::unique_ptr<AllocatorT>(new AllocatorT());
+
+  constexpr scudo::uptr AllocatedSize = 1024;
+
+  void *P = Allocator->allocate(AllocatedSize, Origin);
+  ASSERT_NE(P, nullptr);
+  memset(P, 'B', AllocatedSize);
+
+  Allocator->deallocate(P, Origin);
+
+  for (scudo::uptr I = 1; I < AllocatedSize; ++I) {
+    // Verifies the memory was left unchanged.
+    ASSERT_EQ(static_cast<char *>(P)[I], 'B')
+        << "at position " << I;
+  }
+}
+
+TEST(ScudoCombinedTest, ZeroOnDeallocEnabled) {
+  // When option `EnableZeroOnDealloc` is true, and the flag `zero_on_dealloc_max_size` is
+  // not set there is no size limit and the memory is always cleared on deallocation.
+  using AllocatorT = TestAllocator<TestZeroOnDeallocConfig>;
+  auto Allocator = std::unique_ptr<AllocatorT>(new AllocatorT());
+
+  constexpr scudo::uptr AllocatedSize = 1024;
+
+  void *P = Allocator->allocate(AllocatedSize, Origin);
+  ASSERT_NE(P, nullptr);
+  memset(P, 'B', AllocatedSize);
+
+  char *Begin = reinterpret_cast<char *>(Allocator->getBlockBeginTestOnly(P));
+  char *End = reinterpret_cast<char *>(P) + AllocatedSize;
+  // Deallocates and eventually clears the memory.
+  Allocator->deallocate(P, Origin);
+
+  // Verifies the memory was cleared, including the header.
+  for (char *T = Begin; T < End; ++T) {
+    ASSERT_EQ(*T, 0) << "at position=" << T - Begin;
+  }
+}
+
+// Verify that the quarantine exists by default.
+TEST(ScudoCombinedTest, ZeroOnDeallocEnabledAndFlag) {
+  ([]() { // Cleanup on exit scope.
+    for (scudo::uptr FlagValue = 128; FlagValue <= 2048; FlagValue *= 2) {
+      // Set the size limit flag via the environment variable.
+      char Options[256];
+      snprintf(Options, sizeof(Options),
+               "SCUDO_OPTIONS=zero_on_dealloc_max_size=%d", FlagValue);
+      putenv(Options);
+
+      // Creates an allocator, configured from the environment.
+      using AllocatorT = TestAllocator<TestZeroOnDeallocConfig>;
+      auto Allocator = std::unique_ptr<AllocatorT>(new AllocatorT());
+
+      for (scudo::uptr AllocatedSize : {FlagValue / 2, FlagValue}) {
+        // Allocates and sets the memory.
+        void *P = Allocator->allocate(AllocatedSize, Origin);
+        ASSERT_NE(P, nullptr);
+        memset(P, 'B', AllocatedSize);
+
+        char *Begin =
+            reinterpret_cast<char *>(Allocator->getBlockBeginTestOnly(P));
+        char *End = reinterpret_cast<char *>(P) + AllocatedSize;
+        // Deallocates and eventually clears the memory.
+        Allocator->deallocate(P, Origin);
+
+        if (End - Begin <= FlagValue) {
+          // Verifies the memory was cleared, including the header.
+          for (char *T = Begin; T < End; ++T) {
+            ASSERT_EQ(*T, 0) << "at position=" << T - Begin
+                            << " for FlagValue=" << FlagValue
+                            << " AllocatedSize=" << AllocatedSize;
+          }
+        } else {
+          for (scudo::uptr I = 1; I < AllocatedSize; ++I) {
+            // Verifies the memory was left unchanged.
+            ASSERT_EQ(static_cast<char *>(P)[I], 'B')
+                << "at position " << I << " for FlagValue=" << FlagValue
+                << " AllocatedSize=" << AllocatedSize;
+          }
+        }
+      }
+    }
+  })();
+  // Clear the scudo flag option.
+  unsetenv("SCUDO_OPTIONS");
+}
+
 struct TestQuarantineConfig {
   static const bool MaySupportMemoryTagging = false;
 



More information about the llvm-commits mailing list