[libc-commits] [libc] [libc] add option for hardened freelist (PR #205382)
via libc-commits
libc-commits at lists.llvm.org
Wed Jul 1 12:01:28 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-libc
Author: Schrodinger ZHU Yifan (SchrodingerZhu)
<details>
<summary>Changes</summary>
Introduce LIBC_COPT_HARDEN_FREELIST option together with a `santize_heap` interface.
The hardening/sanitization feature encode the prev pointers as `prev ^ K1`; and backward pointers as `ror(prev ^ K2, 17) ^ back ^ k3`.
When hardening option is provided:
- from a security perspective, it makes it harder for attackers to modify the free lists freely.
- for passive sanitization:
- it detects BufferOverFlow as if the BOF happens to modify the freeblock after the allocated region, it alters the encoding and will be detected during some freelist walk
- it detects DF, because DF will modify the backward pointers encoding; when following the original freelist, the encoding error will be detected
- it detects UAF, for similar reasons
The exposed `santize_heap` interface can be called during tearing down or periodically to check the integrity of the heap.
---
Patch is 29.14 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/205382.diff
11 Files Affected:
- (modified) libc/src/__support/CMakeLists.txt (+1)
- (modified) libc/src/__support/freelist.cpp (+46-15)
- (modified) libc/src/__support/freelist.h (+98-6)
- (modified) libc/src/__support/freelist_heap.h (+36-13)
- (modified) libc/src/__support/freestore.h (+25-12)
- (modified) libc/src/__support/freetrie.cpp (+14-2)
- (modified) libc/src/__support/freetrie.h (+8-4)
- (modified) libc/src/__support/macros/attributes.h (+12)
- (modified) libc/test/src/__support/freelist_heap_test.cpp (+28)
- (modified) libc/test/src/__support/freelist_test.cpp (+75-8)
- (modified) libc/test/src/__support/freestore_test.cpp (+26-21)
``````````diff
diff --git a/libc/src/__support/CMakeLists.txt b/libc/src/__support/CMakeLists.txt
index f4cb283976bc3..19d0d95873812 100644
--- a/libc/src/__support/CMakeLists.txt
+++ b/libc/src/__support/CMakeLists.txt
@@ -35,6 +35,7 @@ add_object_library(
.block
libc.src.__support.fixedvector
libc.src.__support.CPP.array
+ libc.src.__support.CPP.bit
libc.src.__support.CPP.cstddef
libc.src.__support.CPP.new
libc.src.__support.CPP.span
diff --git a/libc/src/__support/freelist.cpp b/libc/src/__support/freelist.cpp
index 28f73b2726d7d..430f2cd818e48 100644
--- a/libc/src/__support/freelist.cpp
+++ b/libc/src/__support/freelist.cpp
@@ -15,33 +15,64 @@
namespace LIBC_NAMESPACE_DECL {
-void FreeList::push(Node *node) {
+void FreeList::push(Node *node, const FreeListSecrets &secrets) {
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;
+ Node *begin_prev = secrets.decrypt_prev(begin_, begin_->prev);
+
+ LIBC_HARDENING_ASSERT(secrets.decrypt_next(begin_prev->next) == begin_ &&
+ "Corrupted free list links (push check)");
+
+ node->prev = secrets.encrypt_prev(node, begin_prev);
+ node->next = secrets.encrypt_next(begin_);
+ begin_prev->next = secrets.encrypt_next(node);
+ begin_->prev = secrets.encrypt_prev(begin_, node);
} else {
- begin_ = node->prev = node->next = node;
+ begin_ = node;
+ node->next = secrets.encrypt_next(node);
+ node->prev = secrets.encrypt_prev(node, node);
}
}
-void FreeList::remove(Node *node) {
+void FreeList::remove(Node *node, const FreeListSecrets &secrets) {
LIBC_ASSERT(begin_ && "cannot remove from empty list");
- if (node == node->next) {
+ Node *node_next = secrets.decrypt_next(node->next);
+ 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;
+ Node *node_prev = secrets.decrypt_prev(node, node->prev);
+
+ LIBC_HARDENING_ASSERT(
+ secrets.decrypt_next(node_prev->next) == node &&
+ "Corrupted free list links (remove check prev->next)");
+ LIBC_HARDENING_ASSERT(
+ secrets.decrypt_prev(node_next, node_next->prev) == node &&
+ "Corrupted free list links (remove check next->prev)");
+
+ node_prev->next = secrets.encrypt_next(node_next);
+ node_next->prev = secrets.encrypt_prev(node_next, node_prev);
if (begin_ == node)
- begin_ = node->next;
+ begin_ = node_next;
}
}
+void FreeList::sanitize(const FreeListSecrets &secrets) const {
+ if (!begin_)
+ return;
+ Node *curr = begin_;
+ do {
+ Node *next_node = secrets.decrypt_next(curr->next);
+ Node *prev_node = secrets.decrypt_prev(curr, curr->prev);
+ (void)prev_node;
+ LIBC_HARDENING_ASSERT(
+ secrets.decrypt_next(prev_node->next) == curr &&
+ "Corrupted free list links (sanitize check prev->next)");
+ LIBC_HARDENING_ASSERT(
+ secrets.decrypt_prev(next_node, next_node->prev) == curr &&
+ "Corrupted free list links (sanitize check next->prev)");
+ curr = next_node;
+ } while (curr != begin_);
+}
+
} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/freelist.h b/libc/src/__support/freelist.h
index 48e70c7c29df6..70e91305c4554 100644
--- a/libc/src/__support/freelist.h
+++ b/libc/src/__support/freelist.h
@@ -15,9 +15,86 @@
#define LLVM_LIBC_SRC___SUPPORT_FREELIST_H
#include "block.h"
+#include "hdr/stdint_proxy.h"
+#include "src/__support/CPP/bit.h"
+#include "src/__support/libc_assert.h"
+#include "src/__support/macros/config.h"
+
+#ifndef LIBC_COPT_HARDEN_FREELIST
+#define LIBC_COPT_HARDEN_FREELIST false
+#endif
+
+#if LIBC_COPT_HARDEN_FREELIST
+#define LIBC_HARDENING_ASSERT(cond) \
+ do { \
+ if (LIBC_UNLIKELY(!(cond))) { \
+ __builtin_trap(); \
+ } \
+ } while (0)
+#else
+#define LIBC_HARDENING_ASSERT(cond) LIBC_ASSERT(cond)
+#endif
namespace LIBC_NAMESPACE_DECL {
+struct FreeListSecrets {
+ static constexpr int NODE_PTR_ROTATE_DISTANCE = 17;
+
+#if LIBC_COPT_HARDEN_FREELIST
+ uintptr_t k0;
+ uintptr_t k1;
+ uintptr_t k2;
+
+ LIBC_INLINE constexpr FreeListSecrets(uintptr_t k0, uintptr_t k1,
+ uintptr_t k2)
+ : k0(k0), k1(k1), k2(k2) {}
+ LIBC_INLINE constexpr FreeListSecrets() : k0(0), k1(0), k2(0) {}
+#else
+ LIBC_INLINE constexpr FreeListSecrets() = default;
+#endif
+
+ template <typename T> LIBC_INLINE T *decrypt_next(T *next_val) const {
+#if LIBC_COPT_HARDEN_FREELIST
+ return reinterpret_cast<T *>(reinterpret_cast<uintptr_t>(next_val) ^ k0);
+#else
+ return next_val;
+#endif
+ }
+
+ template <typename T>
+ LIBC_INLINE T *decrypt_prev([[maybe_unused]] const void *node,
+ T *prev_val) const {
+#if LIBC_COPT_HARDEN_FREELIST
+ uintptr_t val = reinterpret_cast<uintptr_t>(prev_val) ^ k2 ^
+ reinterpret_cast<uintptr_t>(node);
+ val = cpp::rotl(val, NODE_PTR_ROTATE_DISTANCE);
+ return reinterpret_cast<T *>(val ^ k1);
+#else
+ return prev_val;
+#endif
+ }
+
+ template <typename T> LIBC_INLINE T *encrypt_next(T *next_val) const {
+#if LIBC_COPT_HARDEN_FREELIST
+ return reinterpret_cast<T *>(reinterpret_cast<uintptr_t>(next_val) ^ k0);
+#else
+ return next_val;
+#endif
+ }
+
+ template <typename T>
+ LIBC_INLINE T *encrypt_prev([[maybe_unused]] const void *node,
+ T *prev_val) const {
+#if LIBC_COPT_HARDEN_FREELIST
+ uintptr_t val = reinterpret_cast<uintptr_t>(prev_val) ^ k1;
+ val = cpp::rotr(val, NODE_PTR_ROTATE_DISTANCE);
+ return reinterpret_cast<T *>(val ^ reinterpret_cast<uintptr_t>(node) ^ k2);
+#else
+ return prev_val;
+#endif
+ }
+};
+
/// A circularly-linked FIFO list storing free Blocks. All Blocks on a list
/// are the same size. The blocks are referenced by Nodes in the list; the list
/// refers to these, but it does not own them.
@@ -65,25 +142,40 @@ class FreeList {
/// @returns The first block in the list.
LIBC_INLINE BlockRef front() { return begin_->block(); }
+ LIBC_INLINE Node *next_node(const Node *node,
+ const FreeListSecrets &secrets) const {
+ return node ? secrets.decrypt_next(node->next) : nullptr;
+ }
+
+ LIBC_INLINE Node *prev_node(const Node *node,
+ const FreeListSecrets &secrets) const {
+ return node ? secrets.decrypt_prev(node, node->prev) : nullptr;
+ }
+
/// Push a block to the back of the list.
/// The block must be large enough to contain a node.
- LIBC_INLINE void push(BlockRef block) {
+ LIBC_INLINE void push(BlockRef block, const FreeListSecrets &secrets) {
LIBC_ASSERT(!block.used() &&
"only free blocks can be placed on free lists");
- LIBC_ASSERT(block.inner_size_free() >= sizeof(FreeList) &&
+ LIBC_ASSERT(block.inner_size_free() >= sizeof(Node) &&
"block too small to accomodate free list node");
- push(new (block.usable_space()) Node);
+ push(new (block.usable_space()) Node, secrets);
}
/// Push an already-constructed node to the back of the list.
/// This allows pushing derived node types with additional data.
- void push(Node *node);
+ void push(Node *node, const FreeListSecrets &secrets);
/// Pop the first node from the list.
- LIBC_INLINE void pop() { remove(begin_); }
+ LIBC_INLINE void pop(const FreeListSecrets &secrets) {
+ remove(begin_, secrets);
+ }
/// Remove an arbitrary node from the list.
- void remove(Node *node);
+ void remove(Node *node, const FreeListSecrets &secrets);
+
+ /// Verify secret invariants for all nodes in the list.
+ void sanitize(const FreeListSecrets &secrets) const;
private:
Node *begin_;
diff --git a/libc/src/__support/freelist_heap.h b/libc/src/__support/freelist_heap.h
index a591d63ddd4e5..46fc0083a17b7 100644
--- a/libc/src/__support/freelist_heap.h
+++ b/libc/src/__support/freelist_heap.h
@@ -21,6 +21,7 @@
#include "src/__support/CPP/optional.h"
#include "src/__support/CPP/span.h"
#include "src/__support/libc_assert.h"
+#include "src/__support/macros/attributes.h"
#include "src/__support/macros/config.h"
#include "src/__support/math_extras.h"
#include "src/string/memory_utils/inline_memcpy.h"
@@ -53,9 +54,15 @@ class FreeListHeap {
cpp::span<cpp::byte> region() const { return {begin, end}; }
-private:
- void init();
+#if LIBC_COPT_HARDEN_FREELIST
+ void init(const FreeListSecrets &secrets);
+#else
+ void init(const FreeListSecrets &secrets = {});
+#endif
+
+ void sanitize_heap() const;
+private:
void *allocate_impl(size_t alignment, size_t size);
span<cpp::byte> block_to_span(BlockRef block) {
@@ -70,6 +77,7 @@ class FreeListHeap {
cpp::byte *end;
bool is_initialized = false;
FreeStore free_store;
+ LIBC_NO_UNIQUE_ADDRESS FreeListSecrets secrets;
};
template <size_t BUFF_SIZE> class FreeListHeapBuffer : public FreeListHeap {
@@ -80,12 +88,14 @@ template <size_t BUFF_SIZE> class FreeListHeapBuffer : public FreeListHeap {
cpp::byte buffer[BUFF_SIZE];
};
-LIBC_INLINE void FreeListHeap::init() {
+[[gnu::noinline]] LIBC_INLINE void
+FreeListHeap::init(const FreeListSecrets &secrets_arg) {
LIBC_ASSERT(!is_initialized && "duplicate initialization");
+ secrets = secrets_arg;
auto result = BlockRef::init(region());
BlockRef block = *result;
free_store.set_range({0, cpp::bit_ceil(block.inner_size())});
- free_store.insert(block);
+ free_store.insert(block, secrets);
is_initialized = true;
}
@@ -93,22 +103,29 @@ LIBC_INLINE void *FreeListHeap::allocate_impl(size_t alignment, size_t size) {
if (size == 0)
return nullptr;
- if (!is_initialized)
+ if (!is_initialized) {
+#if LIBC_COPT_HARDEN_FREELIST
+ LIBC_HARDENING_ASSERT(
+ false &&
+ "Hardened heap must be explicitly initialized via init(secrets)");
+#else
init();
+#endif
+ }
size_t request_size = BlockRef::min_size_for_allocation(alignment, size);
if (!request_size)
return nullptr;
- BlockRef block = free_store.remove_best_fit(request_size);
+ BlockRef block = free_store.remove_best_fit(request_size, secrets);
if (!block)
return nullptr;
auto block_info = BlockRef::allocate(block, alignment, size);
if (block_info.next)
- free_store.insert(block_info.next);
+ free_store.insert(block_info.next, secrets);
if (block_info.prev)
- free_store.insert(block_info.prev);
+ free_store.insert(block_info.prev, secrets);
block_info.block.mark_used();
return block_info.block.usable_space();
@@ -153,16 +170,16 @@ LIBC_INLINE void FreeListHeap::free(void *ptr) {
if (prev_free) {
// Remove from free store and merge.
- free_store.remove(prev_free);
+ free_store.remove(prev_free, secrets);
block = prev_free;
block.merge_next();
}
if (!next.used()) {
- free_store.remove(next);
+ free_store.remove(next, secrets);
block.merge_next();
}
// Add back to the freelist
- free_store.insert(block);
+ free_store.insert(block, secrets);
}
LIBC_INLINE bool FreeListHeap::shrink_in_place(BlockRef block, size_t size) {
@@ -182,10 +199,10 @@ LIBC_INLINE bool FreeListHeap::shrink_in_place(BlockRef block, size_t size) {
// to be non-null.
LIBC_ASSERT(right && "right block must be non-null");
if (!right.used()) {
- free_store.remove(right);
+ free_store.remove(right, secrets);
next_block.merge_next();
}
- free_store.insert(next_block);
+ free_store.insert(next_block, secrets);
}
return true;
}
@@ -239,6 +256,12 @@ LIBC_INLINE void *FreeListHeap::calloc(size_t num, size_t size) {
return ptr;
}
+LIBC_INLINE void FreeListHeap::sanitize_heap() const {
+ if (!is_initialized)
+ return;
+ free_store.sanitize(secrets);
+}
+
extern FreeListHeap *freelist_heap;
} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/__support/freestore.h b/libc/src/__support/freestore.h
index adc0e061ace93..808fcc48b0617 100644
--- a/libc/src/__support/freestore.h
+++ b/libc/src/__support/freestore.h
@@ -36,15 +36,18 @@ class FreeStore {
/// Insert a free block. If the block is too small to be tracked, nothing
/// happens.
- void insert(BlockRef block);
+ void insert(BlockRef block, const FreeListSecrets &secrets);
/// Remove a free block. If the block is too small to be tracked, nothing
/// happens.
- void remove(BlockRef block);
+ void remove(BlockRef block, const FreeListSecrets &secrets);
/// Remove a best-fit free block that can contain the given size when
/// allocated. Returns nullptr if there is no such block.
- BlockRef remove_best_fit(size_t size);
+ BlockRef remove_best_fit(size_t size, const FreeListSecrets &secrets);
+
+ /// Verify secret invariants for all free lists in the store.
+ void sanitize(const FreeListSecrets &secrets) const;
private:
static constexpr size_t MIN_OUTER_SIZE = align_up(
@@ -68,35 +71,39 @@ class FreeStore {
FreeTrie large_trie;
};
-LIBC_INLINE void FreeStore::insert(BlockRef block) {
+LIBC_INLINE void FreeStore::insert(BlockRef block,
+ const FreeListSecrets &secrets) {
if (too_small(block))
return;
if (is_small(block))
- small_list(block).push(block);
+ small_list(block).push(block, secrets);
else
- large_trie.push(block);
+ large_trie.push(block, secrets);
}
-LIBC_INLINE void FreeStore::remove(BlockRef block) {
+LIBC_INLINE void FreeStore::remove(BlockRef block,
+ const FreeListSecrets &secrets) {
if (too_small(block))
return;
if (is_small(block)) {
small_list(block).remove(
- reinterpret_cast<FreeList::Node *>(block.usable_space()));
+ reinterpret_cast<FreeList::Node *>(block.usable_space()), secrets);
} else {
- large_trie.remove(reinterpret_cast<FreeTrie::Node *>(block.usable_space()));
+ large_trie.remove(reinterpret_cast<FreeTrie::Node *>(block.usable_space()),
+ secrets);
}
}
-LIBC_INLINE BlockRef FreeStore::remove_best_fit(size_t size) {
+LIBC_INLINE BlockRef
+FreeStore::remove_best_fit(size_t size, const FreeListSecrets &secrets) {
if (FreeList *list = find_best_small_fit(size)) {
BlockRef block = list->front();
- list->pop();
+ list->pop(secrets);
return block;
}
if (FreeTrie::Node *best_fit = large_trie.find_best_fit(size)) {
BlockRef block = best_fit->block();
- large_trie.remove(best_fit);
+ large_trie.remove(best_fit, secrets);
return block;
}
return BlockRef();
@@ -115,6 +122,12 @@ LIBC_INLINE FreeList *FreeStore::find_best_small_fit(size_t size) {
return nullptr;
}
+LIBC_INLINE void FreeStore::sanitize(const FreeListSecrets &secrets) const {
+ for (const FreeList &list : small_lists)
+ list.sanitize(secrets);
+ large_trie.sanitize(secrets);
+}
+
} // namespace LIBC_NAMESPACE_DECL
#endif // LLVM_LIBC_SRC___SUPPORT_FREESTORE_H
diff --git a/libc/src/__support/freetrie.cpp b/libc/src/__support/freetrie.cpp
index e76efe717f215..056a79b0b4f8a 100644
--- a/libc/src/__support/freetrie.cpp
+++ b/libc/src/__support/freetrie.cpp
@@ -10,10 +10,10 @@
namespace LIBC_NAMESPACE_DECL {
-void FreeTrie::remove(Node *node) {
+void FreeTrie::remove(Node *node, const FreeListSecrets &secrets) {
LIBC_ASSERT(!empty() && "cannot remove from empty trie");
FreeList list = node;
- list.pop();
+ list.pop(secrets);
Node *new_node = static_cast<Node *>(list.begin());
if (!new_node) {
// The freelist is empty. Replace the subtrie root with an arbitrary leaf.
@@ -61,4 +61,16 @@ void FreeTrie::replace_node(Node *node, Node *new_node) {
node->upper->parent = new_node;
}
+void FreeTrie::sanitize(const FreeListSecrets &secrets) const {
+ auto sanitize_trie_node = [&](auto &self, const Node *node) -> void {
+ if (!node)
+ return;
+ FreeList list = const_cast<Node *>(node);
+ list.sanitize(secrets);
+ 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..31c62543b04a5 100644
--- a/libc/src/__support/freetrie.h
+++ b/libc/src/__support/freetrie.h
@@ -101,15 +101,18 @@ class FreeTrie {
LIBC_INLINE bool empty() const { return !root; }
/// Push a block to the trie.
- void push(BlockRef block);
+ void push(BlockRef block, const FreeListSecrets &secrets);
/// Remove a node from this trie node's free list.
- void remove(Node *node);
+ void remove(Node *node, const FreeListSecrets &secrets);
/// @returns A smallest node that can allocate the given size; otherwise
/// nullptr.
Node *find_best_fit(size_t size);
+ /// Verify secret invariants for all free lists in the trie.
+ void sanitize(const FreeListSecrets &secrets) const;
+
private:
/// @returns Whether a node is the head of its containing freelist.
bool is_head(Node *node) const { return node->parent || node == root; }
@@ -122,7 +125,8 @@ class FreeTrie {
SizeRange range;
};
-LIBC_INLINE void FreeTrie::push(BlockRef block) {
+LIBC_INLINE void FreeTrie::push(BlockRef block,
+ const FreeListSecrets &secrets) {
LIBC_ASSERT(block.inner_size_free() >= sizeof(Node) &&
"block too small to accomodate free trie node");
size_t size = block.inner_size();
@@ -152,7 +156,7 @@ LIBC_INLINE void FreeTrie::push(BlockRef block) {
} else {
node->parent = nullptr;
}
- list.push(node);
+ list.push(node, secrets);
*cur = static_cast<Node *>(list.begin());
}
diff --git a/libc/src/__support/macros/attributes.h b/libc/src/__support/macros/attributes.h
index cffcabcd29bd3..64012b415f235 100644
--- a/libc/src/__support/macros/attributes.h
+++ b/libc/src/__support/macros/attributes.h
@@ -165,4 +165,16 @@ LIBC_THREAD_MODE_EXTERNAL.
#define LIBC_NO_SANITIZE_OOB_ACCESS
#endif
+#if defined(__has_cpp_attribute)
+#if __has_cpp_attribute(msvc::no_unique_address)
+#define LIBC_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]]
+#elif __has_cpp_attribute(no_unique_address)
+#define LIBC_NO_UNIQUE_ADDRESS [[no_unique_address]]
+#endif
+#endif
+
+#ifndef LIBC_NO_UNIQUE_ADDRESS
+#define LIBC_NO_UNIQUE_ADDRESS
+#endif
+
#endif // LLVM_LIBC_SRC___SUPPORT_MACROS_ATTRIBUTES_H
diff --git a/libc/test/src/__support/freelist_heap_test.cpp b/libc/test/src/__support/freelist_heap_test.cpp
index 68cc30152cd6d..57085275de7bb 100644
--- a/libc/test/src/__support/freelist_heap_test.cpp
+++ b/libc/test/src/__support/freelist_heap_test.cpp
@@ -32,9 +32,17 @@ using LIBC_NAMESPACE::BlockRef;
using LIBC_NAMESPACE::freelist_heap;
using LIBC_NAMESPACE::FreeListHeap;
using LIBC_NAMESPACE::FreeListHeapBuffer;
+using LIBC_NAMESPACE::FreeListSecrets;
using LIBC_NAMESPACE::cpp::byte;
using LIBC_NAMESPACE::cpp::span;
+#if LIBC_COPT_HARDEN_FREELIST
+#define TEST_SECRETS_INIT FreeListSecrets{0x123, 0x456, 0x789}
+#else
+#define TEST_SECRETS_INIT \
+ FreeListSecrets {}
+#endif
+
// Similar to `LlvmLibcBlockTest` in block_test.cpp, we'd like to run the same
// tests independently for different parameters. In this case, we'd like to test
// functionality for a `FreeListHeap` and the global `freelist_heap` which was
@@ -53,12 +61,14 @@ using LIBC_NAMESPACE::cpp::span;
void SetUp() override { \
freelist_heap = \
new (&fake_global_buffer) FreeListHeapBuffer<BufferSize>; \
+ ...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/205382
More information about the libc-commits
mailing list