Skip to content

Commit 385961d

Browse files
committed
Reapply "[libc] Use best-fit binary trie to make malloc logarithmic (#117065)"
This reverts commit 93b8364. - Correct riscv32 assumption about alignment (bit of a hack). - Fix test case where the largest_small and smallest sizes are the same.
1 parent 4862feb commit 385961d

18 files changed

+1198
-472
lines changed

libc/fuzzing/__support/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,11 @@ add_libc_fuzzer(
2323
COMPILE_OPTIONS
2424
-D__LIBC_EXPLICIT_SIMD_OPT
2525
)
26+
27+
add_libc_fuzzer(
28+
freelist_heap_fuzz
29+
SRCS
30+
freelist_heap_fuzz.cpp
31+
DEPENDS
32+
libc.src.__support.freelist_heap
33+
)
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
//===-- freelist_heap_fuzz.cpp --------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
///
9+
/// Fuzzing test for llvm-libc freelist-based heap implementation.
10+
///
11+
//===----------------------------------------------------------------------===//
12+
13+
#include "src/__support/CPP/bit.h"
14+
#include "src/__support/CPP/optional.h"
15+
#include "src/__support/freelist_heap.h"
16+
#include "src/string/memory_utils/inline_memcpy.h"
17+
#include "src/string/memory_utils/inline_memmove.h"
18+
#include "src/string/memory_utils/inline_memset.h"
19+
20+
using LIBC_NAMESPACE::FreeListHeap;
21+
using LIBC_NAMESPACE::inline_memset;
22+
using LIBC_NAMESPACE::cpp::nullopt;
23+
using LIBC_NAMESPACE::cpp::optional;
24+
25+
// Record of an outstanding allocation.
26+
struct Alloc {
27+
void *ptr;
28+
size_t size;
29+
size_t alignment;
30+
uint8_t canary; // Byte written to the allocation
31+
};
32+
33+
// A simple vector that tracks allocations using the heap.
34+
class AllocVec {
35+
public:
36+
AllocVec(FreeListHeap &heap) : heap(&heap), size_(0), capacity(0) {
37+
allocs = nullptr;
38+
}
39+
40+
bool empty() const { return !size_; }
41+
42+
size_t size() const { return size_; }
43+
44+
bool push_back(Alloc alloc) {
45+
if (size_ == capacity) {
46+
size_t new_cap = capacity ? capacity * 2 : 1;
47+
Alloc *new_allocs = reinterpret_cast<Alloc *>(
48+
heap->realloc(allocs, new_cap * sizeof(Alloc)));
49+
if (!new_allocs)
50+
return false;
51+
allocs = new_allocs;
52+
capacity = new_cap;
53+
}
54+
allocs[size_++] = alloc;
55+
return true;
56+
}
57+
58+
Alloc &operator[](size_t idx) { return allocs[idx]; }
59+
60+
void erase_idx(size_t idx) {
61+
LIBC_NAMESPACE::inline_memmove(&allocs[idx], &allocs[idx + 1],
62+
sizeof(Alloc) * (size_ - idx - 1));
63+
--size_;
64+
}
65+
66+
private:
67+
FreeListHeap *heap;
68+
Alloc *allocs;
69+
size_t size_;
70+
size_t capacity;
71+
};
72+
73+
// Choose a T value by casting libfuzzer data or exit.
74+
template <typename T>
75+
optional<T> choose(const uint8_t *&data, size_t &remainder) {
76+
if (sizeof(T) > remainder)
77+
return nullopt;
78+
T out;
79+
LIBC_NAMESPACE::inline_memcpy(&out, data, sizeof(T));
80+
data += sizeof(T);
81+
remainder -= sizeof(T);
82+
return out;
83+
}
84+
85+
// The type of allocation to perform
86+
enum class AllocType : uint8_t {
87+
MALLOC,
88+
ALIGNED_ALLOC,
89+
REALLOC,
90+
CALLOC,
91+
NUM_ALLOC_TYPES,
92+
};
93+
94+
template <>
95+
optional<AllocType> choose<AllocType>(const uint8_t *&data, size_t &remainder) {
96+
auto raw = choose<uint8_t>(data, remainder);
97+
if (!raw)
98+
return nullopt;
99+
return static_cast<AllocType>(
100+
*raw % static_cast<uint8_t>(AllocType::NUM_ALLOC_TYPES));
101+
}
102+
103+
constexpr size_t heap_size = 64 * 1024;
104+
105+
optional<size_t> choose_size(const uint8_t *&data, size_t &remainder) {
106+
auto raw = choose<size_t>(data, remainder);
107+
if (!raw)
108+
return nullopt;
109+
return *raw % heap_size;
110+
}
111+
112+
optional<size_t> choose_alloc_idx(const AllocVec &allocs, const uint8_t *&data,
113+
size_t &remainder) {
114+
if (allocs.empty())
115+
return nullopt;
116+
auto raw = choose<size_t>(data, remainder);
117+
if (!raw)
118+
return nullopt;
119+
return *raw % allocs.size();
120+
}
121+
122+
#define ASSIGN_OR_RETURN(TYPE, NAME, EXPR) \
123+
auto maybe_##NAME = EXPR; \
124+
if (!maybe_##NAME) \
125+
return 0; \
126+
TYPE NAME = *maybe_##NAME
127+
128+
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t remainder) {
129+
LIBC_NAMESPACE::FreeListHeapBuffer<heap_size> heap;
130+
AllocVec allocs(heap);
131+
132+
uint8_t canary = 0;
133+
while (true) {
134+
ASSIGN_OR_RETURN(auto, should_alloc, choose<bool>(data, remainder));
135+
if (should_alloc) {
136+
ASSIGN_OR_RETURN(auto, alloc_type, choose<AllocType>(data, remainder));
137+
ASSIGN_OR_RETURN(size_t, alloc_size, choose_size(data, remainder));
138+
139+
// Perform allocation.
140+
void *ptr = nullptr;
141+
size_t alignment = alignof(max_align_t);
142+
switch (alloc_type) {
143+
case AllocType::MALLOC:
144+
ptr = heap.allocate(alloc_size);
145+
break;
146+
case AllocType::ALIGNED_ALLOC: {
147+
ASSIGN_OR_RETURN(size_t, alignment, choose_size(data, remainder));
148+
alignment = LIBC_NAMESPACE::cpp::bit_ceil(alignment);
149+
ptr = heap.aligned_allocate(alignment, alloc_size);
150+
break;
151+
}
152+
case AllocType::REALLOC: {
153+
if (!alloc_size)
154+
return 0;
155+
ASSIGN_OR_RETURN(size_t, idx,
156+
choose_alloc_idx(allocs, data, remainder));
157+
Alloc &alloc = allocs[idx];
158+
ptr = heap.realloc(alloc.ptr, alloc_size);
159+
if (ptr) {
160+
// Extend the canary region if necessary.
161+
if (alloc_size > alloc.size)
162+
inline_memset(static_cast<char *>(ptr) + alloc.size, alloc.canary,
163+
alloc_size - alloc.size);
164+
alloc.ptr = ptr;
165+
alloc.size = alloc_size;
166+
alloc.alignment = alignof(max_align_t);
167+
}
168+
break;
169+
}
170+
case AllocType::CALLOC: {
171+
ASSIGN_OR_RETURN(size_t, count, choose_size(data, remainder));
172+
size_t total;
173+
if (__builtin_mul_overflow(count, alloc_size, &total))
174+
return 0;
175+
ptr = heap.calloc(count, alloc_size);
176+
if (ptr)
177+
for (size_t i = 0; i < total; ++i)
178+
if (static_cast<char *>(ptr)[i] != 0)
179+
__builtin_trap();
180+
break;
181+
}
182+
case AllocType::NUM_ALLOC_TYPES:
183+
__builtin_unreachable();
184+
}
185+
186+
if (ptr) {
187+
// aligned_allocate should automatically apply a minimum alignment.
188+
if (alignment < alignof(max_align_t))
189+
alignment = alignof(max_align_t);
190+
// Check alignment.
191+
if (reinterpret_cast<uintptr_t>(ptr) % alignment)
192+
__builtin_trap();
193+
194+
// Reallocation is treated specially above, since we would otherwise
195+
// lose the original size.
196+
if (alloc_type != AllocType::REALLOC) {
197+
// Fill the object with a canary byte.
198+
inline_memset(ptr, canary, alloc_size);
199+
200+
// Track the allocation.
201+
if (!allocs.push_back({ptr, alloc_size, alignment, canary}))
202+
return 0;
203+
++canary;
204+
}
205+
}
206+
} else {
207+
// Select a random allocation.
208+
ASSIGN_OR_RETURN(size_t, idx, choose_alloc_idx(allocs, data, remainder));
209+
Alloc &alloc = allocs[idx];
210+
211+
// Check alignment.
212+
if (reinterpret_cast<uintptr_t>(alloc.ptr) % alloc.alignment)
213+
__builtin_trap();
214+
215+
// Check the canary.
216+
uint8_t *ptr = reinterpret_cast<uint8_t *>(alloc.ptr);
217+
for (size_t i = 0; i < alloc.size; ++i)
218+
if (ptr[i] != alloc.canary)
219+
__builtin_trap();
220+
221+
// Free the allocation and untrack it.
222+
heap.free(alloc.ptr);
223+
allocs.erase_idx(idx);
224+
}
225+
}
226+
return 0;
227+
}

libc/src/__support/CMakeLists.txt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,47 @@ add_header_library(
1414
libc.src.__support.CPP.type_traits
1515
)
1616

17-
add_header_library(
17+
add_object_library(
1818
freelist
1919
HDRS
2020
freelist.h
21+
SRCS
22+
freelist.cpp
2123
DEPENDS
24+
.block
2225
libc.src.__support.fixedvector
2326
libc.src.__support.CPP.array
2427
libc.src.__support.CPP.cstddef
2528
libc.src.__support.CPP.new
2629
libc.src.__support.CPP.span
2730
)
2831

32+
add_object_library(
33+
freetrie
34+
HDRS
35+
freetrie.h
36+
SRCS
37+
freetrie.cpp
38+
DEPENDS
39+
.block
40+
.freelist
41+
)
42+
43+
add_header_library(
44+
freestore
45+
HDRS
46+
freestore.h
47+
DEPENDS
48+
.freetrie
49+
)
50+
2951
add_header_library(
3052
freelist_heap
3153
HDRS
3254
freelist_heap.h
3355
DEPENDS
3456
.block
35-
.freelist
57+
.freestore
3658
libc.src.__support.CPP.cstddef
3759
libc.src.__support.CPP.array
3860
libc.src.__support.CPP.optional

libc/src/__support/block.h

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,16 +174,32 @@ class Block {
174174
return inner_size - sizeof(prev_) + BLOCK_OVERHEAD;
175175
}
176176

177-
/// @returns The number of usable bytes inside the block.
177+
/// @returns The number of usable bytes inside the block were it to be
178+
/// allocated.
178179
size_t inner_size() const {
179180
if (!next())
180181
return 0;
181182
return inner_size(outer_size());
182183
}
183184

185+
/// @returns The number of usable bytes inside a block with the given outer
186+
/// size were it to be allocated.
184187
static size_t inner_size(size_t outer_size) {
185188
// The usable region includes the prev_ field of the next block.
186-
return outer_size - BLOCK_OVERHEAD + sizeof(prev_);
189+
return inner_size_free(outer_size) + sizeof(prev_);
190+
}
191+
192+
/// @returns The number of usable bytes inside the block if it remains free.
193+
size_t inner_size_free() const {
194+
if (!next())
195+
return 0;
196+
return inner_size_free(outer_size());
197+
}
198+
199+
/// @returns The number of usable bytes inside a block with the given outer
200+
/// size if it remains free.
201+
static size_t inner_size_free(size_t outer_size) {
202+
return outer_size - BLOCK_OVERHEAD;
187203
}
188204

189205
/// @returns A pointer to the usable space inside this block.
@@ -201,14 +217,11 @@ class Block {
201217

202218
/// Attempts to split this block.
203219
///
204-
/// If successful, the block will have an inner size of `new_inner_size`,
205-
/// rounded to ensure that the split point is on an ALIGNMENT boundary. The
206-
/// remaining space will be returned as a new block. Note that the prev_ field
207-
/// of the next block counts as part of the inner size of the returnd block.
208-
///
209-
/// This method may fail if the remaining space is too small to hold a new
210-
/// block. If this method fails for any reason, the original block is
211-
/// unmodified.
220+
/// If successful, the block will have an inner size of at least
221+
/// `new_inner_size`, rounded to ensure that the split point is on an
222+
/// ALIGNMENT boundary. The remaining space will be returned as a new block.
223+
/// Note that the prev_ field of the next block counts as part of the inner
224+
/// size of the returnd block.
212225
optional<Block *> split(size_t new_inner_size);
213226

214227
/// Merges this block with the one that comes after it.
@@ -442,7 +455,7 @@ Block<OffsetType, kAlign>::split(size_t new_inner_size) {
442455
// The prev_ field of the next block is always available, so there is a
443456
// minimum size to a block created through splitting.
444457
if (new_inner_size < sizeof(prev_))
445-
return {};
458+
new_inner_size = sizeof(prev_);
446459

447460
size_t old_inner_size = inner_size();
448461
new_inner_size =

libc/src/__support/freelist.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===-- Implementation for freelist ---------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "freelist.h"
10+
11+
namespace LIBC_NAMESPACE_DECL {
12+
13+
void FreeList::push(Node *node) {
14+
if (begin_) {
15+
LIBC_ASSERT(Block<>::from_usable_space(node)->outer_size() ==
16+
begin_->block()->outer_size() &&
17+
"freelist entries must have the same size");
18+
// Since the list is circular, insert the node immediately before begin_.
19+
node->prev = begin_->prev;
20+
node->next = begin_;
21+
begin_->prev->next = node;
22+
begin_->prev = node;
23+
} else {
24+
begin_ = node->prev = node->next = node;
25+
}
26+
}
27+
28+
void FreeList::remove(Node *node) {
29+
LIBC_ASSERT(begin_ && "cannot remove from empty list");
30+
if (node == node->next) {
31+
LIBC_ASSERT(node == begin_ &&
32+
"a self-referential node must be the only element");
33+
begin_ = nullptr;
34+
} else {
35+
node->prev->next = node->next;
36+
node->next->prev = node->prev;
37+
if (begin_ == node)
38+
begin_ = node->next;
39+
}
40+
}
41+
42+
} // namespace LIBC_NAMESPACE_DECL

0 commit comments

Comments
 (0)