From 3dad5d57d742dd52695b20bb50c36311a9a119f4 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Fri, 31 Jul 2026 12:26:51 -0400 Subject: [PATCH 1/5] fix(security): reject non-Btree/Hash type in __part_verify (OOB write) 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. --- src/db/partition.c | 24 +++++++++++++++--- .../crashes/dbfile_typeconf_part_verify.seed | Bin 0 -> 732 bytes 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 test/fuzz/crashes/dbfile_typeconf_part_verify.seed diff --git a/src/db/partition.c b/src/db/partition.c index 9c43951ef..f6c6b990b 100644 --- a/src/db/partition.c +++ b/src/db/partition.c @@ -1822,16 +1822,32 @@ __part_verify(dbp, vdp, fname, handle, callback, flags) dbc = NULL; ip = vdp->thread_info; - if (dbp->type == DB_BTREE) { + if (dbp->type == DB_BTREE || dbp->type == DB_RECNO) { if ((ret = __bam_open(dbp, ip, NULL, fname, PGNO_BASE_MD, flags)) != 0) goto err; } #ifdef HAVE_HASH - else if ((ret = __ham_open(dbp, ip, - NULL, fname, PGNO_BASE_MD, flags)) != 0) - goto err; + else if (dbp->type == DB_HASH) { + if ((ret = __ham_open(dbp, ip, + NULL, fname, PGNO_BASE_MD, flags)) != 0) + goto err; + } #endif + else { + /* + * Only Btree/Recno and Hash databases can be partitioned. A + * corrupt/hostile file whose meta page claims another type (e.g. + * Heap or Queue) while setting the partition flag must not be + * opened with the Hash access method: __db_cursor would allocate + * a cursor sized for dbp->type, which __ham_get_meta then casts + * to HASH_CURSOR, writing its hlock field past the end of the + * smaller allocation (a heap-buffer-overflow / type confusion). + * Reject the unexpected type instead. + */ + ret = __db_unknown_type(env, "__part_verify", dbp->type); + goto err; + } /* * Initalize partition db handles and get the names. Set DB_RDWRMASTER diff --git a/test/fuzz/crashes/dbfile_typeconf_part_verify.seed b/test/fuzz/crashes/dbfile_typeconf_part_verify.seed new file mode 100644 index 0000000000000000000000000000000000000000..5ae29e65660ee68121eabf40d5a07a55bec9f4eb GIT binary patch literal 732 zcmZQzU|?VbVi0I@Wrwht82(TI7#R2<<}pDi6!KbT0H+F2q?LIkiuj=BfC32S8FmH^ oAar14;D5n*fpG`p0>%!;0>%hNpvaH~c38oX^$y74Ir)hx0K=b3<^TWy literal 0 HcmV?d00001 From f9b00fd8546790b915a7406c5b2dd03fc6797b9d Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Fri, 31 Jul 2026 12:27:03 -0400 Subject: [PATCH 2/5] fix(security): init offsets in __heap_vrfy (free of indeterminate pointer) __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. --- src/heap/heap_verify.c | 22 ++++++++++++++++++ .../crashes/dbfile_doublefree_heap_vrfy.seed | Bin 0 -> 1647 bytes 2 files changed, 22 insertions(+) create mode 100644 test/fuzz/crashes/dbfile_doublefree_heap_vrfy.seed diff --git a/src/heap/heap_verify.c b/src/heap/heap_verify.c index 4f121d106..6b85c16c3 100644 --- a/src/heap/heap_verify.c +++ b/src/heap/heap_verify.c @@ -71,6 +71,19 @@ __heap_vrfy_meta(dbp, vdp, meta, pgno, flags) h = (HEAP *)dbp->heap_internal; h->region_size = meta->region_size; last_pgno = meta->dbmeta.last_pgno; + /* + * region_size is used as a divisor (HEAP_REGION_SIZE(dbp)+1) below and + * in the structure pass; a corrupt 0 or UINT32_MAX value would divide + * by zero (the +1 wraps). Reject it as bad rather than crash. + */ + if (meta->region_size == 0 || + meta->region_size > HEAP_REGION_COUNT(dbp, dbp->pgsize)) { + EPRINT((dbp->env, DB_STR_A("1174", + "Page %lu: invalid heap region size %lu", + "%lu %lu"), (u_long)pgno, (u_long)meta->region_size)); + isbad = 1; + goto err; + } if (meta->nregions != HEAP_REGION_NUM(dbp, last_pgno)) { EPRINT((dbp->env, DB_STR_A("1157", "Page %lu: Number of heap regions incorrect", @@ -124,6 +137,15 @@ __heap_vrfy(dbp, vdp, h, pgno, flags) int cnt, i, j, ret; db_indx_t *offsets, *offtbl, end; + /* + * offsets is freed unconditionally at the err label. If + * __db_vrfy_datapage below fails on a corrupt page we jump there + * before offsets is assigned, so it must start NULL (a free of an + * indeterminate pointer is otherwise undefined behavior / a wild + * free on a hostile heap file). + */ + offsets = NULL; + if ((ret = __db_vrfy_datapage(dbp, vdp, h, pgno, flags)) != 0) goto err; diff --git a/test/fuzz/crashes/dbfile_doublefree_heap_vrfy.seed b/test/fuzz/crashes/dbfile_doublefree_heap_vrfy.seed new file mode 100644 index 0000000000000000000000000000000000000000..d63fb73a00452e57728519f7fc96eef7b4c044a1 GIT binary patch literal 1647 zcmZQzU|?VbVi0I@WrwhtfE+%s5Ce#b4z6VeaH;?WTbWn>CjdSK^GY(<;R7JUu^EVN zDP8y?3>iRsK;gm2zz(-@27(C}8)OVl65OLff?6b{vAjqe@|gd~(>AJ$Rv`dM=?K$V z7#{F5vi{)z!2g2(0Y5?<#`;x#p97ThpL#vqeB!MkOc-8DC;&xZr3qYelrbDaKnMts a`~gcjNc95~g8)44S&)=MnUy*Di75ab9Z7Eh literal 0 HcmV?d00001 From 71c5c07d272dc6a92a7cf432cc6a33e331e90ec6 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Fri, 31 Jul 2026 12:27:17 -0400 Subject: [PATCH 3/5] fix(security): guard btree descent level in __bam_search (infinite-loop 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). --- src/btree/bt_search.c | 18 ++++++++++++++++++ .../crashes/dbfile_infloop_bam_search.seed | Bin 0 -> 8192 bytes 2 files changed, 18 insertions(+) create mode 100644 test/fuzz/crashes/dbfile_infloop_bam_search.seed diff --git a/src/btree/bt_search.c b/src/btree/bt_search.c index 236e32dba..19ac4db5d 100644 --- a/src/btree/bt_search.c +++ b/src/btree/bt_search.c @@ -1066,6 +1066,24 @@ skip_lock: stack = set_stack; if ((ret = __memp_fget(mpf, &pg, dbc->thread_info, dbc->txn, get_mode, &h)) != 0) goto err; + /* + * On an untrusted/corrupt file a BINTERNAL child pointer can + * point back up the tree (to itself, a sibling, or an ancestor) + * at the same or a higher level. The descent then never reaches + * LEAFLEVEL and this loop spins forever (a denial of service). + * A valid Btree always has strictly decreasing levels from root + * to leaf, so a child whose level is not below its parent's is + * corruption -- reject it as a clean page error rather than loop. + * (The lock-retry path above already enforces LEVEL(h)==level-1; + * this guards the common latch-coupling fast path.) + */ + if (LEVEL(h) >= level) { + (void)__memp_fput(mpf, + dbc->thread_info, h, dbc->priority); + h = NULL; + ret = DB_PAGE_NOTFOUND; + goto err; + } /* Release the parent. */ if (parent_h != NULL && (ret = __memp_fput(mpf, dbc->thread_info, parent_h, dbc->priority)) != 0) diff --git a/test/fuzz/crashes/dbfile_infloop_bam_search.seed b/test/fuzz/crashes/dbfile_infloop_bam_search.seed new file mode 100644 index 0000000000000000000000000000000000000000..2aa08c9bbefe3088598cf4ea31a628f37374a8f6 GIT binary patch literal 8192 zcmZQzU|?VbVh~6&WM$w4F$91Z%s|M2gkj)U^?eQ%Am^#q!_6n&8p4Dbn4l~LAPqAU zE;-5=4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=CksJcBj&hSLJEYSL>P7QG zLIl>KMh>5AnE{-j&UY*G$`8T3l3*#288CA|0yEgb!juqmDbYNtm)H>a&(O92s1_E1 zND@6d46rLqLQf zBQ>$0C^b31C`H#?w;-`HCqFTTfdNI_42QTW4sjD4;>Kv=|8WACi;U28z{Cx4h#Mfp zv3hq@a5Mx)LttP-;Qzo{KWh7E2#kinXb6mkz-S2I4*^yN# Date: Fri, 31 Jul 2026 12:27:31 -0400 Subject: [PATCH 4/5] fix(security): reject zero divisor meta fields in heap/queue open+verify (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. --- src/heap/heap_open.c | 18 ++++++++++++++++++ src/qam/qam_open.c | 14 ++++++++++++++ src/qam/qam_verify.c | 7 +++++-- .../crashes/dbfile_fpe_heap_region_size.seed | Bin 0 -> 673 bytes test/fuzz/crashes/dbfile_fpe_qam_recpage.seed | Bin 0 -> 1283 bytes 5 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 test/fuzz/crashes/dbfile_fpe_heap_region_size.seed create mode 100644 test/fuzz/crashes/dbfile_fpe_qam_recpage.seed diff --git a/src/heap/heap_open.c b/src/heap/heap_open.c index 6eee3dfa8..9f0c1f8be 100644 --- a/src/heap/heap_open.c +++ b/src/heap/heap_open.c @@ -172,6 +172,24 @@ __heap_read_meta(dbp, ip, txn, meta_pgno, flags) * metadata page will be created/initialized elsewhere. */ if (meta->dbmeta.magic == DB_HEAPMAGIC) { + /* + * region_size comes from the (possibly corrupt) on-disk meta + * page and is used as a divisor via HEAP_REGION_SIZE(dbp)+1 in + * HEAP_REGION_PGNO / HEAP_REGION_NUM. A region_size of 0 or of + * UINT32_MAX (so the +1 wraps to 0) would divide by zero; any + * value larger than the per-page region count is impossible for + * this page size. Reject it before it is trusted, mirroring the + * bound __heap_new_file already enforces on creation. + */ + if (meta->region_size == 0 || + meta->region_size > HEAP_REGION_COUNT(dbp, dbp->pgsize)) { + __db_errx(dbp->env, DB_STR_A("1169", + "region size may not be larger than %lu", + "%lu"), + (u_long)HEAP_REGION_COUNT(dbp, dbp->pgsize)); + ret = EINVAL; + goto err; + } h->curregion = meta->curregion; h->curpgindx = 0; h->gbytes = meta->gbytes; diff --git a/src/qam/qam_open.c b/src/qam/qam_open.c index 68617ef5c..3c0618c56 100644 --- a/src/qam/qam_open.c +++ b/src/qam/qam_open.c @@ -106,6 +106,20 @@ __qam_open(dbp, ip, txn, name, base_pgno, mode, flags) t->re_len = qmeta->re_len; t->rec_page = qmeta->rec_page; + /* + * rec_page (records per page) is trusted from the on-disk meta page + * and used as a divisor throughout the queue access method via + * QAM_RECNO_PAGE (records/page). A corrupt 0 divides by zero (SIGFPE). + * A valid queue always has at least one record per page. + */ + if (t->rec_page == 0) { + __db_errx(env, DB_STR_A("1136", + "__qam_open: %s: unexpected file type or format", "%s"), + name); + ret = EINVAL; + goto err; + } + t->q_meta = base_pgno; t->q_root = base_pgno + 1; diff --git a/src/qam/qam_verify.c b/src/qam/qam_verify.c index b887bedf1..85b2509b6 100644 --- a/src/qam/qam_verify.c +++ b/src/qam/qam_verify.c @@ -81,9 +81,12 @@ __qam_vrfy_meta(dbp, vdp, meta, pgno, flags) /* * re_len: If this is bad, we can't safely verify queue data pages, so - * return DB_VERIFY_FATAL + * return DB_VERIFY_FATAL. rec_page (records per page) must be non-zero: + * it is used as a divisor via QAM_RECNO_PAGE below and throughout the + * queue AM, so a corrupt 0 would divide by zero (SIGFPE). */ - if (DB_ALIGN(meta->re_len + sizeof(QAMDATA) - 1, sizeof(u_int32_t)) * + if (meta->rec_page == 0 || + DB_ALIGN(meta->re_len + sizeof(QAMDATA) - 1, sizeof(u_int32_t)) * meta->rec_page + QPAGE_SZ(dbp) > dbp->pgsize) { EPRINT((env, DB_STR_A("1147", "Page %lu: queue record length %lu too high for page size and recs/page", diff --git a/test/fuzz/crashes/dbfile_fpe_heap_region_size.seed b/test/fuzz/crashes/dbfile_fpe_heap_region_size.seed new file mode 100644 index 0000000000000000000000000000000000000000..2095b80f53a6c45eb1c50f9fa19f246cd569ea0a GIT binary patch literal 673 zcmZQzU|?VbVi0I@WrwhtfE+%s5Ce#b4z6VeaH;?WTbWn>CjdSK^GY(<;REP~4?eyK zLk7@Mpg>||V23+z27(C}8)OVl65OLff?6V#3IPFoQD?7v%kjZ=yT_7epxRx2f zsR9&iWnTFo445Dk7V<+duOx#V2%sB6F`tuS6KG|?4=NdqB|Py8fvm-rD3F}vfW#gc zCMdD_ Date: Fri, 31 Jul 2026 12:28:03 -0400 Subject: [PATCH 5/5] test(fuzz): ASan-instrumented crash gate + heap/queue corpus seeds 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. --- .gitignore | 1 + test/fuzz/check-crashes.sh | 33 ++++++++++++++++++++++++- test/fuzz/corpus/dbfile/valid_heap.db | Bin 0 -> 1536 bytes test/fuzz/corpus/dbfile/valid_queue.db | Bin 0 -> 1024 bytes test/fuzz/crashes/README.md | 25 +++++++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 test/fuzz/corpus/dbfile/valid_heap.db create mode 100644 test/fuzz/corpus/dbfile/valid_queue.db diff --git a/.gitignore b/.gitignore index 3ee23ede1..aaa2a6ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ build_unix/** build_asan/** +build_asan_gate/** compile_commands.json test/tcl/tclIndex diff --git a/test/fuzz/check-crashes.sh b/test/fuzz/check-crashes.sh index d154ce83a..7b221f269 100755 --- a/test/fuzz/check-crashes.sh +++ b/test/fuzz/check-crashes.sh @@ -17,8 +17,18 @@ # DB_ASSERT in __memp_fopen, or a recovery-failure panic), which is by-design # diagnostic behavior, not the OOB/FPE crash class this gate guards against. # +# libdb ASan instrumentation: +# Some crash classes (a heap-buffer-overflow / use-after-free / double-free +# *inside* libdb's own allocations -- e.g. the __part_verify type-confusion +# OOB write) are only observable when libdb itself is compiled with +# AddressSanitizer; a harness-only ASan build (libdb.a plain) cannot see +# them. If a build_unix built with `CFLAGS=-fsanitize=address` (ASan only, +# NOT undefined -- UBSan flags libdb's pervasive base+offset pointer idioms) +# is available, point LIBDB_BUILD at it to catch those. This gate +# auto-builds one under build_asan_gate/ when LIBDB_ASAN=1 (default on). +# # Usage: ./check-crashes.sh -# Env: CC, LIBDB_BUILD (see run.sh) +# Env: CC, LIBDB_BUILD (see run.sh), LIBDB_ASAN (1=build+use an ASan libdb) # # Run from test/fuzz/ inside a `nix develop` shell. @@ -27,6 +37,27 @@ set -eu HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) cd "$HERE" +CC=${CC:-clang} +LIBDB_ASAN=${LIBDB_ASAN:-1} + +# Build (once) an ASan-instrumented libdb so a memory fault *inside* libdb is +# caught, then link the standalone harnesses against it. ASan only -- UBSan +# would fire on libdb's legitimate base+offset pointer arithmetic. The +# harness's own SAN flags in run.sh still add UBSan to the harness .c, so we +# neutralise it for the lib by exporting an ASan-only LIBDB build here. +if [ "$LIBDB_ASAN" = "1" ] && [ -z "${LIBDB_BUILD:-}" ]; then + GATE_BUILD="$HERE/../../build_asan_gate" + if [ ! -f "$GATE_BUILD/libdb.a" ]; then + mkdir -p "$GATE_BUILD" + ( cd "$GATE_BUILD" && + ../dist/configure --enable-debug \ + CC="$CC" CFLAGS="-fsanitize=address -g -O1" >configure.log 2>&1 && + make -j4 >build.log 2>&1 ) || + { echo "warning: ASan libdb build failed; falling back to plain lib" >&2; } + fi + [ -f "$GATE_BUILD/libdb.a" ] && export LIBDB_BUILD="$GATE_BUILD" +fi + # Build the standalone (no-libFuzzer) drivers for every harness once. FUZZ_STANDALONE=1 ./run.sh build diff --git a/test/fuzz/corpus/dbfile/valid_heap.db b/test/fuzz/corpus/dbfile/valid_heap.db new file mode 100644 index 0000000000000000000000000000000000000000..364aaf7230e7b04cd8f4ab37449e952b25d5dffe GIT binary patch literal 1536 zcmZQzU|?VbVi0I@WrwhtfE+%s5Ce#b4z6VeaH;?WTbWmW2 z83kBFK!hP9HL;*5H95a1Mb})nAh9whKQV=Y0Y%&lhqx&YaT6Tk#yG@{aEKe?5I2B{ F0|0SWNJjtw literal 0 HcmV?d00001 diff --git a/test/fuzz/corpus/dbfile/valid_queue.db b/test/fuzz/corpus/dbfile/valid_queue.db new file mode 100644 index 0000000000000000000000000000000000000000..5af96bc7df3ba0bde76ebece62d892cf7cd10df3 GIT binary patch literal 1024 zcmZQzU|?VbVh{*cVqstbF_?gu3&g+<9L@xCssN=V&8Ht|DUbmx0-3=M!~#I90K^hN zJPL+J2!P@amImN1=4N1EE-Xq-Hh_zO8DKF(EMi7j#Eh|snP3q!MHL(6(Jur5WE>D| literal 0 HcmV?d00001 diff --git a/test/fuzz/crashes/README.md b/test/fuzz/crashes/README.md index 537d8a911..6b15db50c 100644 --- a/test/fuzz/crashes/README.md +++ b/test/fuzz/crashes/README.md @@ -58,6 +58,31 @@ masking them. Both are the same trust-a-length-from-the-file class and are | `dbfile_fpe_bam_minkey.seed` | SIGFPE (divide-by-zero) | `__bamc_refresh` `src/btree/bt_cursor.c:285`, via `B_MINKEY_TO_OVFLSIZE` on btree open/cursor-init | The btree meta page's `minkey` field is 0; it is used as a divisor. | Reject `minkey < 2` when loaded in `__bam_read_root` (`bt_open.c`), matching what verify and the public setter already require. | | `recover_oob_read_log_chksum.seed` | ASan OOB read | `__ham_func4` `src/hash/hash_func.c:171`, via `__db_check_chksum` ← `__log_valid` `src/log/log.c:818` | A corrupt log header's `hdr->len` makes the checksum hash read past the fixed-size `persist` record buffer. The crypto path already bounded this; the non-crypto path did not. | Add the same `hdr->len - hdrsize == recsize` bound (and underflow guard) on the non-crypto path in `__log_valid`. | +## Findings from the security / pentest review (2026-07) + +Five more, all on the untrusted `.db`-file parse/verify surface, found by +fuzzing an **ASan-instrumented** libdb (a heap-buffer-overflow *inside* libdb's +own allocations is invisible to a plain-lib harness -- see the ASan-gate note +below). All are **FIXED** in this PR; each ships a regression seed. + +| Seed | Fault | Site | Fix | +|------|-------|------|-----| +| `dbfile_typeconf_part_verify.seed` | ASan heap-buffer-overflow (8-byte WRITE) | `__db_lget` via `__ham_get_meta` <- `__ham_open` <- `__part_verify` `partition.c` | Type confusion: a non-Btree (e.g. Heap) file with the partition flag was opened with the Hash AM but a Heap-sized cursor internal -> `LOCK_INIT(&hcp->hlock)` writes past the 88B alloc. Dispatch by exact type; reject non-Btree/Recno/Hash. | +| `dbfile_doublefree_heap_vrfy.seed` | ASan double-free / free of indeterminate ptr | `__heap_vrfy` `heap_verify.c` | `offsets` freed at `err:` while uninitialized (early `__db_vrfy_datapage` failure jumps there). `offsets = NULL;` at decl. (was the OPEN item in `fuzz-found-bugs.md`.) | +| `dbfile_infloop_bam_search.seed` | DoS -- infinite loop | `__bam_search` `bt_search.c` fast-path child fetch | A `P_IBTREE` child pointer to itself/an ancestor at same-or-higher level spins the descent forever. Guard `LEVEL(child) >= LEVEL(parent)` -> `DB_PAGE_NOTFOUND` (levels must strictly decrease). | +| `dbfile_fpe_heap_region_size.seed` | SIGFPE (divide-by-zero) | `__heap_vrfy_meta` `heap_verify.c` (`HEAP_REGION_NUM`) | heap meta `region_size` 0 or UINT32_MAX (`+1` wraps) used as divisor. Reject at open+verify. | +| `dbfile_fpe_qam_recpage.seed` | SIGFPE (divide-by-zero) | `__qam_vrfy_meta` `qam_verify.c` (`QAM_RECNO_PAGE`) | queue meta `rec_page` 0 used as divisor. Reject at open+verify. | + +> **ASan gate:** `check-crashes.sh` now builds an ASan-only libdb under +> `build_asan_gate/` (gitignored) and links the standalone harnesses against +> it, so a memory fault *inside* libdb (e.g. the `__part_verify` OOB write) is +> caught -- a plain-lib harness cannot see it. Set `LIBDB_ASAN=0` to skip. + +A sixth finding (a **bounded** queue extent-scan DoS in `__qam_vrfy_walkqueue` +on a crafted huge `cur_recno`) is documented in `.agents/security-review.md` +and DEFERRED: it terminates, and a safe fix must be extent-aware so it does not +reject valid large/wrapped queues. + ### Still open (documented, not fixed here) `fuzz_recover` / verify-on-a-corrupt-file additionally show a **memory leak on