Problem
Fields read from the file (slot offset, ksize, dsize/data_shim, upper/lower, pgno, first_unallocated) are used as addresses or slice lengths without checking that they fit in the 4096-byte page (or in the mmap).
In ReleaseFast, std.debug.assert is compiled out. Opening a .monolith with a lying header or nodes (restored/copied file, or a bitflip in the header) becomes an out-of-page or out-of-mapping read/write.
This is not a one-off: the same pattern shows up in several places.
Where
src/page/page.zig
getNode — adds an on-disk u16 to the page pointer. Page size is 4096; the offset can be up to 65535. There is no offset >= 20 && offset + node_header <= page_size.
getKey / getData — slice ksize (up to 64 KiB) and data_shim (up to 4 GiB) from a many-pointer, with no cap to the page.
getDupFixedVal — dupfix_ksize * index is unbounded.
putNode / delNode — header upper/ksize can write past the 4096-byte buffer.
src/env.zig
getPagePtr — the only guard is std.debug.assert(offset + PAGE_SIZE <= map.len). Gone in ReleaseFast. txn.getPage and open use this path.
getPagePtrSafe already exists, does the right check, and is never called.
Environment.copy — copy_len = first_unallocated * PAGE_SIZE from the meta. map.ptr[0..copy_len] does not use map.len. An inflated number reads past the mapping (process memory written into the destination file, or SIGSEGV).
Defenses that do not hold
- The checksum covers only the body (
bytes[20..page_size]). Flags, lower/upper, and txnid are omitted — exactly the fields used as pointers.
P_LEAF2 in the header makes validatePageChecksum always return true.
Meta.validate() only compares the magic. No root < first_unallocated, no first_unallocated * 4096 <= map.len.
- Open checksums the roots; the read path (
txn.getPage) does not re-validate.
Impact
Opening a malicious file (or a corrupted header with a “valid” body checksum) → out-of-bounds read/write on the mmap or the dirty buffer. Crash at minimum.
copy() with a lying first_unallocated copies memory past the file.
Suggested fix
- Reject offset/ksize/dsize outside
[hdr, page_size) in getNode, getKey, getData, getDupFixedVal, putNode, delNode.
- Replace every
getPagePtr with getPagePtrSafe (especially txn.getPage and open).
- Include the header in the checksum (except the CRC field itself). Do not short-circuit
P_LEAF2 without validating dupfix_ksize.
Meta.validate: geometry (root < first_unallocated, size fits in the map).
copy(): copy_len = min(num_pages * PAGE_SIZE, map.len) and reject impossible geometry.
Body checksums, rejecting out-of-map pgnos in readOverflowInto, and MAX_DEPTH in btreePut are fine — the hole is treating a page/meta as a trusted C struct.
Problem
Fields read from the file (slot offset,
ksize,dsize/data_shim,upper/lower,pgno,first_unallocated) are used as addresses or slice lengths without checking that they fit in the 4096-byte page (or in the mmap).In
ReleaseFast,std.debug.assertis compiled out. Opening a.monolithwith a lying header or nodes (restored/copied file, or a bitflip in the header) becomes an out-of-page or out-of-mapping read/write.This is not a one-off: the same pattern shows up in several places.
Where
src/page/page.ziggetNode— adds an on-disku16to the page pointer. Page size is 4096; the offset can be up to 65535. There is nooffset >= 20 && offset + node_header <= page_size.getKey/getData— sliceksize(up to 64 KiB) anddata_shim(up to 4 GiB) from a many-pointer, with no cap to the page.getDupFixedVal—dupfix_ksize * indexis unbounded.putNode/delNode— headerupper/ksizecan write past the 4096-byte buffer.src/env.ziggetPagePtr— the only guard isstd.debug.assert(offset + PAGE_SIZE <= map.len). Gone inReleaseFast.txn.getPageand open use this path.getPagePtrSafealready exists, does the right check, and is never called.Environment.copy—copy_len = first_unallocated * PAGE_SIZEfrom the meta.map.ptr[0..copy_len]does not usemap.len. An inflated number reads past the mapping (process memory written into the destination file, or SIGSEGV).Defenses that do not hold
bytes[20..page_size]). Flags,lower/upper, andtxnidare omitted — exactly the fields used as pointers.P_LEAF2in the header makesvalidatePageChecksumalways returntrue.Meta.validate()only compares the magic. Noroot < first_unallocated, nofirst_unallocated * 4096 <= map.len.txn.getPage) does not re-validate.Impact
Opening a malicious file (or a corrupted header with a “valid” body checksum) → out-of-bounds read/write on the mmap or the dirty buffer. Crash at minimum.
copy()with a lyingfirst_unallocatedcopies memory past the file.Suggested fix
[hdr, page_size)ingetNode,getKey,getData,getDupFixedVal,putNode,delNode.getPagePtrwithgetPagePtrSafe(especiallytxn.getPageand open).P_LEAF2without validatingdupfix_ksize.Meta.validate: geometry (root < first_unallocated, size fits in the map).copy():copy_len = min(num_pages * PAGE_SIZE, map.len)and reject impossible geometry.Body checksums, rejecting out-of-map pgnos in
readOverflowInto, andMAX_DEPTHinbtreePutare fine — the hole is treating a page/meta as a trusted C struct.