#include <alloc/common.h>
#include <alloc/asan.h>
#include <alloc/buddy.h>
volatile int zero = 0;
enum {
BUDDY_POISONED = (s8)0xef,
};
static inline int buddy_lock(struct buddy *buddy)
{
return arena_spin_lock(buddy->lock);
}
static inline void buddy_unlock(struct buddy *buddy)
{
arena_spin_unlock(buddy->lock);
}
static int buddy_reserve_arena_vaddr(struct buddy *buddy)
{
buddy->vaddr = 0;
return bpf_arena_reserve_pages(&arena,
(void __arena *)BUDDY_VADDR_OFFSET,
BUDDY_VADDR_SIZE / PAGE_SIZE);
}
static void buddy_unreserve_arena_vaddr(struct buddy *buddy)
{
bpf_arena_free_pages(
&arena, (void __arena *)(BUDDY_VADDR_OFFSET + buddy->vaddr),
(BUDDY_VADDR_SIZE - buddy->vaddr) / PAGE_SIZE);
buddy->vaddr = 0;
}
static int buddy_alloc_arena_vaddr(struct buddy *buddy, u64 *vaddrp)
{
u64 vaddr, old, new;
do {
vaddr = buddy->vaddr;
new = vaddr + BUDDY_CHUNK_BYTES;
if (new > BUDDY_VADDR_SIZE)
return -EINVAL;
old = __sync_val_compare_and_swap(&buddy->vaddr, vaddr, new);
} while (old != vaddr && can_loop);
if (old != vaddr)
return -EINVAL;
*vaddrp = BUDDY_VADDR_OFFSET + vaddr;
return 0;
}
static u64 arena_next_pow2(__u64 n)
{
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n |= n >> 32;
n++;
return n;
}
__weak
int idx_set_allocated(buddy_chunk_t __arg_arena *chunk, u64 idx, bool allocated)
{
if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
arena_stderr("setting order of invalid idx (%d, max %d)\n", idx,
BUDDY_CHUNK_ITEMS);
return -EINVAL;
}
if (allocated)
chunk->allocated[idx / 8] |= 1 << (idx % 8);
else
chunk->allocated[idx / 8] &= ~(1 << (idx % 8));
return 0;
}
static int idx_is_allocated(buddy_chunk_t *chunk, u64 idx, bool *allocated)
{
if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
arena_stderr("setting order of invalid idx (%d, max %d)\n", idx,
BUDDY_CHUNK_ITEMS);
return -EINVAL;
}
*allocated = chunk->allocated[idx / 8] & (1 << (idx % 8));
return 0;
}
__weak
int idx_set_order(buddy_chunk_t __arg_arena *chunk, u64 idx, u8 order)
{
u8 prev_order;
if (unlikely(order >= BUDDY_CHUNK_NUM_ORDERS)) {
arena_stderr("setting invalid order %u\n", order);
return -EINVAL;
}
if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
arena_stderr("setting order of invalid idx (%d, max %d)\n", idx,
BUDDY_CHUNK_ITEMS);
return -EINVAL;
}
prev_order = chunk->orders[idx / 2];
if (idx & 0x1) {
order &= 0xf;
order |= (prev_order & 0xf0);
} else {
order <<= 4;
order |= (prev_order & 0xf);
}
chunk->orders[idx / 2] = order;
return 0;
}
static u8 idx_get_order(buddy_chunk_t *chunk, u64 idx)
{
u8 result;
_Static_assert(BUDDY_CHUNK_NUM_ORDERS <= 16,
"order must fit in 4 bits");
if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
arena_stderr("setting order of invalid idx\n");
return BUDDY_CHUNK_NUM_ORDERS;
}
result = chunk->orders[idx / 2];
return (idx & 0x1) ? (result & 0xf) : (result >> 4);
}
static void __arena *idx_to_addr(buddy_chunk_t *chunk, size_t idx)
{
u64 address;
if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
arena_stderr("setting order of invalid idx\n");
return NULL;
}
if ((u64)chunk % BUDDY_CHUNK_BYTES)
DIAG();
address = (u64)chunk + (idx * BUDDY_MIN_ALLOC_BYTES);
return (void __arena *)address;
}
static buddy_header_t *idx_to_header(buddy_chunk_t *chunk, size_t idx)
{
bool allocated;
u64 address;
if (unlikely(idx_is_allocated(chunk, idx, &allocated))) {
arena_stderr("accessing invalid idx 0x%lx\n", idx);
return NULL;
}
if (unlikely(allocated)) {
arena_stderr("accessing allocated idx 0x%lx as header\n", idx);
return NULL;
}
address = (u64)idx_to_addr(chunk, idx);
return (buddy_header_t *)(address + BUDDY_HEADER_OFF);
}
static void header_add_freelist(buddy_chunk_t *chunk,
buddy_header_t *header, u64 idx, u8 order)
{
buddy_header_t *tmp_header;
idx_set_order(chunk, idx, order);
header->next_index = chunk->freelists[order];
header->prev_index = BUDDY_CHUNK_ITEMS;
if (header->next_index != BUDDY_CHUNK_ITEMS) {
tmp_header = idx_to_header(chunk, header->next_index);
tmp_header->prev_index = idx;
}
chunk->freelists[order] = idx;
}
static void header_remove_freelist(buddy_chunk_t *chunk,
buddy_header_t *header, u8 order)
{
buddy_header_t *tmp_header;
if (header->prev_index != BUDDY_CHUNK_ITEMS) {
tmp_header = idx_to_header(chunk, header->prev_index);
tmp_header->next_index = header->next_index;
}
if (header->next_index != BUDDY_CHUNK_ITEMS) {
tmp_header = idx_to_header(chunk, header->next_index);
tmp_header->prev_index = header->prev_index;
}
if (idx_to_header(chunk, chunk->freelists[order]) == header)
chunk->freelists[order] = header->next_index;
header->prev_index = BUDDY_CHUNK_ITEMS;
header->next_index = BUDDY_CHUNK_ITEMS;
}
static u64 size_to_order(size_t size)
{
u64 order;
if (unlikely(!size)) {
arena_stderr("size 0 has no order\n");
return 64;
}
order = arena_fls(arena_next_pow2(size));
if (order < BUDDY_MIN_ALLOC_SHIFT)
return 0;
return order - BUDDY_MIN_ALLOC_SHIFT;
}
__weak
int add_leftovers_to_freelist(buddy_chunk_t __arg_arena *chunk, u32 cur_idx,
u64 min_order, u64 max_order)
{
buddy_header_t *header;
u64 ord;
u32 idx;
bpf_for(ord, min_order, max_order) {
idx = cur_idx + (1 << ord);
header = idx_to_header(chunk, idx);
if (unlikely(!header))
return -EINVAL;
asan_unpoison(header, sizeof(*header));
idx_set_allocated(chunk, idx, false);
header_add_freelist(chunk, header, idx, ord);
}
return 0;
}
static buddy_chunk_t *buddy_chunk_get(struct buddy *buddy)
{
u64 order, ord, min_order, max_order;
buddy_chunk_t *chunk;
size_t left;
int power2;
u64 vaddr;
u32 idx;
int ret;
buddy_unlock(buddy);
ret = buddy_alloc_arena_vaddr(buddy, &vaddr);
if (ret) {
DIAG();
return NULL;
}
if (vaddr % BUDDY_CHUNK_BYTES) {
DIAG();
return NULL;
}
bpf_arena_free_pages(&arena, (void __arena *)vaddr,
BUDDY_CHUNK_PAGES);
chunk = bpf_arena_alloc_pages(&arena, (void __arena *)vaddr,
BUDDY_CHUNK_PAGES, NUMA_NO_NODE, 0);
if (!chunk) {
arena_stderr("[ALLOC FAILED]");
return NULL;
}
if ((ret = buddy_lock(buddy))) {
bpf_arena_free_pages(&arena, chunk, BUDDY_CHUNK_PAGES);
return NULL;
}
asan_poison(chunk, BUDDY_POISONED, BUDDY_CHUNK_PAGES * PAGE_SIZE);
asan_unpoison(chunk, sizeof(*chunk));
for (ord = zero; ord < BUDDY_CHUNK_NUM_ORDERS && can_loop; ord++)
chunk->freelists[ord] = BUDDY_CHUNK_ITEMS;
_Static_assert(BUDDY_CHUNK_PAGES * PAGE_SIZE >=
BUDDY_MIN_ALLOC_BYTES *
BUDDY_CHUNK_ITEMS,
"chunk must fit within the allocation");
max_order = BUDDY_CHUNK_NUM_ORDERS;
left = sizeof(*chunk);
idx = 0;
while (left && can_loop) {
power2 = arena_fls(left);
if (unlikely(power2 >= BUDDY_CHUNK_NUM_ORDERS)) {
arena_stderr(
"buddy chunk metadata require allocation of order %d\n",
power2);
arena_stderr(
"chunk has size of 0x%lx bytes (left %lx bytes)\n",
sizeof(*chunk), left);
buddy_unlock(buddy);
return NULL;
}
left -= (power2 >= BUDDY_MIN_ALLOC_SHIFT) ? 1 << power2 : left;
order = (power2 >= BUDDY_MIN_ALLOC_SHIFT) ? power2 - BUDDY_MIN_ALLOC_SHIFT : 0;
idx_set_allocated(chunk, idx, true);
min_order = left ? order + 1 : order;
if (add_leftovers_to_freelist(chunk, idx, min_order, max_order)) {
buddy_unlock(buddy);
return NULL;
}
idx += 1 << order;
max_order = order;
}
return chunk;
}
__hidden int buddy_init(struct buddy *buddy,
arena_spinlock_t __arg_arena __arena *lock)
{
buddy_chunk_t *chunk;
int ret;
buddy->lock = lock;
if ((ret = buddy_reserve_arena_vaddr(buddy))) {
DIAG();
return ret;
}
_Static_assert(BUDDY_CHUNK_PAGES > 0,
"chunk must use one or more pages");
if (buddy_lock(buddy)) {
DIAG();
return -EINVAL;
}
chunk = buddy_chunk_get(buddy);
if (!chunk) {
buddy->first_chunk = NULL;
return -ENOMEM;
}
chunk->next = buddy->first_chunk;
chunk->prev = NULL;
buddy->first_chunk = chunk;
buddy_unlock(buddy);
return 0;
}
__weak int buddy_destroy(struct buddy *buddy)
{
buddy_chunk_t *chunk, *next;
if (!buddy)
return -EINVAL;
for (chunk = buddy->first_chunk; chunk && can_loop; chunk = next) {
next = chunk->next;
asan_poison(chunk, BUDDY_POISONED,
BUDDY_CHUNK_PAGES * PAGE_SIZE);
bpf_arena_free_pages(&arena, chunk, BUDDY_CHUNK_PAGES);
}
buddy_unreserve_arena_vaddr(buddy);
buddy->first_chunk = NULL;
return 0;
}
__weak u64 buddy_chunk_alloc(buddy_chunk_t __arg_arena *chunk,
int order_req)
{
buddy_header_t *header, *tmp_header, *next_header;
u32 idx, tmpidx, retidx;
u64 address;
u64 order = 0;
u64 i;
bpf_for(order, order_req, BUDDY_CHUNK_NUM_ORDERS) {
if (chunk->freelists[order] != BUDDY_CHUNK_ITEMS)
break;
}
if (order >= BUDDY_CHUNK_NUM_ORDERS)
return (u64)NULL;
retidx = chunk->freelists[order];
header = idx_to_header(chunk, retidx);
chunk->freelists[order] = header->next_index;
if (header->next_index != BUDDY_CHUNK_ITEMS) {
next_header = idx_to_header(chunk, header->next_index);
next_header->prev_index = BUDDY_CHUNK_ITEMS;
}
header->prev_index = BUDDY_CHUNK_ITEMS;
header->next_index = BUDDY_CHUNK_ITEMS;
if (idx_set_order(chunk, retidx, order_req))
return (u64)NULL;
if (idx_set_allocated(chunk, retidx, true))
return (u64)NULL;
address = (u64)idx_to_addr(chunk, retidx);
bpf_for(i, order_req, order) {
idx = retidx + (1 << i);
header = idx_to_header(chunk, idx);
asan_unpoison(header, sizeof(*header));
if (idx_set_allocated(chunk, idx, false))
return (u64)NULL;
if (idx_set_order(chunk, idx, i))
return (u64)NULL;
tmpidx = chunk->freelists[i];
header->prev_index = BUDDY_CHUNK_ITEMS;
header->next_index = tmpidx;
if (tmpidx != BUDDY_CHUNK_ITEMS) {
tmp_header = idx_to_header(chunk, tmpidx);
tmp_header->prev_index = idx;
}
chunk->freelists[i] = idx;
}
return address;
}
__weak u64 buddy_alloc_internal(struct buddy *buddy, size_t size)
{
buddy_chunk_t *chunk;
u64 address;
int order;
if (!buddy)
return (u64)NULL;
order = size_to_order(size);
if (order >= BUDDY_CHUNK_NUM_ORDERS || order < 0) {
arena_stderr("invalid order %d (sz %lu)\n", order, size);
return (u64)NULL;
}
if (buddy_lock(buddy))
return (u64)NULL;
for (chunk = buddy->first_chunk; chunk != NULL && can_loop;
chunk = chunk->next) {
address = buddy_chunk_alloc(chunk, order);
if (address)
goto done;
}
chunk = buddy_chunk_get(buddy);
if (!chunk)
return (u64)NULL;
chunk->next = buddy->first_chunk;
chunk->prev = NULL;
buddy->first_chunk = chunk;
address = buddy_chunk_alloc(buddy->first_chunk, order);
done:
if (!address) {
buddy_unlock(buddy);
return (u64)NULL;
}
if (size < BUDDY_HEADER_OFF + sizeof(buddy_header_t))
asan_poison((u8 __arena *)address + BUDDY_HEADER_OFF,
BUDDY_POISONED, sizeof(buddy_header_t));
asan_unpoison((u8 __arena *)address, size);
buddy_unlock(buddy);
return address;
}
static __always_inline int buddy_free_unlocked(struct buddy *buddy, u64 addr)
{
buddy_header_t *header, *buddy_header;
u64 idx, buddy_idx, tmp_idx;
buddy_chunk_t *chunk;
bool allocated;
u8 order;
if (!buddy)
return -EINVAL;
if (addr & (BUDDY_MIN_ALLOC_BYTES - 1)) {
arena_stderr("Freeing unaligned address %llx\n", addr);
return 0;
}
chunk = (void __arena *)(addr & ~BUDDY_CHUNK_OFFSET_MASK);
idx = (addr & BUDDY_CHUNK_OFFSET_MASK) / BUDDY_MIN_ALLOC_BYTES;
idx_set_allocated(chunk, idx, false);
order = idx_get_order(chunk, idx);
header = idx_to_header(chunk, idx);
asan_poison((u8 __arena *)addr, BUDDY_POISONED,
BUDDY_MIN_ALLOC_BYTES << order);
asan_unpoison(header, sizeof(*header));
bpf_for(order, order, BUDDY_CHUNK_NUM_ORDERS) {
buddy_idx = idx ^ (1 << order);
idx_is_allocated(chunk, buddy_idx, &allocated);
if (allocated)
break;
if (idx_get_order(chunk, buddy_idx) != order)
break;
buddy_header = idx_to_header(chunk, buddy_idx);
header_remove_freelist(chunk, buddy_header, order);
if (buddy_idx < idx) {
tmp_idx = idx;
idx = buddy_idx;
buddy_idx = tmp_idx;
}
idx_set_order(chunk, buddy_idx, order);
buddy_header = idx_to_header(chunk, buddy_idx);
asan_poison(buddy_header, BUDDY_POISONED,
sizeof(*buddy_header));
}
idx_set_order(chunk, idx, order);
header = idx_to_header(chunk, idx);
header_add_freelist(chunk, header, idx, order);
return 0;
}
__weak int buddy_free_internal(struct buddy *buddy, u64 addr)
{
int ret;
if (!buddy)
return -EINVAL;
if ((ret = buddy_lock(buddy)))
return ret;
buddy_free_unlocked(buddy, addr);
buddy_unlock(buddy);
return 0;
}