fix(security): harden untrusted .db parse/verify surface - #122
Merged
Conversation
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.
Coccinelle convention checksNo new violations. ✅ Resolved since baseline (2) -- update dist/cocci/baseline.txt to lock these in. |
ABI diff vs
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security review of the embedded core's untrusted on-disk parse/verify surface (a hostile/corrupt
.dbfile 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 withEINVAL/DB_VERIFY_BAD/DB_PAGE_NOTFOUND, no panic, no on-disk/log/region/ABI change, no valid-input rejection.__part_verifyblindly called__ham_openon a Heap/Queue-typed dbp →LOCK_INITwrites past an 88-byte HEAP_CURSOR (partition.c)__heap_vrfyfreed uninitializedoffsetson early datapage failure (heap_verify.c)__bam_searchfast-path descent lacked a child-level-decrease check → pointer spins forever (bt_search.c)region_sizeas divisor (heap_open.c)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.creverted + a clean ASan rebuild,dbfile_typeconf_part_verify.seedFAILs (heap-buffer-overflow WRITE); with the fix, all 9 crash seeds PASS. Regression:test001btree/hash/heap/queue +test025recno 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-instrumentedlibdb.a. 5 new seeds wired intocheck-crashes.sh(9/9 PASS, each FAILs when its fix is reverted) + heap/queue corpus seeds.Deferred (documented in the review notes)
getrandom).Bottom line: after these fixes a single hostile
.dbfile 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.