Skip to content

fix(security): harden untrusted .db parse/verify surface - #122

Merged
gburd merged 5 commits into
masterfrom
security/pentest-final
Jul 31, 2026
Merged

fix(security): harden untrusted .db parse/verify surface#122
gburd merged 5 commits into
masterfrom
security/pentest-final

Conversation

@gburd

@gburd gburd commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Security review of the embedded core's untrusted on-disk parse/verify surface (a hostile/corrupt .db file the application opens). 6 confirmed vulnerabilities FIXED, 2 deferred (documented). Each fix is a minimal root-cause guard at a shared chokepoint — reject bad input with EINVAL/DB_VERIFY_BAD/DB_PAGE_NOTFOUND, no panic, no on-disk/log/region/ABI change, no valid-input rejection.

Sev Class Site
HIGH OOB write / type confusion (memory corruption) __part_verify blindly called __ham_open on a Heap/Queue-typed dbp → LOCK_INIT writes past an 88-byte HEAP_CURSOR (partition.c)
MED Free of indeterminate pointer (double-free) __heap_vrfy freed uninitialized offsets on early datapage failure (heap_verify.c)
MED DoS — infinite loop __bam_search fast-path descent lacked a child-level-decrease check → pointer spins forever (bt_search.c)
MED SIGFPE ÷0 heap region_size as divisor (heap_open.c)
MED SIGFPE ÷0 queue rec_page==0 as divisor (qam_open.c, qam_verify.c)

Evidence (independently re-verified)

Every seed reproduces on master and is clean after the fix (ASan-instrumented libdb). Confirmed the HIGH fix is load-bearing: with partition.c reverted + a clean ASan rebuild, dbfile_typeconf_part_verify.seed FAILs (heap-buffer-overflow WRITE); with the fix, all 9 crash seeds PASS. Regression: test001 btree/hash/heap/queue + test025 recno all PASS (ERR=0) — no valid DB rejected. No shared-format/ABI/log-record file touched.

Also closed a harness gap

The crash-seed gate now builds an ASan-instrumented libdb (build_asan_gate/) so memory faults inside libdb (like the HIGH OOB write) are caught — previously invisible because the gate linked a non-instrumented libdb.a. 5 new seeds wired into check-crashes.sh (9/9 PASS, each FAILs when its fix is reverted) + heap/queue corpus seeds.

Deferred (documented in the review notes)

  • Queue extent-scan DoS (bounded, terminates — not memory corruption; a safe fix must be extent-aware to not reject valid large/wrapped queues).
  • Weak IV RNG seed (AES-CBC mode + IV-uniqueness are correct; only IV unpredictability is weak — a design trait, not memory safety; suggest seeding from getrandom).

Bottom line: after these fixes a single hostile .db file no longer yields OOB write, double-free, SIGFPE, or an infinite loop on the fuzzer-reachable parse/verify paths. Recovery/multi-process/crypto-mode paths showed no memory-safety defects this pass.

gburd added 5 commits July 31, 2026 12:35
A partitioned-database verify (__part_verify) chose the access-method open
with a blind `if (type==DB_BTREE) __bam_open; else __ham_open;`.  The else
catches every non-Btree type, so a corrupt/hostile file whose meta page
declares DB_HEAP (or DB_QUEUE) while setting the partition flag is opened with
the Hash AM.  __ham_open's __db_cursor then allocates the cursor internal sized
for dbp->type (an 88-byte HEAP_CURSOR), which __ham_get_meta casts to
HASH_CURSOR and __db_lget writes hcp->hlock past the end of -- an 8-byte
heap-buffer-overflow WRITE (type confusion, memory corruption) reachable from
DB->verify on an untrusted .db file.

Partitioned databases only support Btree/Recno and Hash, so dispatch by exact
type and reject anything else via __db_unknown_type(), mirroring the type
switch already used in __part_truncate.

Regression: test/fuzz/crashes/dbfile_typeconf_part_verify.seed (ASan
heap-buffer-overflow before, clean after).  Found by fuzzing an
ASan-instrumented libdb with test/fuzz/fuzz_dbfile.
…nter)

__heap_vrfy declared `db_indx_t *offsets;` uninitialized and freed it
unconditionally at the err label.  When __db_vrfy_datapage (called first, before
offsets is assigned) fails on a corrupt heap page, control jumps to
`err: __os_free(env, offsets)` with offsets holding stack garbage -- a free of
an indeterminate pointer (ASan observed a double-free where the garbage aliased
a freshly-freed 88-byte VRFY_PAGEINFO; in general a wild free / heap
corruption) reachable from DB->verify on an untrusted heap file.

Initialize offsets = NULL at declaration; __os_free(NULL) is a documented no-op.
This is the HEAP-verify double-free left OPEN in .agents/fuzz-found-bugs.md,
now root-caused.

Regression: test/fuzz/crashes/dbfile_doublefree_heap_vrfy.seed.
…op DoS)

__bam_search descends the tree in a for(;;) that terminates only at
LEVEL(h)==LEAFLEVEL.  The common latch-coupling fast path fetched the child page
and re-looped with no check that the child's level is below the parent's (the
lock-retry path already enforces LEVEL(h)==level-1, but the fast path did not).
A corrupt P_IBTREE page whose BINTERNAL child pointer targets itself, a sibling,
or an ancestor at the same-or-higher level makes the descent never reach a leaf,
so a read cursor (DB_FIRST/DB_NEXT) on an untrusted .db file spins forever -- a
denial of service.

A valid Btree always has strictly decreasing levels root->leaf, so guard
`LEVEL(child) >= LEVEL(parent)` after the fast-path fetch and return a clean
DB_PAGE_NOTFOUND (no __env_panic), bounding the descent to <=255 iterations.
Cannot fire on a valid tree.  Hot-path safety verified: test001 btree/hash/heap/
queue, test011, test003 recno, recd005/recd015 recovery, and the DST
crash-recover capstone all pass.

Regression: test/fuzz/crashes/dbfile_infloop_bam_search.seed (hang before,
0.02s clean after).
…ify (SIGFPE)

Two divide-by-zero crashes (SIGFPE) reachable from DB->open/verify on an
untrusted .db file, both because a meta-page scalar is trusted before being used
as a divisor and DB_ASSERT is compiled out of production builds:

- heap region_size: used via HEAP_REGION_SIZE(dbp)+1 in HEAP_REGION_PGNO/
  HEAP_REGION_NUM.  region_size==0 or ==UINT32_MAX (the +1 wraps to 0) divides
  by zero (SIGFPE __heap_vrfy_meta).  Reject region_size==0 || >
  HEAP_REGION_COUNT at both load points (__heap_read_meta open, __heap_vrfy_meta
  verify) -- exactly the bound __heap_new_file enforces on creation.

- queue rec_page (records/page): used via QAM_RECNO_PAGE ((recno-1)/rec_page)
  throughout the queue AM.  rec_page==0 divides by zero (SIGFPE __qam_vrfy_meta).
  Reject rec_page==0 at both load points (__qam_open, __qam_vrfy_meta).

Regression: test/fuzz/crashes/dbfile_fpe_heap_region_size.seed and
dbfile_fpe_qam_recpage.seed.
check-crashes.sh linked a non-ASan libdb.a (only the harness .c was
instrumented), so a heap-buffer-overflow / UAF *inside* libdb's own allocations
(e.g. the __part_verify type-confusion OOB write) produced no ASan report and
passed the gate.  Build an ASan-only libdb under build_asan_gate/ (UBSan omitted
-- it fires on libdb's legitimate base+offset pointer idioms) and link the
standalone harnesses against it so libdb-internal memory faults are now caught.
Opt out with LIBDB_ASAN=0.

Add valid_heap.db / valid_queue.db corpus seeds so fuzz_dbfile reaches the heap
and queue meta/verify paths (which surfaced findings above).  Document the five
new crash seeds and the deferred queue extent-scan DoS in crashes/README.md.
build_asan_gate/ is gitignored.
@github-actions

Copy link
Copy Markdown

Coccinelle convention checks

No new violations. ✅

Resolved since baseline (2) -- update dist/cocci/baseline.txt to lock these in.
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/crypto/mersenne/mt19937db.c|return (ret);
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/mp/mp_register.c|return (ret);

@github-actions

Copy link
Copy Markdown

ABI diff vs v5.3.33 (libabigail — authoritative)

Removed exported symbols (nm -D, _NNNN version suffix normalized)

None.


Advisory: libabigail/nm is the authoritative binary-ABI check; Coccinelle is complementary source-level early warning. See dist/cocci/README.md.

@gburd
gburd merged commit d43eeb7 into master Jul 31, 2026
49 of 51 checks passed
@gburd
gburd deleted the security/pentest-final branch July 31, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant