[libc-commits] [libc] [libc][hermetic] use mutex-protected linearly growable freelist heap for hermetic test (PR #208587)

Schrodinger ZHU Yifan via libc-commits libc-commits at lists.llvm.org
Thu Jul 16 13:56:25 PDT 2026


https://github.com/SchrodingerZhu updated https://github.com/llvm/llvm-project/pull/208587

>From 67d810ac873048132dc2dc83e26cf2740eb500b2 Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Thu, 9 Jul 2026 14:55:16 -0700
Subject: [PATCH 1/4] [libc][freetrie] add dynamic expansion support

---
 libc/src/__support/CMakeLists.txt         |  1 +
 libc/src/__support/freetrie.h             | 26 ++++++++++++++
 libc/test/src/__support/freetrie_test.cpp | 43 +++++++++++++++++++++++
 3 files changed, 70 insertions(+)

diff --git a/libc/src/__support/CMakeLists.txt b/libc/src/__support/CMakeLists.txt
index f4cb283976bc3..94d9cc8baa6d1 100644
--- a/libc/src/__support/CMakeLists.txt
+++ b/libc/src/__support/CMakeLists.txt
@@ -49,6 +49,7 @@ add_object_library(
   DEPENDS
     .block
     .freelist
+    libc.src.__support.math_extras
 )
 
 add_header_library(
diff --git a/libc/src/__support/freetrie.h b/libc/src/__support/freetrie.h
index 9e35463462b38..3dbb11eb3a063 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/math_extras.h"
 
 namespace LIBC_NAMESPACE_DECL {
 
@@ -126,6 +127,31 @@ LIBC_INLINE void FreeTrie::push(BlockRef block) {
   LIBC_ASSERT(block.inner_size_free() >= sizeof(Node) &&
               "block too small to accomodate free trie node");
   size_t size = block.inner_size();
+
+  if (!range.contains(size)) {
+    if (empty()) {
+      range = SizeRange(
+          range.min,
+          cpp::max(range.width, cpp::bit_ceil(size - range.min + 1)));
+    } else {
+    // Dynamically expand the trie upwards by doubling the range and creating a
+    // new root node using the pushed block. The previous root becomes the
+    // lower child of the new root.
+    Node *node = new (block.usable_space()) Node;
+    node->parent = nullptr;
+    node->lower = root;
+    node->upper = nullptr;
+    FreeList list;
+    list.push(node);
+    root->parent = node;
+    root = node;
+    range = SizeRange(range.min, range.width * 2);
+    LIBC_ASSERT(range.contains(size) &&
+                "pushed block size exceeds dynamic trie expansion limit (at most 2x current range when non-empty)");
+    return;
+    }
+  }
+
   LIBC_ASSERT(range.contains(size) && "requested size out of trie range");
 
   // Find the position in the tree to push to.
diff --git a/libc/test/src/__support/freetrie_test.cpp b/libc/test/src/__support/freetrie_test.cpp
index bf3284b2faf64..9cc0f9edf583f 100644
--- a/libc/test/src/__support/freetrie_test.cpp
+++ b/libc/test/src/__support/freetrie_test.cpp
@@ -132,3 +132,46 @@ TEST(LlvmLibcFreeTrie, Remove) {
   EXPECT_EQ(trie.find_best_fit(large.inner_size())->block().addr(),
             large.addr());
 }
+
+TEST(LlvmLibcFreeTrie, DynamicExpansion) {
+  byte mem[4096];
+  optional<BlockRef> maybe_block = BlockRef::init(mem);
+  ASSERT_TRUE(maybe_block.has_value());
+  BlockRef block1 = *maybe_block;
+  maybe_block = block1.split(256);
+  ASSERT_TRUE(maybe_block.has_value());
+  BlockRef block2 = *maybe_block;
+  maybe_block = block2.split(1500);
+  ASSERT_TRUE(maybe_block.has_value());
+
+  FreeTrie trie({0, 1024});
+  trie.push(block1);
+  EXPECT_EQ(trie.find_best_fit(block1.inner_size())->block().addr(),
+            block1.addr());
+
+  // Pushing block2 (inner_size >= 1500) exceeds the initial range of 1024,
+  // triggering dynamic trie expansion to 2048 and creating a new root.
+  trie.push(block2);
+  EXPECT_EQ(trie.find_best_fit(block2.inner_size())->block().addr(),
+            block2.addr());
+  EXPECT_EQ(trie.find_best_fit(block1.inner_size())->block().addr(),
+            block1.addr());
+
+  trie.remove(trie.find_best_fit(block2.inner_size()));
+  EXPECT_EQ(trie.find_best_fit(block1.inner_size())->block().addr(),
+            block1.addr());
+  trie.remove(trie.find_best_fit(block1.inner_size()));
+  EXPECT_TRUE(trie.empty());
+}
+
+TEST(LlvmLibcFreeTrie, DynamicExpansionFromEmpty) {
+  byte mem[4096];
+  optional<BlockRef> maybe_block = BlockRef::init(mem);
+  ASSERT_TRUE(maybe_block.has_value());
+  BlockRef block = *maybe_block;
+
+  FreeTrie trie; // Default constructed with {0, 0}
+  trie.push(block);
+  EXPECT_EQ(trie.find_best_fit(block.inner_size())->block().addr(),
+            block.addr());
+}

>From 3ff98a298036601935c1640fe67f10d8e9f2e483 Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Thu, 9 Jul 2026 15:04:57 -0700
Subject: [PATCH 2/4] [libc][freelist-heap] add adopt API and mmap tests

---
 libc/src/__support/freelist_heap.h            | 14 +++++++++++
 .../test/src/__support/freelist_heap_test.cpp | 25 +++++++++++++++++++
 2 files changed, 39 insertions(+)

diff --git a/libc/src/__support/freelist_heap.h b/libc/src/__support/freelist_heap.h
index 73a80754050dd..a5b834e0a2f81 100644
--- a/libc/src/__support/freelist_heap.h
+++ b/libc/src/__support/freelist_heap.h
@@ -43,6 +43,8 @@ class FreeListHeap {
   constexpr FreeListHeap(span<cpp::byte> region)
       : begin(region.begin()), end(region.end()) {}
 
+  bool adopt(span<cpp::byte> mem);
+
   void *allocate(size_t size);
   void *aligned_allocate(size_t alignment, size_t size);
   // NOTE: All pointers passed to free must come from one of the other
@@ -90,6 +92,18 @@ LIBC_INLINE void FreeListHeap::init() {
   is_initialized = true;
 }
 
+LIBC_INLINE bool FreeListHeap::adopt(span<cpp::byte> mem) {
+  if (!is_initialized)
+    init();
+  LIBC_ASSERT(end == mem.begin() && "Adopted region must be contiguous");
+  auto result = BlockRef::init(mem);
+  if (!result.has_value())
+    return false;
+  end = mem.end();
+  free_store.insert(*result);
+  return true;
+}
+
 LIBC_INLINE void *FreeListHeap::allocate_impl(size_t alignment, size_t size) {
   if (size == 0)
     return nullptr;
diff --git a/libc/test/src/__support/freelist_heap_test.cpp b/libc/test/src/__support/freelist_heap_test.cpp
index 1ee6bf0ce4ab4..273671822b29b 100644
--- a/libc/test/src/__support/freelist_heap_test.cpp
+++ b/libc/test/src/__support/freelist_heap_test.cpp
@@ -366,3 +366,28 @@ TEST_FOR_EACH_ALLOCATOR(AllocationSize, 2048) {
   allocator.free(ptr);
   EXPECT_EQ(allocator.allocation_size(ptr), size_t(0));
 }
+
+TEST(LlvmLibcFreeListHeap, Adopt) {
+  constexpr size_t N = 2048;
+  byte buf[N * 2] = {byte(0)};
+  span<byte> buf1(&buf[0], N);
+  span<byte> buf2(&buf[N], N);
+
+  FreeListHeap allocator(buf1);
+  void *ptr1 = allocator.allocate(1500);
+  EXPECT_NE(ptr1, static_cast<void *>(nullptr));
+
+  // Initial buffer is now almost full; allocating another 1500 bytes should fail.
+  void *ptr_fail = allocator.allocate(1500);
+  EXPECT_EQ(ptr_fail, static_cast<void *>(nullptr));
+
+  // Adopt the second buffer into the heap.
+  EXPECT_TRUE(allocator.adopt(buf2));
+
+  // Now allocating 1500 bytes should succeed from the adopted buffer.
+  void *ptr2 = allocator.allocate(1500);
+  EXPECT_NE(ptr2, static_cast<void *>(nullptr));
+
+  allocator.free(ptr1);
+  allocator.free(ptr2);
+}

>From 59a24f310755d834c98611a5c7722972ee64ee8b Mon Sep 17 00:00:00 2001
From: yfzhu <yfzhu at google.com>
Date: Thu, 9 Jul 2026 16:44:33 -0700
Subject: [PATCH 3/4] [libc][hermetic] use mutex-protected freelist heap  for
 hermetic test

TAG=agy
CONV=c9abb6ae-50f2-4602-87aa-fb0c972e80f0
---
 libc/src/__support/CMakeLists.txt          |   8 +-
 libc/src/__support/freelist.cpp            |  49 ------
 libc/src/__support/freelist.h              |  29 +++-
 libc/src/__support/freetrie.cpp            |  64 --------
 libc/src/__support/freetrie.h              |  51 ++++++
 libc/test/UnitTest/CMakeLists.txt          |  10 ++
 libc/test/UnitTest/HermeticHeap.cpp        |  30 ++++
 libc/test/UnitTest/HermeticTestUtils.cpp   |  44 +-----
 libc/test/UnitTest/sbrk_heap.h             | 173 +++++++++++++++++++++
 libc/test/src/__support/CMakeLists.txt     |  15 ++
 libc/test/src/__support/sbrk_heap_test.cpp |  56 +++++++
 11 files changed, 367 insertions(+), 162 deletions(-)
 delete mode 100644 libc/src/__support/freelist.cpp
 delete mode 100644 libc/src/__support/freetrie.cpp
 create mode 100644 libc/test/UnitTest/HermeticHeap.cpp
 create mode 100644 libc/test/UnitTest/sbrk_heap.h
 create mode 100644 libc/test/src/__support/sbrk_heap_test.cpp

diff --git a/libc/src/__support/CMakeLists.txt b/libc/src/__support/CMakeLists.txt
index 94d9cc8baa6d1..8b623ee11d970 100644
--- a/libc/src/__support/CMakeLists.txt
+++ b/libc/src/__support/CMakeLists.txt
@@ -25,12 +25,10 @@ add_header_library(
     libc.src.__support.math_extras
 )
 
-add_object_library(
+add_header_library(
   freelist
   HDRS
     freelist.h
-  SRCS
-    freelist.cpp
   DEPENDS
     .block
     libc.src.__support.fixedvector
@@ -40,12 +38,10 @@ add_object_library(
     libc.src.__support.CPP.span
 )
 
-add_object_library(
+add_header_library(
   freetrie
   HDRS
     freetrie.h
-  SRCS
-    freetrie.cpp
   DEPENDS
     .block
     .freelist
diff --git a/libc/src/__support/freelist.cpp b/libc/src/__support/freelist.cpp
deleted file mode 100644
index 2fdcaadbdb554..0000000000000
--- a/libc/src/__support/freelist.cpp
+++ /dev/null
@@ -1,49 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// 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
-//
-//===----------------------------------------------------------------------===//
-///
-/// \file
-/// Implementation for freelist.
-///
-//===----------------------------------------------------------------------===//
-
-#include "freelist.h"
-
-namespace LIBC_NAMESPACE_DECL {
-
-void FreeList::push(Node *node) {
-  if (begin_) {
-    LIBC_ASSERT(BlockRef::from_usable_space(node).outer_size() ==
-                    begin_->block().outer_size() &&
-                "freelist entries must have the same size");
-    // Since the list is circular, insert the node immediately before begin_.
-    node->prev = begin_->prev;
-    node->next = begin_;
-    begin_->prev->next = node;
-    begin_->prev = node;
-  } else {
-    begin_ = node->prev = node->next = node;
-  }
-}
-
-void FreeList::remove(Node *node) {
-  LIBC_ASSERT(begin_ && "cannot remove from empty list");
-  Node *next = node->next;
-  if (node == next) {
-    LIBC_ASSERT(node == begin_ &&
-                "a self-referential node must be the only element");
-    begin_ = nullptr;
-  } else {
-    Node *prev = node->prev;
-    prev->next = next;
-    next->prev = prev;
-    if (begin_ == node)
-      begin_ = next;
-  }
-}
-
-} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/freelist.h b/libc/src/__support/freelist.h
index 48e70c7c29df6..b61da261c1ec3 100644
--- a/libc/src/__support/freelist.h
+++ b/libc/src/__support/freelist.h
@@ -77,13 +77,38 @@ class FreeList {
 
   /// Push an already-constructed node to the back of the list.
   /// This allows pushing derived node types with additional data.
-  void push(Node *node);
+  LIBC_INLINE void push(Node *node) {
+    if (begin_) {
+      LIBC_ASSERT(BlockRef::from_usable_space(node).outer_size() ==
+                      begin_->block().outer_size() &&
+                  "freelist entries must have the same size");
+      // Since the list is circular, insert the node immediately before begin_.
+      node->prev = begin_->prev;
+      node->next = begin_;
+      begin_->prev->next = node;
+      begin_->prev = node;
+    } else {
+      begin_ = node->prev = node->next = node;
+    }
+  }
 
   /// Pop the first node from the list.
   LIBC_INLINE void pop() { remove(begin_); }
 
   /// Remove an arbitrary node from the list.
-  void remove(Node *node);
+  LIBC_INLINE void remove(Node *node) {
+    LIBC_ASSERT(begin_ && "cannot remove from empty list");
+    if (node == node->next) {
+      LIBC_ASSERT(node == begin_ &&
+                  "a self-referential node must be the only element");
+      begin_ = nullptr;
+    } else {
+      node->prev->next = node->next;
+      node->next->prev = node->prev;
+      if (begin_ == node)
+        begin_ = node->next;
+    }
+  }
 
 private:
   Node *begin_;
diff --git a/libc/src/__support/freetrie.cpp b/libc/src/__support/freetrie.cpp
deleted file mode 100644
index e76efe717f215..0000000000000
--- a/libc/src/__support/freetrie.cpp
+++ /dev/null
@@ -1,64 +0,0 @@
-//===-- Implementation for freetrie ---------------------------------------===//
-//
-// 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
-//
-//===----------------------------------------------------------------------===//
-
-#include "freetrie.h"
-
-namespace LIBC_NAMESPACE_DECL {
-
-void FreeTrie::remove(Node *node) {
-  LIBC_ASSERT(!empty() && "cannot remove from empty trie");
-  FreeList list = node;
-  list.pop();
-  Node *new_node = static_cast<Node *>(list.begin());
-  if (!new_node) {
-    // The freelist is empty. Replace the subtrie root with an arbitrary leaf.
-    // 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)
-      leaf = leaf->lower ? leaf->lower : leaf->upper;
-    if (leaf == node) {
-      // If the root is a leaf, then removing it empties the subtrie.
-      replace_node(node, nullptr);
-      return;
-    }
-
-    replace_node(leaf, nullptr);
-    new_node = leaf;
-  }
-
-  if (!is_head(node))
-    return;
-
-  // Copy the trie links to the new head.
-  new_node->lower = node->lower;
-  new_node->upper = node->upper;
-  new_node->parent = node->parent;
-  replace_node(node, new_node);
-}
-
-void FreeTrie::replace_node(Node *node, Node *new_node) {
-  LIBC_ASSERT(is_head(node) && "only head nodes contain trie links");
-
-  if (node->parent) {
-    Node *&parent_child =
-        node->parent->lower == node ? node->parent->lower : node->parent->upper;
-    LIBC_ASSERT(parent_child == node &&
-                "no reference to child node found in parent");
-    parent_child = new_node;
-  } else {
-    LIBC_ASSERT(root == node && "non-root node had no parent");
-    root = new_node;
-  }
-  if (node->lower)
-    node->lower->parent = new_node;
-  if (node->upper)
-    node->upper->parent = new_node;
-}
-
-} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/freetrie.h b/libc/src/__support/freetrie.h
index 3dbb11eb3a063..78e1940f08769 100644
--- a/libc/src/__support/freetrie.h
+++ b/libc/src/__support/freetrie.h
@@ -263,6 +263,57 @@ LIBC_INLINE FreeTrie::Node *FreeTrie::find_best_fit(size_t size) {
   }
 }
 
+LIBC_INLINE void FreeTrie::remove(Node *node) {
+  LIBC_ASSERT(!empty() && "cannot remove from empty trie");
+  FreeList list = node;
+  list.pop();
+  Node *new_node = static_cast<Node *>(list.begin());
+  if (!new_node) {
+    // The freelist is empty. Replace the subtrie root with an arbitrary leaf.
+    // 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)
+      leaf = leaf->lower ? leaf->lower : leaf->upper;
+    if (leaf == node) {
+      // If the root is a leaf, then removing it empties the subtrie.
+      replace_node(node, nullptr);
+      return;
+    }
+
+    replace_node(leaf, nullptr);
+    new_node = leaf;
+  }
+
+  if (!is_head(node))
+    return;
+
+  // Copy the trie links to the new head.
+  new_node->lower = node->lower;
+  new_node->upper = node->upper;
+  new_node->parent = node->parent;
+  replace_node(node, new_node);
+}
+
+LIBC_INLINE void FreeTrie::replace_node(Node *node, Node *new_node) {
+  LIBC_ASSERT(is_head(node) && "only head nodes contain trie links");
+
+  if (node->parent) {
+    Node *&parent_child =
+        node->parent->lower == node ? node->parent->lower : node->parent->upper;
+    LIBC_ASSERT(parent_child == node &&
+                "no reference to child node found in parent");
+    parent_child = new_node;
+  } else {
+    LIBC_ASSERT(root == node && "non-root node had no parent");
+    root = new_node;
+  }
+  if (node->lower)
+    node->lower->parent = new_node;
+  if (node->upper)
+    node->upper->parent = new_node;
+}
+
 } // namespace LIBC_NAMESPACE_DECL
 
 #endif // LLVM_LIBC_SRC___SUPPORT_FREETRIE_H
diff --git a/libc/test/UnitTest/CMakeLists.txt b/libc/test/UnitTest/CMakeLists.txt
index 4a47597ae3a45..52d8ffa99d70b 100644
--- a/libc/test/UnitTest/CMakeLists.txt
+++ b/libc/test/UnitTest/CMakeLists.txt
@@ -114,9 +114,19 @@ add_unittest_framework_library(
 add_unittest_framework_library(
   LibcHermeticTestSupport
   SRCS
+    HermeticHeap.cpp
     HermeticTestUtils.cpp
   DEPENDS
     libc.hdr.stdint_proxy
+    libc.src.__support.block
+    libc.src.__support.freelist_heap
+    libc.src.__support.CPP.mutex
+    libc.src.__support.CPP.new
+    libc.src.__support.CPP.span
+    libc.src.__support.OSUtil.osutil
+    libc.src.__support.math_extras
+    libc.src.__support.threads.raw_mutex
+    libc.src.string.memory_utils.inline_memset
 )
 
 add_header_library(
diff --git a/libc/test/UnitTest/HermeticHeap.cpp b/libc/test/UnitTest/HermeticHeap.cpp
new file mode 100644
index 0000000000000..f62eac5881b82
--- /dev/null
+++ b/libc/test/UnitTest/HermeticHeap.cpp
@@ -0,0 +1,30 @@
+//===-- Implementation of hermetic heap functions -------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "src/__support/common.h"
+#include "src/__support/macros/config.h"
+#include "test/UnitTest/sbrk_heap.h"
+#include <stddef.h>
+
+namespace LIBC_NAMESPACE_DECL {
+static SbrkHeap hermetic_heap(4096);
+} // namespace LIBC_NAMESPACE_DECL
+
+extern "C" {
+
+void *malloc(size_t s) noexcept {
+  return LIBC_NAMESPACE::hermetic_heap.allocate(s);
+}
+
+void free(void *ptr) noexcept { LIBC_NAMESPACE::hermetic_heap.free(ptr); }
+
+void *realloc(void *mem, size_t s) noexcept {
+  return LIBC_NAMESPACE::hermetic_heap.realloc(mem, s);
+}
+
+} // extern "C"
diff --git a/libc/test/UnitTest/HermeticTestUtils.cpp b/libc/test/UnitTest/HermeticTestUtils.cpp
index 3c82d1e963643..1e3e03be5c25c 100644
--- a/libc/test/UnitTest/HermeticTestUtils.cpp
+++ b/libc/test/UnitTest/HermeticTestUtils.cpp
@@ -35,22 +35,6 @@ int atexit(void (*func)(void));
 
 } // namespace LIBC_NAMESPACE_DECL
 
-constexpr uint64_t ALIGNMENT = alignof(uintptr_t);
-
-namespace {
-
-// Integration tests cannot use the SCUDO standalone allocator as SCUDO pulls
-// various other parts of the libc. Since SCUDO development does not use
-// LLVM libc build rules, it is very hard to keep track or pull all that SCUDO
-// requires. Hence, as a work around for this problem, we use a simple allocator
-// which just hands out continuous blocks from a statically allocated chunk of
-// memory.
-static constexpr uint64_t MEMORY_SIZE = 65336;
-alignas(ALIGNMENT) static uint8_t memory[MEMORY_SIZE];
-static uint8_t *ptr = memory;
-
-} // anonymous namespace
-
 extern "C" {
 
 // Hermetic tests rely on the following memory functions. This is because the
@@ -78,31 +62,9 @@ void *memset(void *ptr, int value, size_t count) {
 // This is needed if the test was compiled with '-fno-use-cxa-atexit'.
 int atexit(void (*func)(void)) { return LIBC_NAMESPACE::atexit(func); }
 
-void *malloc(size_t s) {
-  // Keep the bump pointer aligned on an eight byte boundary.
-  s = ((s + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT;
-  void *mem = ptr;
-  ptr += s;
-  return static_cast<uint64_t>(ptr - memory) >= MEMORY_SIZE ? nullptr : mem;
-}
-
-void free(void *) {}
-
-void *realloc(void *mem, size_t s) {
-  if (mem == nullptr)
-    return malloc(s);
-  uint8_t *newmem = reinterpret_cast<uint8_t *>(malloc(s));
-  if (newmem == nullptr)
-    return nullptr;
-  uint8_t *oldmem = reinterpret_cast<uint8_t *>(mem);
-  // We use a simple for loop to copy the data over.
-  // If |s| is less the previous alloc size, the copy works as expected.
-  // If |s| is greater than the previous alloc size, then garbage is copied
-  // over to the additional part in the new memory block.
-  for (size_t i = 0; i < s; ++i)
-    newmem[i] = oldmem[i];
-  return newmem;
-}
+void *malloc(size_t s) noexcept;
+void free(void *ptr) noexcept;
+void *realloc(void *mem, size_t s) noexcept;
 
 // The unit test framework uses pure virtual functions. Since hermetic tests
 // cannot depend C++ runtime libraries, implement dummy functions to support
diff --git a/libc/test/UnitTest/sbrk_heap.h b/libc/test/UnitTest/sbrk_heap.h
new file mode 100644
index 0000000000000..9310fe3e2f1f5
--- /dev/null
+++ b/libc/test/UnitTest/sbrk_heap.h
@@ -0,0 +1,173 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// A RawMutex protected FreeListHeap backed by sbrk.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_SBRK_HEAP_H
+#define LLVM_LIBC_SRC___SUPPORT_SBRK_HEAP_H
+
+#include <stddef.h>
+
+#include "src/__support/CPP/mutex.h"
+#include "src/__support/CPP/new.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/block.h"
+#include "src/__support/freelist_heap.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/macros/properties/os.h"
+#include "src/__support/math_extras.h"
+#include "src/__support/threads/raw_mutex.h"
+#include "src/string/memory_utils/inline_memset.h"
+
+#if defined(LIBC_TARGET_OS_IS_LINUX)
+#include "src/__support/OSUtil/linux/syscall.h" // syscall_impl
+#include <sys/syscall.h>                        // SYS_brk
+#endif
+
+namespace LIBC_NAMESPACE_DECL {
+
+class SbrkHeap {
+public:
+  LIBC_INLINE constexpr SbrkHeap(size_t initial_size = 4096)
+      : break_ptr(nullptr), current_heap_size(initial_size), heap_storage{},
+        mtx() {}
+
+  LIBC_INLINE void *allocate(size_t size) {
+    cpp::lock_guard lock(mtx);
+    if (LIBC_UNLIKELY(break_ptr == nullptr))
+      if (!grow_heap())
+        return nullptr;
+    void *ptr = heap()->allocate(size);
+    while (ptr == nullptr) {
+      if (!grow_heap())
+        return nullptr;
+      ptr = heap()->allocate(size);
+    }
+    return ptr;
+  }
+
+  LIBC_INLINE void *aligned_allocate(size_t alignment, size_t size) {
+    cpp::lock_guard lock(mtx);
+    if (LIBC_UNLIKELY(break_ptr == nullptr)) {
+      if (!grow_heap())
+        return nullptr;
+    }
+    void *ptr = heap()->aligned_allocate(alignment, size);
+    while (ptr == nullptr) {
+      if (!grow_heap())
+        return nullptr;
+      ptr = heap()->aligned_allocate(alignment, size);
+    }
+    return ptr;
+  }
+
+  LIBC_INLINE void free(void *ptr) {
+    cpp::lock_guard lock(mtx);
+    if (LIBC_LIKELY(break_ptr != nullptr))
+      heap()->free(ptr);
+  }
+
+  LIBC_INLINE void *realloc(void *ptr, size_t size) {
+    cpp::lock_guard lock(mtx);
+    if (LIBC_UNLIKELY(break_ptr == nullptr))
+      if (!grow_heap())
+        return nullptr;
+
+    void *new_ptr = heap()->realloc(ptr, size);
+    while (new_ptr == nullptr && size != 0) {
+      if (!grow_heap())
+        return nullptr;
+      new_ptr = heap()->realloc(ptr, size);
+    }
+    return new_ptr;
+  }
+
+  LIBC_INLINE void *calloc(size_t num, size_t size) {
+    size_t bytes;
+    if (__builtin_mul_overflow(num, size, &bytes))
+      return nullptr;
+    void *ptr = allocate(bytes);
+    if (ptr != nullptr)
+      LIBC_NAMESPACE::inline_memset(ptr, 0, bytes);
+    return ptr;
+  }
+
+  LIBC_INLINE span<cpp::byte> region() const {
+    if (break_ptr == nullptr)
+      return {};
+    return heap()->region();
+  }
+
+private:
+  LIBC_INLINE FreeListHeap *heap() const {
+    return reinterpret_cast<FreeListHeap *>(
+        const_cast<cpp::byte *>(heap_storage));
+  }
+
+  LIBC_INLINE cpp::byte *sbrk(ptrdiff_t increment) {
+#if defined(SYS_brk) && defined(LIBC_TARGET_OS_IS_LINUX)
+    long curr_brk = syscall_impl<long>(SYS_brk, 0);
+    if (curr_brk < 0)
+      return nullptr;
+    if (increment == 0)
+      return reinterpret_cast<cpp::byte *>(curr_brk);
+    long new_brk = syscall_impl<long>(SYS_brk, curr_brk + increment);
+    if (new_brk != curr_brk + increment)
+      return nullptr;
+    return reinterpret_cast<cpp::byte *>(curr_brk);
+#else
+    static constexpr size_t VIRTUAL_HEAP_SIZE = 131072;
+    alignas(FreeListHeap) static cpp::byte virtual_heap[VIRTUAL_HEAP_SIZE];
+    static cpp::byte *virtual_brk = virtual_heap;
+    if (static_cast<size_t>(increment) >
+        VIRTUAL_HEAP_SIZE - static_cast<size_t>(virtual_brk - virtual_heap))
+      return nullptr;
+    cpp::byte *old_brk = virtual_brk;
+    virtual_brk += increment;
+    return old_brk;
+#endif
+  }
+
+  LIBC_INLINE bool grow_heap() {
+    bool first_time = (break_ptr == nullptr);
+    if (first_time) {
+      break_ptr = sbrk(0);
+      if (break_ptr == nullptr)
+        return false;
+    }
+
+    size_t increment = current_heap_size;
+    cpp::byte *new_brk = sbrk(increment);
+    if (new_brk == nullptr || new_brk != break_ptr)
+      return false;
+
+    span<cpp::byte> new_mem(break_ptr, increment);
+    if (first_time)
+      new (heap_storage) FreeListHeap(new_mem);
+    else if (!heap()->adopt(new_mem))
+      return false;
+
+    break_ptr += increment;
+    if (break_ptr != nullptr)
+      current_heap_size += increment;
+    return true;
+  }
+
+  cpp::byte *break_ptr;
+  size_t current_heap_size;
+  alignas(FreeListHeap) cpp::byte heap_storage[sizeof(FreeListHeap)];
+  RawMutex mtx;
+};
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_SBRK_HEAP_H
diff --git a/libc/test/src/__support/CMakeLists.txt b/libc/test/src/__support/CMakeLists.txt
index 8233529266326..60214fbf5eae1 100644
--- a/libc/test/src/__support/CMakeLists.txt
+++ b/libc/test/src/__support/CMakeLists.txt
@@ -71,6 +71,21 @@ if(LLVM_LIBC_FULL_BUILD AND NOT LIBC_TARGET_OS_IS_GPU)
       libc.src.string.memcpy
       libc.src.string.memory_utils.inline_memset
   )
+  add_libc_test(
+    sbrk_heap_test
+    HERMETIC_TEST_ONLY
+    SUITE
+      libc-support-tests
+    SRCS
+      sbrk_heap_test.cpp
+    DEPENDS
+      libc.src.__support.CPP.span
+      libc.src.__support.freelist_heap
+      libc.src.__support.block
+      libc.src.__support.CPP.mutex
+      libc.src.__support.threads.raw_mutex
+      libc.src.string.memory_utils.inline_memset
+  )
 endif()
 
 add_libc_test(
diff --git a/libc/test/src/__support/sbrk_heap_test.cpp b/libc/test/src/__support/sbrk_heap_test.cpp
new file mode 100644
index 0000000000000..a22f74b8b120f
--- /dev/null
+++ b/libc/test/src/__support/sbrk_heap_test.cpp
@@ -0,0 +1,56 @@
+//===-- Unittests for SbrkHeap --------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "test/UnitTest/sbrk_heap.h"
+#include "test/UnitTest/Test.h"
+
+asm(R"(
+.globl _end, __llvm_libc_heap_limit
+
+.bss
+_end:
+  .fill 1024
+__llvm_libc_heap_limit:
+)");
+
+TEST(LlvmLibcSbrkHeapTest, BasicAllocationAndDoubling) {
+  // Start with a very small initial heap (e.g. 512 bytes)
+  LIBC_NAMESPACE::SbrkHeap heap(512);
+
+  // Allocate 300 bytes; this should fit in the initial 512-byte heap.
+  void *ptr1 = heap.allocate(300);
+  EXPECT_NE(ptr1, static_cast<void *>(nullptr));
+
+  // Allocate another 400 bytes; this exceeds the initial 512-byte heap,
+  // triggering SYS_brk growth (doubling the heap by adopting another 512 bytes).
+  void *ptr2 = heap.allocate(400);
+  EXPECT_NE(ptr2, static_cast<void *>(nullptr));
+
+  // Allocate a larger block (2048 bytes), triggering multiple doublings via SYS_brk.
+  void *ptr3 = heap.allocate(2048);
+  EXPECT_NE(ptr3, static_cast<void *>(nullptr));
+
+  heap.free(ptr1);
+  heap.free(ptr2);
+  heap.free(ptr3);
+}
+
+TEST(LlvmLibcSbrkHeapTest, ReallocAndCalloc) {
+  LIBC_NAMESPACE::SbrkHeap heap(1024);
+
+  void *ptr = heap.calloc(10, 100); // 1000 bytes
+  ASSERT_NE(ptr, static_cast<void *>(nullptr));
+  for (int i = 0; i < 1000; ++i)
+    EXPECT_EQ(static_cast<char *>(ptr)[i], char(0));
+
+  // Realloc to a larger size that requires growing the heap via SYS_brk.
+  void *new_ptr = heap.realloc(ptr, 3000);
+  ASSERT_NE(new_ptr, static_cast<void *>(nullptr));
+
+  heap.free(new_ptr);
+}

>From 52bac78081b167fb44c5eb91a6b917fdc85b0f95 Mon Sep 17 00:00:00 2001
From: Yifan Zhu <yfzhu at google.com>
Date: Thu, 16 Jul 2026 13:55:41 -0700
Subject: [PATCH 4/4] [libc] format modified files with clang-format

---
 libc/src/__support/freetrie.h                 | 37 ++++++++++---------
 .../test/src/__support/freelist_heap_test.cpp |  3 +-
 libc/test/src/__support/sbrk_heap_test.cpp    |  8 +++-
 3 files changed, 27 insertions(+), 21 deletions(-)

diff --git a/libc/src/__support/freetrie.h b/libc/src/__support/freetrie.h
index 78e1940f08769..22f0cdec051c3 100644
--- a/libc/src/__support/freetrie.h
+++ b/libc/src/__support/freetrie.h
@@ -130,25 +130,26 @@ LIBC_INLINE void FreeTrie::push(BlockRef block) {
 
   if (!range.contains(size)) {
     if (empty()) {
-      range = SizeRange(
-          range.min,
-          cpp::max(range.width, cpp::bit_ceil(size - range.min + 1)));
+      range =
+          SizeRange(range.min,
+                    cpp::max(range.width, cpp::bit_ceil(size - range.min + 1)));
     } else {
-    // Dynamically expand the trie upwards by doubling the range and creating a
-    // new root node using the pushed block. The previous root becomes the
-    // lower child of the new root.
-    Node *node = new (block.usable_space()) Node;
-    node->parent = nullptr;
-    node->lower = root;
-    node->upper = nullptr;
-    FreeList list;
-    list.push(node);
-    root->parent = node;
-    root = node;
-    range = SizeRange(range.min, range.width * 2);
-    LIBC_ASSERT(range.contains(size) &&
-                "pushed block size exceeds dynamic trie expansion limit (at most 2x current range when non-empty)");
-    return;
+      // Dynamically expand the trie upwards by doubling the range and creating
+      // a new root node using the pushed block. The previous root becomes the
+      // lower child of the new root.
+      Node *node = new (block.usable_space()) Node;
+      node->parent = nullptr;
+      node->lower = root;
+      node->upper = nullptr;
+      FreeList list;
+      list.push(node);
+      root->parent = node;
+      root = node;
+      range = SizeRange(range.min, range.width * 2);
+      LIBC_ASSERT(range.contains(size) &&
+                  "pushed block size exceeds dynamic trie expansion limit (at "
+                  "most 2x current range when non-empty)");
+      return;
     }
   }
 
diff --git a/libc/test/src/__support/freelist_heap_test.cpp b/libc/test/src/__support/freelist_heap_test.cpp
index 273671822b29b..00994a5c2e56b 100644
--- a/libc/test/src/__support/freelist_heap_test.cpp
+++ b/libc/test/src/__support/freelist_heap_test.cpp
@@ -377,7 +377,8 @@ TEST(LlvmLibcFreeListHeap, Adopt) {
   void *ptr1 = allocator.allocate(1500);
   EXPECT_NE(ptr1, static_cast<void *>(nullptr));
 
-  // Initial buffer is now almost full; allocating another 1500 bytes should fail.
+  // Initial buffer is now almost full; allocating another 1500 bytes should
+  // fail.
   void *ptr_fail = allocator.allocate(1500);
   EXPECT_EQ(ptr_fail, static_cast<void *>(nullptr));
 
diff --git a/libc/test/src/__support/sbrk_heap_test.cpp b/libc/test/src/__support/sbrk_heap_test.cpp
index a22f74b8b120f..674c84f03ef4c 100644
--- a/libc/test/src/__support/sbrk_heap_test.cpp
+++ b/libc/test/src/__support/sbrk_heap_test.cpp
@@ -6,8 +6,10 @@
 //
 //===----------------------------------------------------------------------===//
 
+// clang-format off
 #include "test/UnitTest/sbrk_heap.h"
 #include "test/UnitTest/Test.h"
+// clang-format on
 
 asm(R"(
 .globl _end, __llvm_libc_heap_limit
@@ -27,11 +29,13 @@ TEST(LlvmLibcSbrkHeapTest, BasicAllocationAndDoubling) {
   EXPECT_NE(ptr1, static_cast<void *>(nullptr));
 
   // Allocate another 400 bytes; this exceeds the initial 512-byte heap,
-  // triggering SYS_brk growth (doubling the heap by adopting another 512 bytes).
+  // triggering SYS_brk growth (doubling the heap by adopting another 512
+  // bytes).
   void *ptr2 = heap.allocate(400);
   EXPECT_NE(ptr2, static_cast<void *>(nullptr));
 
-  // Allocate a larger block (2048 bytes), triggering multiple doublings via SYS_brk.
+  // Allocate a larger block (2048 bytes), triggering multiple doublings via
+  // SYS_brk.
   void *ptr3 = heap.allocate(2048);
   EXPECT_NE(ptr3, static_cast<void *>(nullptr));
 



More information about the libc-commits mailing list