Skip to content

fix(security): harden untrusted .db parse/verify surface (OOB write, double-free, SIGFPE, infinite-loop DoS) - #121

Closed
gburd wants to merge 5 commits into
masterfrom
security/pentest-review
Closed

fix(security): harden untrusted .db parse/verify surface (OOB write, double-free, SIGFPE, infinite-loop DoS)#121
gburd wants to merge 5 commits into
masterfrom
security/pentest-review

Conversation

@gburd

@gburd gburd commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Security / penetration review — embedded core

Threat-model-scoped review of the embedded engine (untrusted on-disk data is
the primary surface) + ASan/UBSan fuzzing (reused & extended test/fuzz/).
Six confirmed vulnerabilities fixed, each with a before/after ASan proof and
a committed regression seed.
One further DoS documented and deferred (see
below). No speculative findings.

All six were found by fuzzing an ASan-instrumented libdb with
test/fuzz/fuzz_dbfile (open/verify/scan of an untrusted .db file).

Fixed

Sev Class Site Seed
HIGH OOB write / type confusion (memory corruption) __part_verify blind else__ham_open on a Heap-typed dbp → LOCK_INIT(&hcp->hlock) past an 88-byte HEAP_CURSOR dbfile_typeconf_part_verify.seed
MED Free of indeterminate pointer (double-free) __heap_vrfy frees uninitialized offsets on early datapage failure dbfile_doublefree_heap_vrfy.seed
MED DoS — infinite loop __bam_search fast-path descent has no child-level-decreases check; a self/ancestor P_IBTREE pointer spins forever dbfile_infloop_bam_search.seed
MED SIGFPE ÷0 heap region_size (0 / UINT32_MAX+1) as divisor in __heap_vrfy_meta/__heap_read_meta dbfile_fpe_heap_region_size.seed
MED SIGFPE ÷0 queue rec_page==0 as divisor in __qam_vrfy_meta/__qam_open dbfile_fpe_qam_recpage.seed

Every fix is a minimal, root-cause validation guard at the load/descent
chokepoint (mirroring the existing __db_ret_okitem pattern): reject bad input
with EINVAL/DB_VERIFY_BAD/DB_PAGE_NOTFOUND, no panic, no on-disk / log /
region / ABI change
, no valid-input rejection.

The HIGH type-confusion OOB write was invisible to the existing crash gate and
fuzz.yml because both link a non-ASan libdb.a; this PR fixes that gap —
check-crashes.sh now builds an ASan-only libdb (build_asan_gate/) and links
the standalone harnesses against it, so memory faults inside libdb are caught.

Deferred (documented, not force-patched)

  • Queue extent-scan DoS (__qam_vrfy_walkqueue, mirrored in
    qam_method/qam_stat): a crafted huge cur_recno makes the extent-file
    probe loop run for minutes. It terminates (not an infinite loop, not
    memory corruption) and a safe fix must be extent-aware so it doesn't reject
    valid large/wrapped queues. Root cause + scoped fix in the local review doc.
  • Weak IV RNG seed (__db_generate_iv): MT19937 seeded from
    hash(gettimeofday). AES-CBC mode/IV-uniqueness are correct; this is IV
    unpredictability only, a longstanding design characteristic, not a
    memory-safety bug.

Validation

  • check-crashes.sh (ASan gate): 9/9 seeds PASS (4 pre-existing + 5 new); each new seed FAILs the gate when its fix is reverted.
  • TCL: test001 btree/hash/heap/queue, test011 btree, test003 recno — PASS.
  • Recovery: recd005, recd015 (10 000-txn prepare/commit/discard) — PASS.
  • DST capstone (--enable-dst): test_sim_crash_recover / _rng / _torn — PASS (recovery/ACID intact; 0 silent-bad).
  • Bounded post-fix ASan/UBSan fuzzing: fuzz_dbfile 360 s, fuzz_recover 240 s, fuzz_api 180 s — no new memory faults.
  • Clean --enable-debug build, 0 warnings.

Posture

The core is solid on the happy path and mature; the concentrated weakness was
the untrusted-file parse/verify surface trusting meta-page scalars
(heap region_size, queue rec_page, partition type) before using them as
divisors / cursor-size selectors, plus one uninitialized-pointer free. These
extend the existing hardening track's validate-at-the-chokepoint pattern to
heap/queue/partition and add a corruption-termination guard to Btree descent.
After these fixes a single untrusted .db file no longer yields OOB write,
double-free, SIGFPE, or an infinite loop on the fuzzed paths.

gburd added 5 commits July 31, 2026 12:26
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 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by security/pentest-final — rebased onto master to drop the ~1000 docs-src files that #120 already merged; this PR is now ONLY the 6 engine security fixes + 5 regression crash-seeds.

@gburd gburd closed this Jul 31, 2026
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