Skip to content

Commit e4c444a

Browse files
committed
Add a fuzzing test
1 parent e94edb3 commit e4c444a

File tree

7 files changed

+259
-33
lines changed

7 files changed

+259
-33
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: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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 namespace LIBC_NAMESPACE;
21+
22+
struct Alloc {
23+
void *ptr;
24+
size_t size;
25+
size_t alignment;
26+
uint8_t canary;
27+
};
28+
29+
class AllocVec {
30+
public:
31+
AllocVec(FreeListHeap &heap) : heap(&heap), size_(0), capacity(0) {
32+
allocs = nullptr;
33+
}
34+
35+
bool empty() const { return !size_; }
36+
37+
size_t size() const { return size_; }
38+
39+
bool push_back(Alloc alloc) {
40+
if (size_ == capacity) {
41+
size_t new_cap = capacity ? capacity * 2 : 1;
42+
Alloc *new_allocs = reinterpret_cast<Alloc *>(
43+
heap->realloc(allocs, new_cap * sizeof(Alloc)));
44+
if (!new_allocs)
45+
return false;
46+
allocs = new_allocs;
47+
capacity = new_cap;
48+
}
49+
allocs[size_++] = alloc;
50+
return true;
51+
}
52+
53+
Alloc &operator[](size_t idx) { return allocs[idx]; }
54+
55+
void erase_idx(size_t idx) {
56+
inline_memmove(&allocs[idx], &allocs[idx + 1],
57+
sizeof(Alloc) * (size_ - idx - 1));
58+
--size_;
59+
}
60+
61+
private:
62+
FreeListHeap *heap;
63+
Alloc *allocs;
64+
size_t size_;
65+
size_t capacity;
66+
};
67+
68+
// Choose a T value by casting libfuzzer data or exit.
69+
template <typename T>
70+
cpp::optional<T> choose(const uint8_t *&data, size_t &remainder) {
71+
if (sizeof(T) > remainder)
72+
return cpp::nullopt;
73+
T out;
74+
inline_memcpy(&out, data, sizeof(T));
75+
data += sizeof(T);
76+
remainder -= sizeof(T);
77+
return out;
78+
}
79+
80+
enum class AllocType : uint8_t {
81+
MALLOC,
82+
ALIGNED_ALLOC,
83+
REALLOC,
84+
CALLOC,
85+
NUM_ALLOC_TYPES,
86+
};
87+
88+
template <>
89+
cpp::optional<AllocType> choose<AllocType>(const uint8_t *&data,
90+
size_t &remainder) {
91+
auto raw = choose<uint8_t>(data, remainder);
92+
if (!raw)
93+
return cpp::nullopt;
94+
return static_cast<AllocType>(
95+
*raw % static_cast<uint8_t>(AllocType::NUM_ALLOC_TYPES));
96+
}
97+
98+
constexpr size_t heap_size = 64 * 1024;
99+
100+
cpp::optional<size_t> choose_size(const uint8_t *&data, size_t &remainder) {
101+
auto raw = choose<uint8_t>(data, remainder);
102+
if (!raw)
103+
return cpp::nullopt;
104+
return *raw % heap_size;
105+
}
106+
107+
cpp::optional<size_t> choose_alloc_idx(const AllocVec &allocs,
108+
const uint8_t *&data,
109+
size_t &remainder) {
110+
if (allocs.empty())
111+
return cpp::nullopt;
112+
auto raw = choose<size_t>(data, remainder);
113+
if (!raw)
114+
return cpp::nullopt;
115+
return *raw % allocs.size();
116+
}
117+
118+
#define ASSIGN_OR_RETURN(TYPE, NAME, EXPR) \
119+
auto maybe_##NAME = EXPR; \
120+
if (!maybe_##NAME) \
121+
return 0; \
122+
TYPE NAME = *maybe_##NAME
123+
124+
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t remainder) {
125+
FreeListHeapBuffer<heap_size> heap;
126+
AllocVec allocs(heap);
127+
128+
uint8_t canary = 0;
129+
while (true) {
130+
ASSIGN_OR_RETURN(auto, should_alloc, choose<bool>(data, remainder));
131+
if (should_alloc) {
132+
ASSIGN_OR_RETURN(auto, alloc_type, choose<AllocType>(data, remainder));
133+
ASSIGN_OR_RETURN(size_t, alloc_size, choose_size(data, remainder));
134+
135+
// Perform allocation.
136+
void *ptr = nullptr;
137+
size_t alignment = alignof(max_align_t);
138+
switch (alloc_type) {
139+
case AllocType::MALLOC:
140+
ptr = heap.allocate(alloc_size);
141+
break;
142+
case AllocType::ALIGNED_ALLOC: {
143+
ASSIGN_OR_RETURN(size_t, alignment, choose_size(data, remainder));
144+
alignment = cpp::bit_ceil(alignment);
145+
ptr = heap.aligned_allocate(alignment, alloc_size);
146+
break;
147+
}
148+
case AllocType::REALLOC: {
149+
if (!alloc_size)
150+
return 0;
151+
ASSIGN_OR_RETURN(size_t, idx,
152+
choose_alloc_idx(allocs, data, remainder));
153+
Alloc &alloc = allocs[idx];
154+
ptr = heap.realloc(alloc.ptr, alloc_size);
155+
if (ptr) {
156+
// Extend the canary region if necessary.
157+
if (alloc_size > alloc.size)
158+
inline_memset(static_cast<char *>(ptr) + alloc.size, alloc.canary,
159+
alloc_size - alloc.size);
160+
alloc.ptr = ptr;
161+
alloc.size = alloc_size;
162+
alloc.alignment = alignof(max_align_t);
163+
}
164+
break;
165+
}
166+
case AllocType::CALLOC: {
167+
ASSIGN_OR_RETURN(size_t, count, choose_size(data, remainder));
168+
size_t total;
169+
if (__builtin_mul_overflow(count, alloc_size, &total))
170+
return 0;
171+
ptr = heap.calloc(count, alloc_size);
172+
if (ptr)
173+
for (size_t i = 0; i < total; ++i)
174+
if (static_cast<char *>(ptr)[i] != 0)
175+
__builtin_trap();
176+
break;
177+
}
178+
case AllocType::NUM_ALLOC_TYPES:
179+
__builtin_unreachable();
180+
}
181+
182+
if (ptr) {
183+
if (alignment < alignof(max_align_t))
184+
alignment = alignof(max_align_t);
185+
// Check alignment.
186+
if (reinterpret_cast<uintptr_t>(ptr) % alignment)
187+
__builtin_trap();
188+
189+
if (alloc_type != AllocType::REALLOC) {
190+
// Fill the object with a canary byte.
191+
inline_memset(ptr, canary, alloc_size);
192+
193+
// Track the allocation.
194+
if (!allocs.push_back({ptr, alloc_size, alignment, canary}))
195+
return 0;
196+
++canary;
197+
}
198+
}
199+
} else {
200+
// Select a random allocation.
201+
ASSIGN_OR_RETURN(size_t, idx, choose_alloc_idx(allocs, data, remainder));
202+
Alloc &alloc = allocs[idx];
203+
204+
// Check alignment.
205+
if (reinterpret_cast<uintptr_t>(alloc.ptr) % alloc.alignment)
206+
__builtin_trap();
207+
208+
// Check the canary.
209+
uint8_t *ptr = reinterpret_cast<uint8_t *>(alloc.ptr);
210+
for (size_t i = 0; i < alloc.size; ++i)
211+
if (ptr[i] != alloc.canary)
212+
__builtin_trap();
213+
214+
// Free the allocation and untrack it.
215+
heap.free(alloc.ptr);
216+
allocs.erase_idx(idx);
217+
}
218+
}
219+
return 0;
220+
}

libc/src/__support/block.h

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,11 @@ class Block {
217217

218218
/// Attempts to split this block.
219219
///
220-
/// If successful, the block will have an inner size of `new_inner_size`,
221-
/// rounded to ensure that the split point is on an ALIGNMENT boundary. The
222-
/// remaining space will be returned as a new block. Note that the prev_ field
223-
/// of the next block counts as part of the inner size of the returnd block.
224-
///
225-
/// This method may fail if the remaining space is too small to hold a new
226-
/// block. If this method fails for any reason, the original block is
227-
/// 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.
228225
optional<Block *> split(size_t new_inner_size);
229226

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

463460
size_t old_inner_size = inner_size();
464461
new_inner_size =

libc/src/__support/freelist_heap.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "src/__support/CPP/span.h"
1818
#include "src/__support/libc_assert.h"
1919
#include "src/__support/macros/config.h"
20+
#include "src/__support/math_extras.h"
2021
#include "src/string/memory_utils/inline_memcpy.h"
2122
#include "src/string/memory_utils/inline_memset.h"
2223

@@ -90,7 +91,7 @@ LIBC_INLINE void *FreeListHeap::allocate_impl(size_t alignment, size_t size) {
9091

9192
size_t request_size = size;
9293
if (alignment > alignof(max_align_t)) {
93-
if (add_overflow(size, alignment - 1, size))
94+
if (add_overflow(size, alignment - 1, request_size))
9495
return nullptr;
9596
}
9697

libc/src/__support/freetrie.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,19 @@ void FreeTrie::remove(Node *node) {
3232
new_node = leaf;
3333
}
3434

35+
if (!is_head(node))
36+
return;
37+
3538
// Copy the trie links to the new head.
3639
new_node->lower = node->lower;
3740
new_node->upper = node->upper;
3841
new_node->parent = node->parent;
3942
replace_node(node, new_node);
40-
return;
4143
}
4244

4345
void FreeTrie::replace_node(Node *node, Node *new_node) {
46+
LIBC_ASSERT(is_head(node) && "only head nodes contain trie links");
47+
4448
if (node->parent) {
4549
Node *&parent_child =
4650
node->parent->lower == node ? node->parent->lower : node->parent->upper;

libc/src/__support/freetrie.h

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,20 @@ namespace LIBC_NAMESPACE_DECL {
3838
/// The trie refers to, but does not own, the Nodes that comprise it.
3939
class FreeTrie {
4040
public:
41-
/// A trie node that is also a free list. The subtrie contains a continous
42-
/// SizeRange of free lists. The lower and upper subtrie's contain the lower
43-
/// and upper half of the subtries range. There is no direct relationship
44-
/// between the size of this node's free list and the contents of the lower
45-
/// and upper subtries.
41+
/// A trie node that is also a free list. Only the head node of each list is
42+
/// actually part of the trie. The subtrie contains a continous SizeRange of
43+
/// free lists. The lower and upper subtrie's contain the lower and upper half
44+
/// of the subtries range. There is no direct relationship between the size of
45+
/// this node's free list and the contents of the lower and upper subtries.
4646
class Node : public FreeList::Node {
4747
/// The child subtrie covering the lower half of this subtrie's size range.
48+
/// Undefined if this is not the head of the list.
4849
Node *lower;
4950
/// The child subtrie covering the upper half of this subtrie's size range.
51+
/// Undefined if this is not the head of the list.
5052
Node *upper;
51-
/// The parent subtrie or nullptr if this is the root.
53+
/// The parent subtrie. nullptr if this is the root or not the head of the
54+
/// list.
5255
Node *parent;
5356

5457
friend class FreeTrie;
@@ -103,6 +106,9 @@ class FreeTrie {
103106
Node *find_best_fit(size_t size);
104107

105108
private:
109+
/// @returns Whether a node is the head of its containing freelist.
110+
bool is_head(Node *node) const { return node->parent || node == root; }
111+
106112
/// Replaces references to one node with another (or nullptr) in all adjacent
107113
/// parent and child nodes.
108114
void replace_node(Node *node, Node *new_node);
@@ -134,9 +140,13 @@ LIBC_INLINE void FreeTrie::push(Block<> *block) {
134140
}
135141

136142
Node *node = new (block->usable_space()) Node;
137-
node->lower = node->upper = nullptr;
138-
node->parent = parent;
139143
FreeList list = *cur;
144+
if (list.empty()) {
145+
node->parent = parent;
146+
node->lower = node->upper = nullptr;
147+
} else {
148+
node->parent = nullptr;
149+
}
140150
list.push(node);
141151
*cur = static_cast<Node *>(list.begin());
142152
}

libc/test/src/__support/block_test.cpp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -238,20 +238,6 @@ TEST_FOR_EACH_BLOCK_TYPE(CannotMakeSecondBlockLargerInSplit) {
238238
ASSERT_FALSE(result.has_value());
239239
}
240240

241-
TEST_FOR_EACH_BLOCK_TYPE(CannotMakeZeroSizeFirstBlock) {
242-
// This block doesn't support splitting with zero payload size, since the
243-
// prev_ field of the next block is always available.
244-
constexpr size_t kN = 1024;
245-
246-
alignas(BlockType::ALIGNMENT) array<byte, kN> bytes;
247-
auto result = BlockType::init(bytes);
248-
ASSERT_TRUE(result.has_value());
249-
BlockType *block = *result;
250-
251-
result = block->split(0);
252-
EXPECT_FALSE(result.has_value());
253-
}
254-
255241
TEST_FOR_EACH_BLOCK_TYPE(CanMakeMinimalSizeFirstBlock) {
256242
// This block does support splitting with minimal payload size.
257243
constexpr size_t kN = 1024;

0 commit comments

Comments
 (0)