Skip to content

Commit 08226eb

Browse files
h4x0rclaude
andcommitted
docs: install three-tier validation framing; bring codes/corpora current
Make every validation claim tier-accurate across the four reader-facing docs. validation.md — replace "Input provenance" + "Epistemic stance" with a precise THREE-TIER model whose axis is whether the correctness check is trustworthy (Tier 1 real third-party/real-device ground truth; Tier 2 real-engine bytes confirmed by a derivable answer key or sqlite3/calamine, scenario chosen by us; Tier 3 only-we-vouch, essentially just the freeblock-clobbered spilled-cell path). State the one-line rule and correct the in-cell image thumbnail to Tier 2 (calamine reads the embedded media back). Tier-label the Layer-2 oracle table and add rows for journal-anomaly arm split (NIST PERSIST = Tier 1; minted hot-journal arms = Tier 2), table_instance_risk Detector A+B, the survey FP replication, and the iOS no-panic robustness sweep. State limitations plainly (same-schema drop+recreate undecidable; DELETE/TRUNCATE leave no in-band residue; encrypted out of scope; FP benchmark is a replication; Boyer-Moore inapplicable by design). corpus-catalog.md — correct the stale §K PERSIST claim (now 100/100 via carve_rollback_journal, not "pending 0/100"); add §N drop_recreate, §O paper_fp FP scenarios, §P Josh Hickman iOS-17 corpus; extend the §H md5 manifest and the classification header for the REAL-ext/REAL-device entries. README.md — add WAL-UNCHECKPOINTED acquisition guidance; document the table_instance_risk hint column and its honest framing; add the out-of-scope limitations to Trust-but-verify. index.md — anomaly-code SCHEMA-CHANGE keyed on the schema cookie; three-tier pointer + iOS robustness in Validation; rollback-journal/hint entry points. mkdocs build --strict passes. No Rust source touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b0a7166 commit 08226eb

4 files changed

Lines changed: 234 additions & 60 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ Need a queryable database too? Add `--db` to also write `<name>.carved.db` (same
8888
- **`recovered_unattributed` — UNKNOWN.** Dropped-table residue, or a shape matching no surviving table — recovered in full, attributed to nothing.
8989
- **`recovered_fragments`** — the **separate** Tier-2 partial-salvage table (a distinctive cell survived but the row's identity did not), kept distinct so a fragment is never mistaken for a full row. `--no-fragments` drops it.
9090

91+
Each `recovered_<table>` row also carries a `table_instance_risk` column — a non-overclaiming **hint** (not an attribution tier), empty unless the residue is consistent with predating the current table instance: `rowid_exceeds_autoinc_highwater(r=…,seq=…)` (Detector A — a carved rowid above the `AUTOINCREMENT` high-water mark) or `sidecar_schema_changed(table)` (Detector B — a `-wal`/`-journal` sidecar whose prior schema differs). The hint is framed honestly: *consistent with* prior-incarnation residue, but also explainable by an `UPDATE`, a manual `sqlite_sequence` edit, or a current-instance deletion — it never asserts a predecessor or a drop+recreate, and a same-schema drop+recreate (indistinguishable from a benign `VACUUM` page move) is deliberately never flagged.
92+
9193
Want a queryable database, the files elsewhere, or a stream instead? Pick the option:
9294

9395
```console
@@ -230,7 +232,7 @@ This is one workspace (`sqlite-forensic`): two library crates following the flee
230232
|---|:-:|---|
231233
| `SQLITE-DELETED-RECORD-RECOVERED` | Medium | A record-shaped cell recovered from unallocated space — consistent with a deleted row not yet overwritten. Carries page / offset / rowid provenance. |
232234
| `SQLITE-FREELIST-NONEMPTY` | Low | The database holds free pages — consistent with prior deletions (`DELETE` without `VACUUM`); those pages may retain recoverable rows. |
233-
| `SQLITE-WAL-UNCHECKPOINTED` | Medium | A `-wal` sidecar carries committed page versions the main file does not reflect — the main file alone under-reports the true state. |
235+
| `SQLITE-WAL-UNCHECKPOINTED` | Medium | A `-wal` sidecar carries committed page versions the main file does not reflect — the main file alone under-reports the true state. **Acquire the live `-wal` before the application terminates**: a checkpoint (e.g. on its next clean close) folds the WAL into the main file and discards the uncheckpointed deleted/superseded residue, which the post-checkpoint main file no longer contains. |
234236
| `SQLITE-PAGECOUNT-MISMATCH` | High | The in-header page count disagrees with the count implied by file length — consistent with truncation, carving, or out-of-band modification. |
235237
| `SQLITE-RESERVED-SPACE-NONZERO` | Low | The header reserves bytes per page — non-standard; consistent with a page-level extension such as encryption (SQLCipher/SEE) or a checksum VFS. |
236238
| `SQLITE-JOURNAL-HOT` | High | A `-journal` with a valid header sits beside the database — consistent with an interrupted or in-progress write transaction (the main db may require rollback). |
@@ -255,7 +257,8 @@ A carver that *over*-reports is worse than useless on an evidence database — i
255257
- **High precision, structurally — never a live-row re-read.** Our carver carves only the *complement* of the live cell extents on a page, then drops any carved record whose rowid is currently live. Across the Nemetz recall corpus it produces **0 live-re-reads** (verified against the answer key's live rows), with only a small, low-confidence **phantom** class (all-empty/NULL records the inferred carver matches on a run of zero bytes). The two over-reporting failure modes the reference oracles exhibit on no-deletion databases — re-reading live cells, and re-surfacing a stale byte-copy of a live row — our carver does not. On the 2025 SQLite false-positive survey's B-tree-rebalancing scenario, measured on **identical bytes**, this discipline yields **0 false positives** where `bring2lite` re-surfaces 13 live rows as deleted ([`docs/competitive-landscape.md`](docs/competitive-landscape.md)).
256258
- **Strong in-page recall via freeblock reconstruction — reported honestly.** On the cleanest category (`0C`: records deleted in place, `secure_delete=0`, no overwrite, so **every** deleted row's bytes survive) the carver recovers **70 of the 84** cross-tool-scored rows (recall **0.833**), ahead of `fqlite`'s 0.798. SQLite overwrites a freed cell's first four bytes (payload-length + rowid varints, `header_len`, leading serial) with the freeblock pointer; `reconstruct_freeblock_records` rebuilds each record from its surviving serial-type tail plus a schema template derived from a live cell on the same page, with the destroyed rowid surfaced as unknown. It does so at higher precision than `fqlite` and **0 live-re-reads**.
257259
- **Overflow-page chains: partial recovery, honestly bounded.** A deleted row whose payload spilled onto a freed overflow chain is reassembled to a full row **only when every chain page survives as a freelist leaf**; a chain page reallocated as the freelist trunk destroys the record, which is then refused from the full tier and surfaces only as a Tier-2 fragment. On the Nemetz `0E` category this reassembles the one byte-perfectly-recoverable spilled chain (verified `assert_eq!` against the answer key, substrate recall **1.000**) for an **end-to-end `0E` recall of 0.333** — a deliberately bounded capability, graded below the in-page tier, never claimed as full overflow recovery.
258-
- **Secondary checks stay labelled as such.** The undark/fqlite differential ([`docs/validation.md`](docs/validation.md)) is **inter-tool concordance** (the oracles disagree with each other — agreement, not correctness), and the DC3 `sqlite_dissect` corpus is a **no-false-positive regression set** (its `expected_rows` are live content, not a deleted set), never a recall oracle.
260+
- **Secondary checks stay labelled as such.** The undark/fqlite differential ([`docs/validation.md`](docs/validation.md)) is **inter-tool concordance** (the oracles disagree with each other — agreement, not correctness), and the DC3 `sqlite_dissect` corpus is a **no-false-positive regression set** (its `expected_rows` are live content, not a deleted set), never a recall oracle. The 2025 survey false-positive benchmark is a **replication** of the paper's scenario construction (the official corpus is not public yet), and the FQLite scenario-10 number is **cited from the paper**, not measured here (its WAL recovery is GUI-coupled).
261+
- **Out of scope, stated plainly.** A **same-schema drop+recreate** is undecidable from a single snapshot and from a sidecar (indistinguishable from a benign `VACUUM` page move), so `table_instance_risk` flags only `AUTOINCREMENT` rowid-overflow and unambiguous sidecar schema changes — never the same-schema case. **DELETE-mode** (the `-journal` is unlinked) and **TRUNCATE-mode** (it is zeroed) rollback journals leave no in-band residue (a disk-carving-layer concern). **Encrypted databases** (SQLCipher / SEE) are out of scope. The carver is **structural** (b-tree / freelist / journal layout), so a Boyer-Moore signature scan is inapplicable by design.
259262

260263
Carved records remain **confidence-graded observations** ("consistent with a deleted row"), never a verdict. The honest summary: a strict precision discipline confirmed against independent ground truth, and a documented in-page recall gap — not a claim of perfect recall or proof of correctness.
261264

docs/corpus-catalog.md

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ All fixtures were built with the system `sqlite3` CLI / Python `sqlite3` module:
2020
2121
## Classification
2222

23-
`SYNTHETIC` — all built locally with the real `sqlite3` engine (REAL engine,
24-
synthetic data). Confidence `` (confirmed: each generator below was run and the
25-
resulting file inspected, not just named).
23+
The corpus spans three provenance classes, labelled per section below:
24+
`REAL-engine`/`SYNTHETIC` (built locally with the real `sqlite3` engine — real
25+
engine, our data: §A–§E, §J, §L, §M, §N, §O); `REAL-ext` (externally-authored real
26+
artifacts — the Nemetz corpus §I, NIST CFReDS §K, SharifCTF §K, the DC3 corpus §G);
27+
and `REAL-device` (genuine device data — the Josh Hickman iOS-17 images §P).
28+
Confidence `` throughout (each generator was run and the file inspected, or the
29+
external artifact downloaded and its schema/ground-truth parse confirmed — not just
30+
named).
2631

2732
## §A `tests/data/places.db` (pre-existing, WS-C spike)
2833

@@ -453,13 +458,15 @@ manifest: `tests/data/cfreds/README.md` (the single detailed index for this set)
453458
`UPDATE … SET Quantity=200` modifications per variation.
454459
- *WAL* variation (uncheckpointed): main-only view = 2240 rows, WAL-applied =
455460
2140; our WAL handling surfaces both. Validated now.
456-
- *PERSIST* variation (rollback journal): the 100 deletes survive in the
457-
`-journal` page images (header zeroed post-commit, bodies intact, all 100
458-
confirmed present). **Recovering them is a pending capability** — the carver
459-
reads only the main db today (0/100). Rollback-journal carving is designed in
460-
[`design/journal-recovery.md`](design/journal-recovery.md). This is the
461-
Doer-Checker payoff: real NIST ground truth surfaced a real recovery-substrate
462-
gap our synthetic fixtures never exercised.
461+
- *PERSIST* variation (rollback journal): the 100 deletes and 100 modifications
462+
survive in the `-journal` page images (header zeroed post-commit, bodies
463+
intact). `carve_rollback_journal` diffs the journal's pre-transaction snapshot
464+
against the live db and recovers **100/100 deletes + 100/100 modified prior
465+
values** (`forensic/tests/cfreds_journal_recovery.rs`); `audit_journal` raises
466+
the RECOVERABLE observation on it (`cfreds_journal_anomaly.rs`). Rollback-journal
467+
carving is designed in [`design/journal-recovery.md`](design/journal-recovery.md).
468+
This was the Doer-Checker payoff: real NIST ground truth surfaced a real
469+
recovery-substrate gap our synthetic fixtures never exercised, now closed.
463470
- **SFT-05** (BLOB): **not committed** — each db is ~206 MB (gitignored/env-gated
464471
class). Re-download from the SFT-05 dataset link in `tests/data/cfreds/README.md`.
465472

@@ -488,6 +495,90 @@ third-party content); full provenance + recipes + md5s in
488495
(`ALTER TABLE … ADD COLUMN`). Live schema cookie (2) advanced past the journal's
489496
prior page-1 image cookie (1), so SCHEMA-CHANGE fires with both values shown.
490497

498+
## §N `tests/data/drop_recreate/` (real-engine artifact, **committed**)
499+
500+
Five small real-engine databases (plus two `-journal` sidecars) that exercise the
501+
`table_instance_risk` diagnostic **HINT** — Detector A (AUTOINCREMENT high-water
502+
reconciliation) and Detector B (sidecar `-wal`/`-journal` schema change). The flag
503+
is a hint that names its evidence; it is **not** an assertion that a predecessor
504+
table existed. Construction reference: `docs/design/drop-recreate-attribution.md`;
505+
full fixture table + ground truth in `tests/data/drop_recreate/README.md`.
506+
507+
- Classification: `REAL-engine` (minted with the public-domain SQLite engine via
508+
the committed `gen.py`; no third-party content), confidence `` (generated and
509+
the ground truth confirmed with the `sqlite3` CLI). **Committed** (CC0).
510+
- Detector A — `rowid > sqlite_sequence` on an AUTOINCREMENT table: `b_autoinc.db`
511+
fires on residue rowids 6..10; `upd_autoinc.db` fires on rowid 1000 — a row a
512+
*current-instance* `UPDATE` moved past the high-water mark (proving A is a hint,
513+
not proof); `b_plainpk.db` (no AUTOINCREMENT) **never** fires — the honest limit
514+
that a same-schema, plain-PK drop+recreate is undecidable.
515+
- Detector B — sidecar prior schema differs: `b_journal_altered.db` + `-journal`
516+
fires for `students` (the prior CREATE SQL lacks the later `ALTER`'s column);
517+
`b_journal_dml.db` + `-journal` (DML-only last txn) **never** fires. Detector B is
518+
table-level and deliberately does NOT fire on a same-schema drop+recreate or a
519+
`VACUUM` page move.
520+
- Consumed by `forensic/tests/drop_recreate_risk.rs` (Detector A),
521+
`forensic/tests/detector_b.rs` (Detector B), the CLI provenance-column test, and
522+
the `core` prior-schema unit tests.
523+
- md5 (the `.db` files are byte-reproducible; the `-journal` sidecars embed a random
524+
checksum nonce so their md5 varies per run — the tests read content, not hash):
525+
526+
| file | md5 | bytes |
527+
|---|---|---|
528+
| `b_autoinc.db` | `b5f380a6376a8701e73514eb09a4ef27` ||
529+
| `b_plainpk.db` | `042ab37d307951db79df011a9eb0deec` ||
530+
| `upd_autoinc.db` | `6225cdb9cd88973bcad4a4325830c0a1` ||
531+
| `b_journal_altered.db` | `3a77f03ea3ac1ef40f8e9b284af98a59` ||
532+
| `b_journal_dml.db` | `2c1a405f4cc27856b367059554b319bf` ||
533+
534+
## §O `tests/data/paper_fp/` false-positive scenarios (real-engine **replication**, **committed**)
535+
536+
Real-engine **replications** of the three false-positive scenarios from the 2025
537+
survey (Lee, Park, Lee & Choi, *FSI:DI* **55**, art. 302031,
538+
[DOI](https://doi.org/10.1016/j.fsidi.2025.302031)). These reproduce the survey's
539+
Table-5 *construction* with the real SQLite engine — they are **not** the authors'
540+
byte-identical corpus (the official corpus is released "upon request" / not public
541+
yet). Generator + full ground truth: `tests/data/paper_fp/README.md`.
542+
543+
- Classification: `REAL-engine` (minted by the committed `gen.py` via Python stdlib
544+
`sqlite3`; no third-party data embedded), confidence ``. **Committed** (CC0).
545+
- `f.db`**0F**, B-tree rebalancing (Type \*\*): live ids 51..80, deleted 1..50.
546+
Our carver excludes live rowids structurally → **0 live-row false positives**
547+
where `bring2lite` re-surfaces 13.
548+
- `b.db`**0B**, table reinsertion with the SAME schema (Type \*): live ids 1..5
549+
(`NEW-NAME`), dropped residue = 10 `OLD-NAME` rows. The genuinely-undecidable
550+
same-schema case.
551+
- `wcase.db` + `wcase.db-wal`**10**, WAL + `secure_delete=ON`: the residue lives
552+
**only** in the `-wal`; the main image holds zero message bodies. **FQLite's
553+
scenario-10 number is cited from the paper, not measured here** — its WAL recovery
554+
is GUI-coupled (see §F.2).
555+
- Consumed by `forensic/tests/paper_fp_scenarios.rs` and the oracle comparison in
556+
[`competitive-landscape.md`](competitive-landscape.md).
557+
- md5 (`.db` files byte-reproducible; `wcase.db-wal` is content-stable but
558+
salt-variant per run):
559+
560+
| file | md5 | bytes |
561+
|---|---|---|
562+
| `f.db` | `a61a446a1cf0e5304956384b69644071` | 45056 |
563+
| `b.db` | `042ab37d307951db79df011a9eb0deec` | 8192 |
564+
| `wcase.db` | `22ebdd36e102f2af2f5766b7297dcad3` | 4096 |
565+
| `wcase.db-wal` | `baaf207913b60136c1762dbe435bb03e` | 16512 (content-stable, salt-variant) |
566+
567+
## §P Josh Hickman iOS-17 image corpus (REAL-device, env-gated, **not committed**)
568+
569+
Genuine iOS-17 application SQLite databases from Josh Hickman's public reference
570+
image — real-device data used as a **robustness sweep** (no-panic), NOT a
571+
known-answer recall oracle. The full open → audit → carve pipeline must survive
572+
every real db without panicking.
573+
574+
- Classification: `REAL-device` (third-party real-device artifacts), confidence ``
575+
(the sweep runs the pipeline over every db). **Not committed** — large, owned by
576+
the `issen` corpus; downloaded manually and read in place, env-gated like §G/§M.
577+
- Test gate: `SQLITE_FORENSIC_IOS_CORPUS` (absolute path to the extracted corpus
578+
root). `forensic/tests/ios_realdata_robustness.rs` opens every `.db`/`.sqlite`/
579+
`.sqlite3` under it and asserts the pipeline never panics; it **skips cleanly**
580+
when the var is unset, so a plain `cargo test` stays green.
581+
491582
## §M `tests/data/paper_fp/large_messages.db` (throughput benchmark, generated, **not committed**)
492583

493584
A ~100 MB messages-like database for the throughput benchmark that sits alongside
@@ -533,15 +624,28 @@ Committed fixtures (under `tests/data/`, `tests/data/`):
533624
| `tests/data/journal/hot.db-journal` | `d428e2fcf8e6f3d9c71a58b18c6f4dcc` | 22016 |
534625
| `tests/data/journal/ddl_persist.db` | `0271673fb35215d80f313e5f549dbbaf` | 16384 |
535626
| `tests/data/journal/ddl_persist.db-journal` | `fe785dd18b5eb58b6dd4176ae5864130` | 8720 |
536-
537-
The 141 committed Nemetz databases under `tests/data/nemetz/` (CC0, §I) have their
538-
own md5 manifest in `tests/data/nemetz/README.md` to avoid duplicating it here.
627+
| `tests/data/drop_recreate/b_autoinc.db` | `b5f380a6376a8701e73514eb09a4ef27` ||
628+
| `tests/data/drop_recreate/b_plainpk.db` | `042ab37d307951db79df011a9eb0deec` ||
629+
| `tests/data/drop_recreate/upd_autoinc.db` | `6225cdb9cd88973bcad4a4325830c0a1` ||
630+
| `tests/data/drop_recreate/b_journal_altered.db` | `3a77f03ea3ac1ef40f8e9b284af98a59` ||
631+
| `tests/data/drop_recreate/b_journal_dml.db` | `2c1a405f4cc27856b367059554b319bf` ||
632+
| `tests/data/paper_fp/f.db` | `a61a446a1cf0e5304956384b69644071` | 45056 |
633+
| `tests/data/paper_fp/b.db` | `042ab37d307951db79df011a9eb0deec` | 8192 |
634+
| `tests/data/paper_fp/wcase.db` | `22ebdd36e102f2af2f5766b7297dcad3` | 4096 |
635+
| `tests/data/paper_fp/wcase.db-wal` | `baaf207913b60136c1762dbe435bb03e` | 16512 |
636+
637+
The `drop_recreate` and `paper_fp` `-journal`/`-wal` sidecars embed a per-run nonce,
638+
so their md5 varies; the consuming tests read content, not hash. The 141 committed
639+
Nemetz databases under `tests/data/nemetz/` (CC0, §I) have their own md5 manifest in
640+
`tests/data/nemetz/README.md` to avoid duplicating it here.
539641

540642
Not committed (provenance only — see §F, §G and the per-directory READMEs):
541643
`tools/undark`, the fqlite tap under `tools/fqlite/` (source, jars, built classes
542644
— recipe in `tools/fqlite/README.md`), the `bring2lite` checkout + PyQt5 shim
543645
under `tools/bring2lite/` (§F.3), the Py3-ported `sqlparse` under `tools/sqldrp/`
544-
(§F.4), and the DC3 corpus under `tests-oracle-corpus/dc3-sqlite-dissect/` (full
545-
sha256/md5 list in `tests-oracle-corpus/README.md`). The committed
546-
`scripts/run-bring2lite.sh` / `scripts/run-sqldrp.sh` wrappers are the stable
547-
harness interface to the gitignored tool sources.
646+
(§F.4), the DC3 corpus under `tests-oracle-corpus/dc3-sqlite-dissect/` (full
647+
sha256/md5 list in `tests-oracle-corpus/README.md`), the env-gated Josh Hickman
648+
iOS-17 image corpus (`SQLITE_FORENSIC_IOS_CORPUS`, §P), and the ~100 MB throughput
649+
db (`SQLITE_FORENSIC_PERF_DB`, §M). The committed `scripts/run-bring2lite.sh` /
650+
`scripts/run-sqldrp.sh` wrappers are the stable harness interface to the gitignored
651+
tool sources.

0 commit comments

Comments
 (0)