Skip to content

DST depth: write-back crash model, 3 caught planted bugs, 14 scenarios - #54

Closed
gburd wants to merge 4 commits into
masterfrom
agent/dst-depth
Closed

DST depth: write-back crash model, 3 caught planted bugs, 14 scenarios#54
gburd wants to merge 4 commits into
masterfrom
agent/dst-depth

Conversation

@gburd

@gburd gburd commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

DST depth: real write-back crash model, 3 caught planted bugs, 14 scenarios

Deepens the Deterministic Simulation Testing framework (Tier A1+A2+A3 of the
maturity plan). Everything is behind --enable-dst; the OFF library exports
0 __db_sim_* symbols and compiles the __os_* hooks to the stock path.

A1 — write-back durable-frontier crash model, fully wired

A simulated crash now genuinely drops bytes written-but-not-fsynced:
__db_sim_wb_crash() truncates each tracked real file back to its durable
frontier (last fsync), so a commit acked without its log being fsync'd is
detectably lost after crash+recover. The write-side ENOSPC and torn-write knobs
are consumed by __os_io's write fast path; the read fast path is offset-aware
(corrupt bit-flip + a ported stale-read ring); a per-I/O latency hook consumes
the latency knob.

A2 — planted-bug harness (the "DST finds real bugs" proof)

Three known durability bugs planted at real library sites, each caught by a
scenario within K=1 seeds (test/sim/dst-bug-inject.sh builds a dedicated
library per bug and asserts the catch):

Bug Site Caught by
1 NODURABLE __log_flush_int skips the log fsync, still acks test_sim_crash_recover — 64 "committed" txns lost
2 NOCKSUM __db_check_chksum ignores a checksum mismatch test_sim_torn — SILENT-BAD reads
3 LOSTUPDATE __memp_pgwrite skips a dirty-page write, acks test_sim_ckp_crash — flushed records lost

A normal build compiles all three out and every scenario passes.

A3 — scenario catalog grown 3 → 14

btree/hash/recno/queue op+crash+recover, checkpoint/page-flush durability, torn
log, ENOSPC degradation, txn abort atomicity, recovery idempotency, sorted
dups, overflow records, btree split/merge churn. Each: seeded workload → inject
fault → recover → assert invariant; deterministic (same seed → same outcome,
verified). 14/14 pass from a fresh build; each crash/fault scenario passes
an 8-seed sweep.

A4-lite

dst-sweep.sh (seed-range swarm) and dst-bug-inject.sh (bug-detection-latency
yardstick).

Honest gaps (documented in DESIGN.md §4)

Stale-read ring is consumed on read but has no dedicated scenario (needs
offset-threaded write-side snapshot). Determinism-guard planting at
__os_gettime/__os_id deferred (read on legitimate recovery paths; earns its
keep with the v2 scheduler). os_aio*/__os_physwrite hooks and meson wiring
remain follow-ups.

Verified: fresh --enable-dst build → 14/14 scenarios PASS; OFF build → 0 DST
symbols; dst-bug-inject.sh 16 → all 3 bugs caught at seed 1.

gburd added 4 commits July 27, 2026 13:53
…os_io

Make a simulated crash actually DROP bytes written-but-not-fsynced so an
ack-before-fsync durability bug is detectable after crash+recover:

- __db_sim_wb_crash() truncates every tracked real file back to its
  durable frontier (last fsync), keyed by a stable file-name hash so the
  frontier tracks a logical file across libdb's close/reopen; the WB
  entry now stores the on-disk name for the truncation.
- __os_io write fast path consults __db_sim_io_write_fault_hook: returns
  ENOSPC (whole write fails, nothing persists) or a torn-prefix length
  (persist a strict prefix, report full success) -- the previously
  unconsumed ENOSPC and torn-write knobs are now load-bearing.
- __os_io read fast path is offset-aware (__db_sim_io_read_hook now takes
  fhp+offset) and consults a ported stale-read ring plus the corrupt
  bit-flip; a per-I/O __db_sim_io_latency_hook consumes the latency knob
  (a tiny capped sleep when armed).

All behind HAVE_DST; the OFF library still exports zero __db_sim_* symbols
and compiles the hooks to the stock path.
The FoundationDB/TigerBeetle 'DST finds real bugs' proof.  Each planted
bug lives at a real library site, gated by -DDB_DST_INJECT_BUG=<n> and
compiled out of every normal build:

- 1 NODURABLE: __log_flush_int skips the log fsync but still acks the
  commit.  The write-back durable frontier never advances, so the crash
  drops the un-synced log and every 'committed' txn is lost.
- 2 NOCKSUM: __db_check_chksum ignores a checksum mismatch and accepts
  the page, so a corrupt page flows silently into the tree.
- 3 LOSTUPDATE: __memp_pgwrite skips a dirty-page write but reports
  success, so a checkpoint believes a page is durable when it never
  reached disk.

Each is caught by a specific scenario's invariant within K=1 seeds.
Nested #if defined(HAVE_DST)/#if DB_DST_BUG(n) so the OFF build (where
DB_DST_BUG is undefined) still preprocesses and links cleanly.
…rios

Each runs a seeded workload, injects its fault, recovers, and asserts the
safety invariant (committed survives / uncommitted doesn't / checksum
catches corruption / no silent bad data).  All build+run+pass; each
crash/fault scenario passes across a multi-seed sweep and replays
bit-identically per seed.

New (11): test_sim_hash_crash / _recno_crash / _queue_crash (per-access-
method op+crash+recover); test_sim_ckp_crash (page-flush durability with
DB_PRIVATE, catches LOSTUPDATE); test_sim_torn_log (recovery safe past a
torn log tail); test_sim_enospc (graceful ENOSPC, no corruption);
test_sim_abort_atomic (committed present, aborted leave no trace);
test_sim_recover_idempotent (recover twice -> identical state hash);
test_sim_dup_crash (DB_DUPSORT dups, exact multiplicity);
test_sim_overflow_torn (overflow-record corrupt read caught);
test_sim_split_crash (btree split/merge churn survives).

crash_recover reworked to use the real write-back drop so the NODURABLE
planted bug is a hard catch.  sim_scenario.h factors out the
fork+crash+recover boilerplate.  dist/Makefile.in gains the 11 targets
and dst_tests builds all 14.
…ADME

- dst-sweep.sh: run one scenario over a seed range, report pass count and
  the failing seeds (each a reproducer).
- dst-bug-inject.sh: the bug-detection-latency yardstick -- build a
  dedicated library per planted bug (-DDB_DST_INJECT_BUG=n), build the
  guarding scenario, and assert each bug is caught within K seeds
  (all three caught at K=1).
- .gitignore: TESTDIR_sim_* scratch env dirs.
- DESIGN.md / README.md: reflect the landed write-back drop, the three
  real-site planted bugs, the 14-scenario catalog, and the scripts;
  honest notes on what is deferred (stale-read scenario, guard planting,
  os_aio/physwrite hooks, meson wiring).
@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.30 (libabigail — authoritative)

Functions changes summary: 0 Removed, 0 Changed, 1 Added function
Variables changes summary: 0 Removed, 0 Changed, 0 Added variable

1 Added function:

  [A] 'function atomic_value_t __os_atomic_read_relaxed(const db_atomic_t*)'    {__os_atomic_read_relaxed}

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 27, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by a rebased branch (dst/foundation-rebased) to cleanly pick up the OOM/SIGFPE fixes merged from #52/#53 and avoid reverting them; reopening as a new PR.

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