blob: 7b7985a83c3e656acab506018f168dd99f4403e5 [file] [log] [blame]
//===-- freelist_heap_fuzz.cpp --------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
///
/// Fuzzing test for llvm-libc freelist-based heap implementation.
///
//===----------------------------------------------------------------------===//
#include "src/__support/CPP/bit.h"
#include "src/__support/CPP/optional.h"
#include "src/__support/freelist_heap.h"
#include "src/string/memory_utils/inline_memcpy.h"
#include "src/string/memory_utils/inline_memmove.h"
#include "src/string/memory_utils/inline_memset.h"
asm(R"(
.globl _end, __llvm_libc_heap_limit
.bss
_end:
.fill 1024
__llvm_libc_heap_limit:
)";
using LIBC_NAMESPACE::FreeListHeap;
using LIBC_NAMESPACE::inline_memset;
using LIBC_NAMESPACE::cpp::nullopt;
using LIBC_NAMESPACE::cpp::optional;
// Record of an outstanding allocation.
struct Alloc {
void *ptr;
size_t size;
size_t alignment;
uint8_t canary; // Byte written to the allocation
};
// A simple vector that tracks allocations using the heap.
class AllocVec {
public:
AllocVec(FreeListHeap &heap) : heap(&heap), size_(0), capacity(0) {
allocs = nullptr;
}
bool empty() const { return !size_; }
size_t size() const { return size_; }
bool push_back(Alloc alloc) {
if (size_ == capacity) {
size_t new_cap = capacity ? capacity * 2 : 1;
Alloc *new_allocs = reinterpret_cast<Alloc *>(
heap->realloc(allocs, new_cap * sizeof(Alloc)));
if (!new_allocs)
return false;
allocs = new_allocs;
capacity = new_cap;
}
allocs[size_++] = alloc;
return true;
}
Alloc &operator[](size_t idx) { return allocs[idx]; }
void erase_idx(size_t idx) {
LIBC_NAMESPACE::inline_memmove(&allocs[idx], &allocs[idx + 1],
sizeof(Alloc) * (size_ - idx - 1));
--size_;
}
private:
FreeListHeap *heap;
Alloc *allocs;
size_t size_;
size_t capacity;
};
// Choose a T value by casting libfuzzer data or exit.
template <typename T>
optional<T> choose(const uint8_t *&data, size_t &remainder) {
if (sizeof(T) > remainder)
return nullopt;
T out;
LIBC_NAMESPACE::inline_memcpy(&out, data, sizeof(T));
data += sizeof(T);
remainder -= sizeof(T);
return out;
}
// The type of allocation to perform
enum class AllocType : uint8_t {
MALLOC,
ALIGNED_ALLOC,
REALLOC,
CALLOC,
NUM_ALLOC_TYPES,
};
template <>
optional<AllocType> choose<AllocType>(const uint8_t *&data, size_t &remainder) {
auto raw = choose<uint8_t>(data, remainder);
if (!raw)
return nullopt;
return static_cast<AllocType>(
*raw % static_cast<uint8_t>(AllocType::NUM_ALLOC_TYPES));
}
constexpr size_t heap_size = 64 * 1024;
optional<size_t> choose_size(const uint8_t *&data, size_t &remainder) {
auto raw = choose<size_t>(data, remainder);
if (!raw)
return nullopt;
return *raw % heap_size;
}
optional<size_t> choose_alloc_idx(const AllocVec &allocs, const uint8_t *&data,
size_t &remainder) {
if (allocs.empty())
return nullopt;
auto raw = choose<size_t>(data, remainder);
if (!raw)
return nullopt;
return *raw % allocs.size();
}
#define ASSIGN_OR_RETURN(TYPE, NAME, EXPR) \
auto maybe_##NAME = EXPR; \
if (!maybe_##NAME) \
return 0; \
TYPE NAME = *maybe_##NAME
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t remainder) {
LIBC_NAMESPACE::FreeListHeapBuffer<heap_size> heap;
AllocVec allocs(heap);
uint8_t canary = 0;
while (true) {
ASSIGN_OR_RETURN(auto, should_alloc, choose<bool>(data, remainder));
if (should_alloc) {
ASSIGN_OR_RETURN(auto, alloc_type, choose<AllocType>(data, remainder));
ASSIGN_OR_RETURN(size_t, alloc_size, choose_size(data, remainder));
// Perform allocation.
void *ptr = nullptr;
size_t alignment = alignof(max_align_t);
switch (alloc_type) {
case AllocType::MALLOC:
ptr = heap.allocate(alloc_size);
break;
case AllocType::ALIGNED_ALLOC: {
ASSIGN_OR_RETURN(size_t, alignment, choose_size(data, remainder));
alignment = LIBC_NAMESPACE::cpp::bit_ceil(alignment);
ptr = heap.aligned_allocate(alignment, alloc_size);
break;
}
case AllocType::REALLOC: {
if (!alloc_size)
return 0;
ASSIGN_OR_RETURN(size_t, idx,
choose_alloc_idx(allocs, data, remainder));
Alloc &alloc = allocs[idx];
ptr = heap.realloc(alloc.ptr, alloc_size);
if (ptr) {
// Extend the canary region if necessary.
if (alloc_size > alloc.size)
inline_memset(static_cast<char *>(ptr) + alloc.size, alloc.canary,
alloc_size - alloc.size);
alloc.ptr = ptr;
alloc.size = alloc_size;
alloc.alignment = alignof(max_align_t);
}
break;
}
case AllocType::CALLOC: {
ASSIGN_OR_RETURN(size_t, count, choose_size(data, remainder));
size_t total;
if (__builtin_mul_overflow(count, alloc_size, &total))
return 0;
ptr = heap.calloc(count, alloc_size);
if (ptr)
for (size_t i = 0; i < total; ++i)
if (static_cast<char *>(ptr)[i] != 0)
__builtin_trap();
break;
}
case AllocType::NUM_ALLOC_TYPES:
__builtin_unreachable();
}
if (ptr) {
// aligned_allocate should automatically apply a minimum alignment.
if (alignment < alignof(max_align_t))
alignment = alignof(max_align_t);
// Check alignment.
if (reinterpret_cast<uintptr_t>(ptr) % alignment)
__builtin_trap();
// Reallocation is treated specially above, since we would otherwise
// lose the original size.
if (alloc_type != AllocType::REALLOC) {
// Fill the object with a canary byte.
inline_memset(ptr, canary, alloc_size);
// Track the allocation.
if (!allocs.push_back({ptr, alloc_size, alignment, canary}))
return 0;
++canary;
}
}
} else {
// Select a random allocation.
ASSIGN_OR_RETURN(size_t, idx, choose_alloc_idx(allocs, data, remainder));
Alloc &alloc = allocs[idx];
// Check alignment.
if (reinterpret_cast<uintptr_t>(alloc.ptr) % alloc.alignment)
__builtin_trap();
// Check the canary.
uint8_t *ptr = reinterpret_cast<uint8_t *>(alloc.ptr);
for (size_t i = 0; i < alloc.size; ++i)
if (ptr[i] != alloc.canary)
__builtin_trap();
// Free the allocation and untrack it.
heap.free(alloc.ptr);
allocs.erase_idx(idx);
}
}
return 0;
}