[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