libdb 2026.09.1
A correctness release. It closes the last open correctness defect (#159 -- self-reported from our own fuzz corpus, not an external report; every externally-reported issue closed in 5.3.35), a residual hole in that fix's predecessor, and two test thresholds I had set from single measurements. For the first time this cycle the issue tracker is empty.
Queue cursor read path (#159)
A queue's first_recno/cur_recno come from the meta page, and QAM_OUTSIDE_QUEUE() treats cur < first as a wrapped queue — so a corrupt or hostile meta page can describe a range covering most of the recno space. A cursor walking it asked for one page per record, and each miss cost a filename construction plus a stat(2): billions of them. Bounded (it terminated and corrupted nothing) but a denial of service.
The bound now comes from the extent files actually present, cached per handle and dropped when an extent is created.
Placement was the entire difficulty, and two earlier attempts failed on it:
| Attempt | Seed | Verify gate |
|---|---|---|
Bound at the top of __qam_fprobe |
bounded | HUNG |
| Bound in the cursor walk | still unbounded | green |
| Shipped — inside the extent-not-yet-open branch | bounded | green |
The hang took gdb to find: the extent-array management block that runs before opening anything is also what closes idle extent files once their pinref reaches zero. Returning early leaked extents until the buffer cache was exhausted — the hang was in __memp_alloc, not in the bound. Bounding in the cursor walk instead didn't work because the probes originate below it.
A second trap is recorded in the code: the bound must be read from the cache only. __qam_extent_maxpage() reads the directory, so consulting it per probe replaces one stat(2) with a whole readdir — worse than the bug.
Residual hole from 5.3.35 also closed: when no extent file exists, __qam_extent_maxpage() returns PGNO_INVALID and __qam_vrfy_walkqueue() skipped its clamp entirely, leaving the verify scan bound at ~4.29e9 pages. There is nothing to walk, so stop = first.
Test thresholds (#169, #170)
Two limits I had set from a single measurement failed during qualification — both on machine load, not on any defect:
| Gate | Was | Now | Why |
|---|---|---|---|
crash-seed SEED_TIMEOUT |
30s | 240s | seed is 16s plain but 74–120s under the ASan library the gate builds by default |
| read-path DoS limit | 60s | 600s | 25–27s idle, 66s under a parallel build |
Both now carry their measurement spread in a comment so the numbers aren't re-tightened by guess, and both were re-verified to still fail when the fix they guard is removed.
Testing
The DoS reproducer moves from test/fuzz/known-slow/ into test/fuzz/crashes/ — a gated regression now rather than a documented limitation; known-slow/ is gone.
test/db/qam_readpath_bound.c asserts both directions, because a bound that is too tight silently truncates real data, which is worse than the DoS it prevents: a legitimate 20,000-record / 770-extent queue reads back every record and survives consume-then-re-append — proving the cached bound cannot pin a queue shut after its extents are unlinked.
Qualification
From a pristine clone, and for the timing-sensitive gates on dedicated hardware (the shared build host runs at load 20–45 from unrelated projects, which is exactly what made my two timing assertions fail spuriously):
| Check | Result |
|---|---|
autoconf --enable-cxx / meson + ninja |
clean, 0 errors |
db_verify -V |
Berkeley DB 5.3.36: (September 9, 2026) |
test/db runners |
9/9 |
| isolation / lockmatrix / soak | PASS · PASS · 5 workloads 0 unexpected |
| crash-seed gate (ASan) | 10/10, exit 0 |
fi_sweep (ASan) |
1107 points — CRASH 0, HANG 0, DIRTY 0 |
| TCL | ssi001/002/009, txn001–003, lock001–003, test001 btree/hash/queue/recno, recd001/002 — fail=0 |
fi_sweep grew from 947 to 1107 failure points as this cycle's fixes added allocation sites — all handled cleanly.
Upgrading
No on-disk, log, or ABI change from 5.3.35. Recommended for anyone using the Queue access method with extents (DB->set_q_extentsize), and for anyone on 5.3.35, which shipped with queue extent verification silently disabled.