fix(lock): heap OOB write in __lock_vec + eliminate the lock-mode enumeration class (#140) - #145
Merged
Conversation
|
ABI diff produced no report (build skipped or no base tag). Advisory: libabigail/nm is the authoritative binary-ABI check; Coccinelle is complementary source-level early warning. See dist/cocci/README.md. |
Coccinelle convention checksNo new violations. ✅ Resolved since baseline (2) -- update dist/cocci/baseline.txt to lock these in. |
`__lock_vec`'s `DB_LOCK_PUT_READ` / `DB_LOCK_UPGRADE_WRITE` handling sized the
temporary DBT descriptor array -- the array that becomes the replication commit
lock list -- from `sh_locker->nwrites`, while the population loop skipped only
the modes it named by hand (`DB_LOCK_READ`, `DB_LOCK_READ_UNCOMMITTED`).
`DB_LOCK_SIREAD` matched neither of those names nor `IS_WRITELOCK`, so a
retained SSI read marker fell through to the populate branch and consumed a
descriptor slot the sizing never allocated:
- a heap-buffer-overflow WRITE of sizeof(DBT) past the allocation. The only
bounds check was a `DB_ASSERT`, which compiles out of every non-DIAGNOSTIC
build, so release builds corrupted the heap silently; and
- `__lock_fix_list` was handed `nwrites` rather than the number of descriptors
actually written, truncating the serialized list. Because newly granted
locks go to the HEAD of the locker's `heldby` list, the SIREAD object was
visited first and could displace the modified page's write-lock object.
`__rep_process_txn` reacquires only the listed objects as write locks, so
apply could change a page a separate client transaction still read-locked --
an isolation violation on the replication apply path.
Fix, in three parts that make the invariant hold by construction:
- Size the array by counting `IS_WRITELOCK` locks on `heldby`, the same
predicate the populate branch now uses, so sizing and population cannot
disagree for any present or future mode.
- Gate the populate branch on `IS_WRITELOCK(lp->mode)`. The list exists so
apply can reacquire WRITE locks; a read marker has no business in it. The
set of modes this path *releases* is deliberately unchanged: SIREAD markers
must stay on `heldby` for `__lock_sicommit` at `__txn_end`.
- Pass `__lock_fix_list` the count actually populated, not a separately
maintained counter.
The `DB_ASSERT` bounds check is promoted to a real runtime guard that fails the
operation via `__env_panic` in every build, since a future sizing/population
skew would be a memory-safety bug precisely where `DB_ASSERT` is absent.
Also fixes three SIREAD omissions of the same class found by the audit:
`__lock_printlock` and `__db_lockmode_to_string` printed SIREAD as `UNKNOWN`,
and `__lock_dump_object` walked only `holders`/`waiters`, so an object pinned
solely by SIREAD markers printed as empty.
Regression test: `test/c/test_lock_sireads.c` + `test/c/chk.locksireads`, which
asserts both halves under ASan -- no heap overflow, and that the serialized
commit lock list names the page the transaction modified.
Fixes #140
Issue #140 was not an SSI bug. It was the consequence of adding a lock MODE (`DB_LOCK_SIREAD`) to a 30-year-old enum without revisiting every pre-existing site that enumerates lock modes exhaustively. Nothing in the tree prevented a recurrence, so add three guards -- two static, one empirical -- and write the discipline down. `dist/cocci/lockmode_inventory.{sh,txt}` -- the AUTHORITATIVE guard, blocking and deliberately NOT baselined. Three hard checks: 1. The `db_lockmode_t` members in `src/dbinc/db.in` must exactly match the recorded mode set. Adding a mode fails CI until every `site` line has a recorded verdict. 2. Every inventoried enumeration site must still exist (a rename would otherwise silently drop it out of review). 3. Every switch marked `exhaustive` must have a `case` arm for EVERY mode. This is the check #140's class cannot evade. The inventory records all 19 mode-enumeration sites across `src/lock/`, `src/db/`, and `src/txn/` with a per-site verdict and the reason, so the judgement can be re-checked rather than re-derived. `dist/cocci/rule_lock_mode_enum.cocci` -- two expression-level shapes, wired into the existing baseline gate so NEW matches fail: - `LOCK_MODE_SIZING`: an allocation sized from `->nwrites`. Verified to match the #140 bug exactly on the pre-fix source, and at zero after it. - `LOCK_MODE_READTEST`: a hand-enumerated read-mode test, which is silently incomplete the moment another non-write mode exists. The five existing sites are deliberately mode-specific and are baselined. Coccinelle CANNOT express "a switch over `db_lockmode_t` missing a case" in this spatch build (`... when != case X:` inside a switch is a parse error, spatch 1.3.1). That is why the exhaustive-switch check lives in the inventory script rather than in SmPL; the limitation is documented in both files. `lock-mode-asan` job -- the empirical half: builds an ASan libdb and runs `test/c/chk.locksireads`, which asserts no heap overflow AND that the serialized commit lock list names the modified page. `rfc/0003/lock-mode-audit.md` -- the rule ("a write-lock counter must never size a buffer that a mode-enumerating loop fills"), the shape to write, the seven-item checklist for adding a mode, and what each guard enforces. Verified: each of the three inventory checks fails on a synthetic regression (new mode, removed case arm, renamed site) and passes when restored.
gburd
force-pushed
the
work/fix140-audit
branch
from
September 6, 2026 22:41
b7daf37 to
22ed648
Compare
test/tiers/meson.build compiled the isolation/soak/lock-matrix drivers with include_directories: inc alone, but db.h/db_int.h are custom_target outputs whose directory meson only wires in when they are listed as SOURCES of the target (test/pbt/meson.build already does exactly this). Entered from the root meson.build, 'inc's relative '.' does not resolve to dist/'s build dir, so the drivers failed with 'fatal error: db.h: No such file or directory' -- which broke the hegel/PBT CI job, since it builds the whole meson tree. Pass db_h/db_int_h/db_int_def_h as sources, matching test/pbt. Verified: all three tier drivers configure, compile and link under meson (282/282).
gburd
added a commit
that referenced
this pull request
Sep 7, 2026
The tiers were deliberately advisory while the bugs they reproduce were open, with their own comments saying to flip them in the fixing PR. Both conditions are now met: - B3 (lock-mode matrix under ASan) reproduced the #140 heap overflow in __lock_vec; #145 fixed the lock-list sizing and the matrix passes, so any ASan fault in the lock list is now a real regression. - B2 (resource-accounting soak) reproduced #137/#138; this PR fixes them and the tier reports 5 workloads / 0 unexpected outcomes. Dropping continue-on-error from both. B1 was already a hard gate. Verified on this PR: B1 pass, B3 pass.
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.
Summary
Fixes #140: a heap out-of-bounds write in
__lock_vecand the incompletereplication commit lock list that follows from it. Also eliminates the
root-cause class — adding a
DB_LOCK_*mode without auditing thepre-existing sites that enumerate lock modes — with a CI-enforced guard.
The bug reproduces exactly as reported. Verified before/after under ASan:
And the isolation half — the commit lock list content:
Part 1 — the fix, and why this one
__lock_vec'sDB_LOCK_PUT_READpath sized the descriptor array fromsh_locker->nwrites, while the population loop skipped only the modes it namedby hand (
DB_LOCK_READ,DB_LOCK_READ_UNCOMMITTED).DB_LOCK_SIREADmatchedneither those names nor
IS_WRITELOCK, so it fell through and consumed a slotthe sizing never allocated.
The brief listed two candidate fixes. I took (b) — exclude SIREAD from the
list — and rejected (a) sizing by
nlocks, because (a) fixes only theoverflow and leaves the isolation bug in place: the list would then be
correctly sized but would still contain SIREAD objects, and
__rep_process_txnreacquires every listed object asDB_LOCK_WRITE. Handingapply a write lock on a page nobody wrote is at best spurious blocking, at
worst a new correctness problem. A SIREAD marker is an SSI read marker; it has
no business in a list whose purpose is "reacquire the write locks".
Three changes, so that sizing and population agree by construction rather
than by coincidence:
IS_WRITELOCKlockson
heldby. Notnwrites: that counter only counts write locks whosestatus is
DB_LSTAT_HELD, and says nothing about the non-write modes theloop retains.
IS_WRITELOCK(lp->mode)rather than addingDB_LOCK_SIREADto the skip list. Naming one more mode would have fixedthis bug and left the next one;
IS_WRITELOCKcovers every future mode.__lock_fix_listthe count actually populated (np - data), not aseparately maintained counter.
What I deliberately did NOT change: the set of modes this path releases.
SIREAD markers must stay on
heldbypastDB_LOCK_PUT_READso__lock_sicommitcan persist-or-drop them at__txn_end. Releasing them herewould have been a smaller diff and would have broken SSI. The bug was never
"SIREAD is retained" — it was that a retained non-write lock silently entered a
list sized only for write locks.
The
DB_ASSERTbounds check is promoted to a real runtime guard(
__env_panic), since a future sizing/population skew is a memory-safety bugprecisely in the builds where
DB_ASSERTis absent.Part 2 — the audit (19 sites)
lock.c__lock_vecfix_listcountIS_WRITELOCKlock_stat.c__lock_printlockUNKNOWNdb_pr.c__db_lockmode_to_stringUNKNOWN LOCK MODElock_stat.c__lock_dump_objectholders+waitersonlysireaderslock.c__lock_get_internalnwrites++underIS_WRITELOCK; SIREAD viasafe_siarmslock.c__lock_freelocknlocks/nwritesunderIS_WRITELOCKlock.c__lock_downgradenwrites--only on write→non-writelock.c__lock_inherit_locksIS_WRITELOCKlock.c__lock_tradeIS_WRITELOCKlock.c__lock_put_internalsireaders, elseholderslock.c__lock_sicommitheldbyfor SIREAD onlylock.c__lock_siclean_objsireaders(all SIREAD)lock_deadlock.c__dd_buildlock_failchk.c__lock_failchknlocks == nwrites⇒ "no non-write locks"nlocksonly, so a marker-holding locker is not skippedlock_region.c__lock_region_initdb_riw_conflicts10×10 incl. SI row+columnlock_mode >= nmodesrejects an unmatrixed modelock_list.c__lock_fix_listlock_list.c__lock_get_listdb_meta.c__db_lgetREAD→SIREAD; coupling tests name single modesdb_meta.c__db_lputtxn_util.c__txn_doeventsIS_WRITELOCKon handle locksAlso checked and found not to be mode enumerations:
lock_util.c(hashingonly),
lock_id.c(nlocks/nwritesinit + thesi_refdeferral),__lock_promote(conflict matrix),lang/tcl/tcl_lock.c(exposes only the 6classic modes to Tcl by design). Every allocation in
src/lock/was inspected;__lock_vecwas the only one keyed on a mode-dependent count.Each verdict, with its reason, is committed in
dist/cocci/lockmode_inventory.txtso it can be re-checked rather thanre-derived.
Part 3 — the permanent guard
Coccinelle (
dist/cocci/rule_lock_mode_enum.cocci), wired into the existingbaseline gate so NEW matches fail CI:
LOCK_MODE_SIZING— an allocation sized from->nwrites. Verified tomatch the libdb 5.3.34: incomplete replication commit lock lists can violate client transaction isolation #140 bug exactly on the pre-fix source, and at zero after it.
LOCK_MODE_READTEST— a hand-enumerated read-mode test. 5 existing sites,all deliberately mode-specific, baselined.
Honest limitation: Coccinelle cannot express "a
switchoverdb_lockmode_tthat is missing acase" in this spatch build —... when != case X:inside aswitchis a parse error (spatch 1.3.1). I tried severalformulations. So the switch check lives in the alternative deliverable:
Checked inventory (
dist/cocci/lockmode_inventory.{sh,txt}) — blocking anddeliberately not baselined. Three hard checks:
db_lockmode_tindb.inmust exactly match the recorded mode set.exhaustivemust have acasearm for every mode.Each check is verified to fail on a synthetic regression and pass when
restored:
ASan job (
lock-mode-asan) — the empirical half: builds an ASan libdb andruns
test/c/chk.locksireads, asserting no heap overflow and that theserialized commit lock list names the modified page.
rfc/0003/lock-mode-audit.md— the rule ("a write-lock counter must neversize a buffer that a mode-enumerating loop fills"), the shape to write, a
7-item checklist for adding a mode, and what each guard enforces.
Validation
--enable-debug --enable-test--enable-debug --enable-diagnosticCFLAGS=-O2)-fsanitize=address)test/c/chk.locksireadstest/fuzz/check-crashes.shdist/s_includeheader driftMessage ID 2056 is the next free in the lock/mutex range.
Fixes #140