Skip to content

B-tree pages and copy(): on-disk offsets/sizes used as pointers with no 4096-byte bound #3

Description

@M-Tesla

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.
  • getDupFixedValdupfix_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.copycopy_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

  1. Reject offset/ksize/dsize outside [hdr, page_size) in getNode, getKey, getData, getDupFixedVal, putNode, delNode.
  2. Replace every getPagePtr with getPagePtrSafe (especially txn.getPage and open).
  3. Include the header in the checksum (except the CRC field itself). Do not short-circuit P_LEAF2 without validating dupfix_ksize.
  4. Meta.validate: geometry (root < first_unallocated, size fits in the map).
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions