index/pager: place a split's new sibling positionally, and fix mmap growth alignment
Two bugs, both of which the crash fuzzer surfaced and neither of which any
existing test could see.
**A split put the new sibling in the wrong slot when separators repeat.**
`split_leaf` located the new right sibling with `separator_pos(node, key)`, a
search for the promoted key. That agrees with "immediately after `left`" only
while separators are distinct. When several children share one -- ten distinct
values across thousands of documents, so each value spans dozens of leaves --
`separator_pos` returns the slot after the *whole* equal-key run, which puts
the sibling at the end of that run while the leaf chain has it right after
`left`.
Parent child order then stops matching leaf chain order, and that is the one
thing a lookup cannot survive: `descend_lower` picks the last child of the equal
run, and `lookup_eq` walks forward from there over keys *smaller* than the one
it wants, stops at the first mismatch, and reports nothing. Every entry is
present, the chain is correctly ordered, `count()` is right -- and the query
returns empty. `crash-fuzz.js` found it after ~700 heavy cycles as
`find({k: 3})` returning 0 of 401 documents while every other key was exact.
Fixed by `child_slot_after`, which is positional by construction.
**mmap growth rounded with a non-power-of-two alignment.** Past 64 MiB the
growth chunk becomes a proportion of the current size (`mapped_pages / 8`),
which is not a power of two -- and `std.mem.alignForward` asserts that it is.
In safe builds that panicked; in ReleaseFast, where the assert is compiled out,
it computed `(addr + align - 1) & ~(align - 1)` with a non-power-of-two mask,
which can round *down*. A mapping shorter than intended is survivable, but a
mapping longer than the file is exactly what this function exists to prevent: a
store into a mapped page past end-of-file raises SIGBUS, which no error path
catches. `alignForwardAnyAlign` instead. Never noticed because no unit test grew
a pager past 64 MiB.
Also here, because both bugs were invisible rather than merely unfixed:
- `assert_indexes_cover_every_document` (db.zig) checks the index invariant
directly -- an index generates candidates and the full filter is re-applied to
those, so a missing entry is a missing query result nothing else detects.
- `Index.unreachable_key_count` counts keys present in the leaf chain but not
reachable by descending from the root, which is precisely the state above:
healthy by every other measure.
- `Index.dbg_root` dumps parent/chain agreement. Marked TEMPORARY; drop it once
the invariant checks have earned their keep.
- `crash-fuzz.js` now asks the same question without the index, so a failure
says whether the documents are wrong or only the index's answer about them,
and reports per-key totals so one lost leaf is distinguishable from an empty
index.
Verified: `zig build test` in ReleaseFast and ReleaseSafe, and seeded fuzzer
runs that previously reproduced the split bug.
This commit is contained in:
@@ -771,8 +771,20 @@ pub const Pager = struct {
|
||||
|
||||
// Round up to a growth chunk, and to the system page size, so a
|
||||
// 16 KiB-page host never gets a partial mapping request.
|
||||
//
|
||||
// `alignForwardAnyAlign`, not `alignForward`: the chunk is a *proportion*
|
||||
// of the current size once the file passes 64 MiB, and `mapped_pages / 8`
|
||||
// is not a power of two. `alignForward` asserts that it is -- so this
|
||||
// panicked in safe builds and, worse, in ReleaseFast (where the assert is
|
||||
// compiled out) computed `(addr + align - 1) & ~(align - 1)` with a
|
||||
// non-power-of-two mask, which can round *down*. A mapping longer than the
|
||||
// file is the one thing this function exists to prevent: a store into a
|
||||
// mapped page past end-of-file raises SIGBUS, which no error path catches.
|
||||
//
|
||||
// Never noticed because no unit test grew a pager past 64 MiB, which is
|
||||
// where the chunk stops being `grow_chunk_pages`.
|
||||
const chunk = @max(grow_chunk_pages, self.mapped_pages / 8);
|
||||
var new_pages = std.mem.alignForward(u32, want_pages, chunk);
|
||||
var new_pages = std.mem.alignForwardAnyAlign(u32, want_pages, chunk);
|
||||
const sys_pages: u32 = @intCast(map_align / page_size);
|
||||
if (sys_pages > 1) new_pages = std.mem.alignForward(u32, new_pages, sys_pages);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user