[libc-commits] [libc] [libc] introduce sanitization check and heap walk utilities (PR #210373)
Schrodinger ZHU Yifan via libc-commits
libc-commits at lists.llvm.org
Fri Jul 17 09:57:44 PDT 2026
https://github.com/SchrodingerZhu updated https://github.com/llvm/llvm-project/pull/210373
>From c01f26dc58c984070e608f1e183dec27fff93f5c Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Fri, 17 Jul 2026 09:02:26 -0700
Subject: [PATCH 1/3] [libc] introduce sanitization check macros
TAG=agy
CONV=a13562f0-1759-4e08-b474-130301f9e743
---
.../modules/LLVMLibCCompileOptionRules.cmake | 4 ++
libc/config/config.json | 4 ++
libc/src/__support/libc_assert.h | 38 +++++++++++++++++++
3 files changed, 46 insertions(+)
diff --git a/libc/cmake/modules/LLVMLibCCompileOptionRules.cmake b/libc/cmake/modules/LLVMLibCCompileOptionRules.cmake
index b013152effa4c..9aefbd6ac5ac9 100644
--- a/libc/cmake/modules/LLVMLibCCompileOptionRules.cmake
+++ b/libc/cmake/modules/LLVMLibCCompileOptionRules.cmake
@@ -193,6 +193,10 @@ function(_get_compile_options_from_config output_var)
list(APPEND config_options "-DLIBC_COPT_USE_C_ASSERT")
endif()
+ if(LIBC_COPT_ENABLE_SANITIZATION)
+ libc_add_definition(config_options "LIBC_COPT_ENABLE_SANITIZATION")
+ endif()
+
set(${output_var} ${config_options} PARENT_SCOPE)
endfunction(_get_compile_options_from_config)
diff --git a/libc/config/config.json b/libc/config/config.json
index d576d7d14e4af..9c03a3cf09110 100644
--- a/libc/config/config.json
+++ b/libc/config/config.json
@@ -189,6 +189,10 @@
"LIBC_COPT_USE_C_ASSERT": {
"value": false,
"doc": "Use the system assert macro for LIBC_ASSERT."
+ },
+ "LIBC_COPT_ENABLE_SANITIZATION": {
+ "value": false,
+ "doc": "Enable sanitization checks in the library."
}
}
}
diff --git a/libc/src/__support/libc_assert.h b/libc/src/__support/libc_assert.h
index 26dd0fc2f9562..0f6ead63d017f 100644
--- a/libc/src/__support/libc_assert.h
+++ b/libc/src/__support/libc_assert.h
@@ -20,6 +20,22 @@
#define LIBC_ASSERT(COND) assert(COND)
#endif // LIBC_ASSERT
+#ifndef LIBC_COPT_ENABLE_SANITIZATION
+#define LIBC_COPT_ENABLE_SANITIZATION false
+#endif
+
+#if LIBC_COPT_ENABLE_SANITIZATION
+#define LIBC_SANITIZATION_CHECK(COND) \
+ do { \
+ LIBC_ASSERT((COND) && "Runtime sanitization failed."); \
+ __builtin_trap(); \
+ } while (false)
+#else
+#define LIBC_SANITIZATION_CHECK(COND) \
+ do { \
+ } while (false)
+#endif
+
#else // Not LIBC_COPT_USE_C_ASSERT
#include "src/__support/OSUtil/exit.h"
@@ -78,6 +94,28 @@ LIBC_INLINE void report_assertion_failure(const char *assertion,
} while (false)
#endif // NDEBUG
+#ifndef LIBC_COPT_ENABLE_SANITIZATION
+#define LIBC_COPT_ENABLE_SANITIZATION false
+#endif
+
+#if LIBC_COPT_ENABLE_SANITIZATION
+#define LIBC_SANITIZATION_CHECK(COND) \
+ do { \
+ if (LIBC_UNLIKELY(!(COND))) { \
+ LIBC_NAMESPACE::write_to_stderr(__FILE__ ":" LLVM_LIBC_STRINGIFY( \
+ __LINE__) ": Runtime sanitization failed: '" #COND \
+ "' in function: '"); \
+ LIBC_NAMESPACE::write_to_stderr(__PRETTY_FUNCTION__); \
+ LIBC_NAMESPACE::write_to_stderr("'\n"); \
+ __builtin_trap(); \
+ } \
+ } while (false)
+#else
+#define LIBC_SANITIZATION_CHECK(COND) \
+ do { \
+ } while (false)
+#endif
+
#endif // LIBC_COPT_USE_C_ASSERT
#endif // LLVM_LIBC_SRC___SUPPORT_LIBC_ASSERT_H
>From 9929f8af024c13b61e747e5ad3f2418fc93e7550 Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Fri, 17 Jul 2026 09:33:12 -0700
Subject: [PATCH 2/3] [libc] sanitize the freelist
---
libc/src/__support/freelist.cpp | 12 ++++++++++++
libc/src/__support/freelist.h | 10 ++++++++++
libc/src/__support/freestore.h | 7 +++++++
libc/src/__support/freetrie.cpp | 21 ++++++++++++++++++++-
libc/src/__support/freetrie.h | 16 ++++++++++++++++
5 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/libc/src/__support/freelist.cpp b/libc/src/__support/freelist.cpp
index 2fdcaadbdb554..133d2da1104f9 100644
--- a/libc/src/__support/freelist.cpp
+++ b/libc/src/__support/freelist.cpp
@@ -20,6 +20,7 @@ void FreeList::push(Node *node) {
LIBC_ASSERT(BlockRef::from_usable_space(node).outer_size() ==
begin_->block().outer_size() &&
"freelist entries must have the same size");
+ begin_->sanitize();
// Since the list is circular, insert the node immediately before begin_.
node->prev = begin_->prev;
node->next = begin_;
@@ -32,6 +33,7 @@ void FreeList::push(Node *node) {
void FreeList::remove(Node *node) {
LIBC_ASSERT(begin_ && "cannot remove from empty list");
+ node->sanitize();
Node *next = node->next;
if (node == next) {
LIBC_ASSERT(node == begin_ &&
@@ -46,4 +48,14 @@ void FreeList::remove(Node *node) {
}
}
+void FreeList::sanitize() const {
+ if (!begin_)
+ return;
+ Node *curr = begin_;
+ do {
+ curr->sanitize();
+ curr = curr->next;
+ } while (curr != begin_);
+}
+
} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/freelist.h b/libc/src/__support/freelist.h
index 48e70c7c29df6..96710bd292850 100644
--- a/libc/src/__support/freelist.h
+++ b/libc/src/__support/freelist.h
@@ -15,6 +15,7 @@
#define LLVM_LIBC_SRC___SUPPORT_FREELIST_H
#include "block.h"
+#include "src/__support/libc_assert.h"
namespace LIBC_NAMESPACE_DECL {
@@ -41,6 +42,12 @@ class FreeList {
/// @returns The inner size of blocks in the list containing this node.
LIBC_INLINE size_t size() const { return block().inner_size(); }
+ protected:
+ LIBC_INLINE void sanitize() const {
+ LIBC_SANITIZATION_CHECK(next->prev == this);
+ LIBC_SANITIZATION_CHECK(prev->next == this);
+ }
+
private:
// Circularly linked pointers to adjacent nodes.
Node *prev;
@@ -85,6 +92,9 @@ class FreeList {
/// Remove an arbitrary node from the list.
void remove(Node *node);
+ /// Verify integrity of all nodes in the list.
+ void sanitize() const;
+
private:
Node *begin_;
};
diff --git a/libc/src/__support/freestore.h b/libc/src/__support/freestore.h
index adc0e061ace93..9a9b084741377 100644
--- a/libc/src/__support/freestore.h
+++ b/libc/src/__support/freestore.h
@@ -46,6 +46,13 @@ class FreeStore {
/// allocated. Returns nullptr if there is no such block.
BlockRef remove_best_fit(size_t size);
+ /// Sanitization check for the entire store.
+ LIBC_INLINE void sanitize() const {
+ large_trie.sanitize();
+ for (const FreeList &list : small_lists)
+ list.sanitize();
+ }
+
private:
static constexpr size_t MIN_OUTER_SIZE = align_up(
BlockRef::HEADER_SIZE + sizeof(FreeList::Node), BlockRef::MIN_ALIGN);
diff --git a/libc/src/__support/freetrie.cpp b/libc/src/__support/freetrie.cpp
index e76efe717f215..9738c4635e581 100644
--- a/libc/src/__support/freetrie.cpp
+++ b/libc/src/__support/freetrie.cpp
@@ -7,11 +7,13 @@
//===----------------------------------------------------------------------===//
#include "freetrie.h"
+#include "src/__support/libc_assert.h"
namespace LIBC_NAMESPACE_DECL {
void FreeTrie::remove(Node *node) {
LIBC_ASSERT(!empty() && "cannot remove from empty trie");
+ node->sanitize();
FreeList list = node;
list.pop();
Node *new_node = static_cast<Node *>(list.begin());
@@ -20,8 +22,11 @@ void FreeTrie::remove(Node *node) {
// This is legal because there is no relationship between the size of the
// root and its children.
Node *leaf = node;
- while (leaf->lower || leaf->upper)
+ while (leaf->lower || leaf->upper) {
+ leaf->sanitize();
leaf = leaf->lower ? leaf->lower : leaf->upper;
+ }
+ leaf->sanitize();
if (leaf == node) {
// If the root is a leaf, then removing it empties the subtrie.
replace_node(node, nullptr);
@@ -44,6 +49,7 @@ void FreeTrie::remove(Node *node) {
void FreeTrie::replace_node(Node *node, Node *new_node) {
LIBC_ASSERT(is_head(node) && "only head nodes contain trie links");
+ node->sanitize();
if (node->parent) {
Node *&parent_child =
@@ -61,4 +67,17 @@ void FreeTrie::replace_node(Node *node, Node *new_node) {
node->upper->parent = new_node;
}
+void FreeTrie::sanitize() const {
+ auto sanitize_trie_node = [&](auto &self, const Node *node) -> void {
+ if (!node)
+ return;
+ node->sanitize();
+ FreeList list = const_cast<Node *>(node);
+ list.sanitize();
+ self(self, node->lower);
+ self(self, node->upper);
+ };
+ sanitize_trie_node(sanitize_trie_node, root);
+}
+
} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/freetrie.h b/libc/src/__support/freetrie.h
index 9e35463462b38..32f6a306f0c7e 100644
--- a/libc/src/__support/freetrie.h
+++ b/libc/src/__support/freetrie.h
@@ -15,6 +15,7 @@
#define LLVM_LIBC_SRC___SUPPORT_FREETRIE_H
#include "freelist.h"
+#include "src/__support/libc_assert.h"
namespace LIBC_NAMESPACE_DECL {
@@ -60,6 +61,16 @@ class FreeTrie {
Node *parent;
friend class FreeTrie;
+
+ LIBC_INLINE void sanitize() const {
+ FreeList::Node::sanitize();
+ if (lower)
+ LIBC_SANITIZATION_CHECK(lower->parent == this);
+ if (upper)
+ LIBC_SANITIZATION_CHECK(upper->parent == this);
+ if (parent)
+ LIBC_SANITIZATION_CHECK(parent->lower == this || parent->upper == this);
+ }
};
/// Power-of-two range of sizes covered by a subtrie.
@@ -110,6 +121,9 @@ class FreeTrie {
/// nullptr.
Node *find_best_fit(size_t size);
+ /// Verify integrity of all nodes in the trie.
+ void sanitize() const;
+
private:
/// @returns Whether a node is the head of its containing freelist.
bool is_head(Node *node) const { return node->parent || node == root; }
@@ -135,6 +149,7 @@ LIBC_INLINE void FreeTrie::push(BlockRef block) {
while (*cur && (*cur)->size() != size) {
LIBC_ASSERT(cur_range.contains(size) && "requested size out of trie range");
parent = *cur;
+ (*cur)->sanitize();
if (size <= cur_range.lower().max()) {
cur = &(*cur)->lower;
cur_range = cur_range.lower();
@@ -167,6 +182,7 @@ LIBC_INLINE FreeTrie::Node *FreeTrie::find_best_fit(size_t size) {
FreeTrie::SizeRange deferred_upper_range{0, 0};
while (true) {
+ cur->sanitize();
LIBC_ASSERT(cur_range.contains(cur->size()) &&
"trie node size out of range");
LIBC_ASSERT(cur_range.max() >= size &&
>From 2b3e18f3666fd00f0badbd283f1eef96b1b3fdef Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Fri, 17 Jul 2026 09:54:01 -0700
Subject: [PATCH 3/3] [libc] add sanitization tests for heap
TAG=agy
CONV=28eb1693-190f-4b50-8010-394735339711
---
libc/src/__support/freelist_heap.h | 1 +
libc/test/src/__support/freelist_heap_test.cpp | 16 ++++++++++++++++
2 files changed, 17 insertions(+)
diff --git a/libc/src/__support/freelist_heap.h b/libc/src/__support/freelist_heap.h
index 73a80754050dd..1b9e5f08ffff3 100644
--- a/libc/src/__support/freelist_heap.h
+++ b/libc/src/__support/freelist_heap.h
@@ -51,6 +51,7 @@ class FreeListHeap {
void *realloc(void *ptr, size_t size);
void *calloc(size_t num, size_t size);
size_t allocation_size(const void *ptr) const;
+ LIBC_INLINE void sanitize() const { free_store.sanitize(); }
cpp::span<cpp::byte> region() const { return {begin, end}; }
diff --git a/libc/test/src/__support/freelist_heap_test.cpp b/libc/test/src/__support/freelist_heap_test.cpp
index 1ee6bf0ce4ab4..4667eedcda599 100644
--- a/libc/test/src/__support/freelist_heap_test.cpp
+++ b/libc/test/src/__support/freelist_heap_test.cpp
@@ -366,3 +366,19 @@ TEST_FOR_EACH_ALLOCATOR(AllocationSize, 2048) {
allocator.free(ptr);
EXPECT_EQ(allocator.allocation_size(ptr), size_t(0));
}
+
+TEST_FOR_EACH_ALLOCATOR(CanSanitize, 2048) {
+ allocator.sanitize();
+
+ void *ptr1 = allocator.allocate(512);
+ allocator.sanitize();
+
+ void *ptr2 = allocator.allocate(512);
+ allocator.sanitize();
+
+ allocator.free(ptr1);
+ allocator.sanitize();
+
+ allocator.free(ptr2);
+ allocator.sanitize();
+}
More information about the libc-commits
mailing list