Skip to content

Buffer manager crashes on kernels with CONFIG_PAGE_SIZE_16KB=y (e.g., Raspberry Pi 5 aarch64) #526

@epruseal

Description

@epruseal

Summary

LadybugDB 0.16.1 crashes at runtime on kernels built with CONFIG_PAGE_SIZE_16KB=y (e.g., the official Raspberry Pi OS kernel for RPi 4/5). The buffer manager calls madvise(addr, 4096, MADV_DONTNEED) with 4 KB alignment, but on 16 KB page kernels madvise returns EINVAL for any address or length not aligned to the kernel page size (16 KB). This causes a fatal RuntimeError after approximately 10,000–15,000 write operations.

System Information

OS:     Raspberry Pi OS (Debian bookworm), aarch64
Kernel: 6.18.29+rpt-rpi-2712 (official RPi kernel, CONFIG_PAGE_SIZE_16KB=y)
Python: 3.13
Package: ladybug==0.16.1
         ladybug-0.16.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl

Key kernel configuration:

CONFIG_PAGE_SIZE_16KB=y   # confirmed via /boot/config-$(uname -r)
$ getconf PAGE_SIZE
16384

Minimal Reproduction

import ladybug as lb

db   = lb.Database("/tmp/test.lbug", buffer_pool_size=256*1024*1024)
conn = lb.Connection(db)
conn.execute("CREATE NODE TABLE T(id STRING, PRIMARY KEY(id))")

# Fails at approximately i=12,000–15,000 (varies with node data size)
for i in range(20000):
    conn.execute(f"MERGE (:T {{id: 'node_{i}_{'x'*100}'}})")

Error:

RuntimeError: Buffer manager exception:
  Releasing physical memory associated with a frame failed with error code -1: Invalid argument.

Root Cause

The buffer manager allocates its pool as:

mmap(NULL, 1073741824, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0)

When evicting a 4 KB frame, it calls:

madvise(frame_addr, 4096, MADV_DONTNEED)  → EINVAL on 16KB-page kernels

On Linux with CONFIG_PAGE_SIZE_16KB=y, the kernel page size is 16384 bytes. The madvise(2) syscall requires both addr and length to be aligned to the kernel page size. With 4 KB database frames, only every 4th frame happens to be 16 KB-aligned, so the vast majority of eviction calls fail.

Python-level verification (run on affected system):

import ctypes, mmap

libc = ctypes.CDLL("libc.so.6", use_errno=True)

# 4KB-misaligned: fails on 16KB-page kernel
m = mmap.mmap(-1, 4096)
buf = (ctypes.c_char * 4096).from_buffer(m)
addr = ctypes.addressof(buf)
ctypes.set_errno(0)
ret = libc.madvise(ctypes.c_void_p(addr), ctypes.c_size_t(4096), ctypes.c_int(4))  # MADV_DONTNEED=4
print(ret, ctypes.get_errno())  # → -1  22  (EINVAL)

# 16KB-aligned: succeeds
m2 = mmap.mmap(-1, 16384)
buf2 = (ctypes.c_char * 16384).from_buffer(m2)
addr2 = ctypes.addressof(buf2)
ctypes.set_errno(0)
ret2 = libc.madvise(ctypes.c_void_p(addr2), ctypes.c_size_t(16384), ctypes.c_int(4))
print(ret2, ctypes.get_errno())  # → 0  0  (Success)

Pattern of Failure

strace -e madvise shows LadybugDB calling madvise with 4096-byte frames:

madvise(0x7ffedbc51000, 4096, MADV_DONTNEED) = -1 EINVAL (Invalid argument)

The failure occurs consistently after frame ~1025, which is the point at which the buffer pool fills and the first eviction is attempted.

Workaround Attempted

An LD_PRELOAD shim converting 16 KB-misaligned MADV_DONTNEED/MADV_FREE calls to no-ops suppresses the initial exception, but LadybugDB 0.16.1 then hits a secondary error:

RuntimeError: Buffer manager exception: Unable to allocate memory! The buffer pool is full and no memory could be freed!

This means the eviction path in 0.16.1 requires that madvise actually succeed and release physical memory. The no-op approach does not work for the current version.

Suggested Fix

In the buffer manager's frame-release code, query the actual kernel page size at startup and round the madvise call up to the kernel page granule:

// Before:
madvise(frame_addr, KUZU_PAGE_SIZE, MADV_DONTNEED);

// After:
static const size_t kernel_page = sysconf(_SC_PAGE_SIZE);
if (KUZU_PAGE_SIZE >= kernel_page) {
    // frame size >= page size: call is well-aligned by construction
    madvise(frame_addr, KUZU_PAGE_SIZE, MADV_DONTNEED);
} else {
    // frame size < page size: round addr down, length up
    uintptr_t aligned_addr = (uintptr_t)frame_addr & ~(kernel_page - 1);
    size_t aligned_len = kernel_page;  // covers at least one frame
    madvise((void*)aligned_addr, aligned_len, MADV_DONTNEED);
}

Alternatively, if KUZU_PAGE_SIZE < sysconf(_SC_PAGE_SIZE), skip the madvise call entirely and accept that physical memory is not immediately returned (lower memory pressure matters less than correctness). A startup warning could inform users of the configuration mismatch.

Impact

All LadybugDB users on:

  • Raspberry Pi 4/5 running the official Raspberry Pi OS kernel
  • Any Linux system with CONFIG_PAGE_SIZE_16KB=y or CONFIG_PAGE_SIZE_64KB=y

Raspberry Pi 5 is increasingly popular for AI/ML edge deployments. The RPi5 official kernel uses 16 KB pages for performance reasons, following Apple Silicon and other modern aarch64 SoCs. This bug makes LadybugDB entirely unusable on that platform for any workload that exceeds the buffer pool capacity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions