speckl/docs/db-sqlite-reference.md
Marcus (agent-cos) 45d82e2ea0
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
docs: SQLite research + blog post for Prove SQLite project
- docs/db-sqlite-reference.md (32KB): pager FSM (7 states, 15 transitions),
  WAL concurrency model, ~50 VDBE opcodes, B-Tree cell layout, 37 formal invariants,
  notable CVEs, formal verification precedents
- docs/blog-07-prove-sqlite.md (1,164 words): framing blog post, all 6 sections
2026-05-10 00:49:15 -07:00

32 KiB

SQLite Pager, WAL, VDBE, and B-Tree Reference

Author: Marcus (agent-cos) cos@marcus.local Date: 2026-05-09 Purpose: Engineering source material for SpeckDL formal verification Target: SQLite version 3.53.1 (check-in c88b22011a54b, 2026-05-05)


1. Pager State Machine

The pager is SQLite's storage engine — manages page cache, transactions, and crash recovery. Located in src/pager.c (~12K lines). The pager owns the lock state, journal state, and page cache.

1.1 Pager State Enum (from pager.c header comment)

PAGER_UNLOCK       (0) — No lock held. Initial state.
PAGER_SHARED       (1) — Read lock (SQLITE_LOCK_SHARED). Read-only access.
PAGER_RESERVED     (2) — Reserved lock (SQLITE_LOCK_RESERVED). Preparing to write.
PAGER_EXCLUSIVE    (3) — Exclusive lock (SQLITE_LOCK_EXCLUSIVE). Actively writing.
PAGER_SYNCED       (4) — All dirty pages written + synced. Journal can be deleted.
PAGER_ERROR        (5) — Error occurred. No further operations allowed.
PAGER_OPEN         (6) — Special state: connection open but no lock. Transitional.

1.2 State Transition Table

From To Trigger / Guard Side Effects
PAGER_OPEN PAGER_SHARED First pager_acquire() / pager_begin() Obtain SQLITE_LOCK_SHARED
PAGER_UNLOCK PAGER_SHARED sqlite3PagerSharedLock() Obtain shared lock, recover hot journal if present
PAGER_SHARED PAGER_UNLOCK pager_unlock() / end of read tx Release shared lock
PAGER_SHARED PAGER_RESERVED pager_write() or pager_begin() Obtain SQLITE_LOCK_RESERVED. No other writer can proceed.
PAGER_RESERVED PAGER_EXCLUSIVE First page modification (in PAGER_RESERVED) -
PAGER_RESERVED PAGER_EXCLUSIVE Commit begins Obtain PENDING then EXCLUSIVE lock
PAGER_RESERVED PAGER_SHARED ROLLBACK Discard changes, release reserved lock
PAGER_EXCLUSIVE PAGER_SYNCED All dirty pages written + fsynced Journal header updated to sync state
PAGER_SYNCED PAGER_SHARED (journal-mode) or PAGER_UNLOCK (WAL) Delete journal file (COMMIT point) Transaction atomically committed
PAGER_EXCLUSIVE PAGER_ERROR I/O error, disk full, etc. errCode set, hot journal preserved
PAGER_SYNCED PAGER_ERROR I/O error during sync errCode set, journal preserved for recovery
any → PAGER_ERROR Any I/O error during pager operation Further ops blocked
PAGER_ERROR PAGER_OPEN pager_unlock() forced close Resets connection but keeps db open

1.3 Journal Mode Variants

Three rollback journal modes (non-WAL):

  • DELETE (default): Journal file deleted on commit. If crash before delete → hot journal → recovery rolls back.
  • TRUNCATE: Journal file truncated to zero length on commit. Faster on some filesystems than delete.
  • PERSIST: Journal header zeroed on commit, file left on disk. Slightly faster, but leaves journal files around.

The pager tracks journalMode and adjusts the commit-delete step accordingly.

1.4 Pager Invariants (to formally specify)

  • I1 (Lock State Consistency): eState must be consistent with actual file lock held. If eState==PAGER_SHARED, must hold SQLITE_LOCK_SHARED. If eState==PAGER_EXCLUSIVE, must hold SQLITE_LOCK_EXCLUSIVE.
  • I2 (Dirty Page Tracking): For any page in the cache, isDirty iff the page has been modified since last checkpoint/journal write.
  • I3 (Journal Existence): The journal file exists iff eState >= PAGER_RESERVED and eState != PAGER_ERROR (except during recovery).
  • I4 (Hot Journal Detection): A journal is "hot" iff it exists but the database was not cleanly closed. On first shared lock, hot journals must be rolled back.
  • I5 (Error Propagation): Once eState == PAGER_ERROR, all subsequent pager operations must return the stored errCode.
  • I6 (Read-Only Pager): If readOnly flag is set, eState must never exceed PAGER_SHARED.
  • I7 (Sync Before Commit): No dirty page is persisted to the database file until after the journal is synced to disk AND the exclusive lock is held.
  • I8 (Commit Atomicity): The commit point is the deletion of the journal file. Before this: crash → rollback. After this: crash → committed state visible.

1.5 Lock Escalation Sequence (from atomiccommit.html)

NO_LOCK → SHARED (read) → RESERVED (intent to write) → PENDING (block new readers)
    → EXCLUSIVE (writing) → no lock (after commit journal deletion)

Key subtlety: PENDING lock allows existing readers to finish but blocks new readers. This prevents writer starvation in the face of constant new read connections.


2. WAL Mode State Machine

WAL mode inverts the rollback journal: original data stays in the database file, changes are appended to the WAL file. Commit = appending a commit record to WAL.

2.1 WAL Files

File Purpose Lifecycle
X-wal Write-ahead log. Sequential append of frames. Exists while connections are open. Deleted on clean close.
X-shm Shared memory (wal-index). Hash table for frame location. mmap'd. Not fsynced. Omitted under EXCLUSIVE locking mode.
X Main database file. Contains original + checkpointed pages. Persistent.

2.2 WAL-Index Header Fields (from walformat.html)

Field Bytes Description
iVersion 0-3 WAL-index format version (3007000)
iChange 8-11 Transaction counter, incremented per tx
isInit 12 1 when shm file initialized
bigEndCksum 13 Checksum endianness flag
szPage 14-15 Page size (or 1 for 65536)
mxFrame 16-19 Number of valid commit frames in WAL
nPage 20-23 Database size in pages
aFrameCksum 24-31 Checksum of last frame
aSalt 32-39 Two salt values from WAL header
nBackfill 96-99 Frames already backfilled by checkpoints
read-mark[0..4] 100-119 Five reader end marks
nBackfillAttempted 128-132 Frames attempted but not confirmed backfilled

2.3 WAL Locks

Lock Byte Purpose
WAL_WRITE_LOCK 120 Exclusive: appending frames to WAL
WAL_CKPT_LOCK 121 Exclusive: running checkpoint
WAL_RECOVER_LOCK 122 Exclusive: running recovery
WAL_READ_LOCK(0) 123 Reader end-marks (0-4)
WAL_READ_LOCK(1) 124
WAL_READ_LOCK(2) 125
WAL_READ_LOCK(3) 126
WAL_READ_LOCK(4) 127

2.4 WAL Frame Lifecycle

Frame format (in WAL file): [page-number] [db-size-after-commit] [salt-1] [salt-2] [checksum-1] [checksum-2]
  followed by page data.

Frame lifecycle states:
  1. WRITER appends frames to end of WAL file (holding WAL_WRITE_LOCK exclusive)
  2. Last frame of a transaction marks "commit" via non-zero db-size field
  3. READERS snapshot mxFrame at transaction start → their "end mark"
  4. CHECKPOINTER copies frames back to database (holding WAL_CKPT_LOCK), increments nBackfill
  5. RESET: When nBackfill == mxFrame and no reader holds WAL_READ_LOCK(N) for N>0,
     writer rewinds WAL to beginning (resets mxFrame, nBackfill to 0)

2.5 Reader Snapshot (End Mark) Protocol

  • When a read transaction begins, the reader notes current mxFrame value → this is its "end mark"
  • The reader acquires one of WAL_READ_LOCK(0..4) to declare its end mark
  • To read page P, the reader calls FindFrame(P, end_mark): find the latest frame for page P in WAL with frame number ≤ end_mark
  • If found: read from WAL frame. If not found: read from database file.
  • Readers hold WAL_READ_LOCK(N) for the duration of their read transaction
  • This prevents checkpointer from overwriting pages the reader still needs

2.6 Checkpoint States

Checkpoint Mode Behavior
PASSIVE Non-blocking. Works around readers. May not complete. Default.
FULL Blocks until checkpoint completes or a writer appears. More aggressive.
RESTART Full checkpoint then blocks until no readers. Ensures WAL reset.
TRUNCATE Like RESTART, also truncates WAL file to zero bytes.

Checkpoint progress constraint: MUST STOP at the minimum end_mark across all active readers.

  • nBackfill ≤ min(read_mark[0..4])
  • nBackfillAttempted ≥ nBackfill (may be ahead if reader blocked a backfill)

2.7 WAL Reset Condition

The WAL is reset (wound back to start) when:

  1. nBackfill == mxFrame (all frames backfilled)
  2. No reader holds WAL_READ_LOCK(N) for any N > 0 (no readers using WAL)
  3. Writer holds WAL_WRITE_LOCK exclusively
  4. On reset: mxFrame = 0, nBackfill = 0, file position rewound

2.8 WAL Invariants (to formally specify)

  • W1 (mxFrame Monotonic): mxFrame is non-decreasing except during WAL reset.
  • W2 (nBackfill Bound): nBackfill ≤ mxFrame always.
  • W3 (Backfill ≤ Readers): nBackfillAttempted ≤ min(read_mark[0..4]) for any checkpoint that respects readers.
  • W4 (Frame Validity): All frames 1..mxFrame have valid checksums. Frames > mxFrame are undefined.
  • W5 (Commit Detection): Frame k is a commit frame iff its db-size-after-commit field ≠ 0.
  • W6 (Read Consistency): A reader with end-mark M sees exactly the database state as of the last commit frame ≤ M.
  • W7 (WAL Write Serialization): Only one connection may append to WAL at a time (WAL_WRITE_LOCK exclusive).
  • W8 (Checkpoint Isolation): Checkpoint must not overwrite pages needed by any active reader (nBackfill ≤ min(read_marks)).
  • W9 (Salt Match): WAL file header salt values must match aSalt in the WAL-index header. Mismatch → WAL is stale → must recover.
  • W10 (WAL Reset Safety): WAL may only be reset when nBackfill == mxFrame AND no reader locks exist.

3. VDBE Instruction Set (Key Opcodes)

The VDBE is SQLite's bytecode virtual machine. Each prepared statement is a bytecode program. Source: vdbe.c (~8K lines). 192 opcodes in version 3.53.1.

3.1 Instruction Format

Each instruction: [opcode] P1 P2 P3 P4 P5
  P1, P2, P3: 32-bit signed integers (registers, cursor numbers, jump targets)
  P4: various (int, string, function pointer, collation)
  P5: 16-bit unsigned flags

3.2 Transaction & Locking Opcodes

Opcode P1 P2 P4 Effect
Transaction iDb write_flag Begin a transaction on database P1. P2=1 for write, 0 for read. Opens pager transaction.
AutoCommit autocommit_flag rollback_flag Set auto-commit on/off. If P2 set, rollback first. Causes VM halt.
Checkpoint iDb mode Run checkpoint on database P1. P2 = PASSIVE(0)/FULL(1)/RESTART(2)/TRUNCATE(3).
Savepoint start_flag savepoint_name Begin/Release/Rollback a savepoint.
TableLock iDb root_page table_name Lock a table before modifying. write_flag in P3.
SchemaLock iDb schema_name Lock schema objects.

3.3 Cursor Opcodes

Opcode P1 P2 P3 P4 Effect
OpenRead cursor root_page iDb Open cursor P1 for read on root page P2 of db P3
OpenWrite cursor root_page iDb table_name Open cursor P1 for write on root page P2 of db P3
OpenAutoindex cursor key_cols nCol Open ephemeral auto-index cursor
OpenEphemeral cursor nCol Open temporary table, auto-clears on close
Close cursor Close cursor P1
SeekGT cursor jump_target key_reg Seek cursor P1 to first entry > key in register
SeekGE cursor jump_target key_reg Seek cursor P1 to first entry >= key in register
SeekLT cursor jump_target key_reg Seek cursor P1 to first entry < key in register
SeekLE cursor jump_target key_reg Seek cursor P1 to first entry <= key in register
SeekRowid cursor Seek cursor P1 to entry with given rowid
NotFound cursor jump_target Jump if key not found in index
Found cursor jump_target Jump if key found in index
Next cursor jump_target Advance cursor P1 to next entry. Jump to P2 if no more rows.
Prev cursor jump_target Move cursor P1 to previous entry. Jump to P2 if at beginning.
Last cursor Move cursor P1 to last entry.
Rewind cursor jump_target Move to first entry. Jump to P2 if empty.
Column cursor column_idx dest_reg Read column P2 from cursor P1 into register P3
Rowid cursor dest_reg Write rowid of cursor P1 into register P2
IdxInsert cursor key Insert entry into index cursor
IdxDelete cursor key Delete entry from index cursor
Insert cursor Insert current record into table cursor
Delete cursor Delete current record from table cursor
SorterSort cursor Sort sorter cursor P1
SorterData cursor dest_reg Read current sorter row

3.4 Value & Register Opcodes

Opcode P1 P2 P3 Effect
Integer value dest Store integer value in register P2 (P1 is the value)
Real value (P4) dest Store real value in register P2
String8 dest string Store UTF-8 string in register P2. Self-modifying → becomes String opcode.
Null start count Set P2 registers starting at P1 to NULL
Blob size dest data Store P1-byte blob in register P2
Copy src dest count Copy P3 registers from P1 to P2
Move src dest count Move P3 registers from P1 to P2 (clear source)
SCopy src dest Soft copy (shallow) register P1 to P2
ResultRow reg_start nReg Pause VM: emit result row from registers P1..P1+P2-1. sqlite3_step() returns SQLITE_ROW.
Yield pc_store Coroutine yield. Swap PC with value in register P1.
Halt p1 p2 p3 Halt execution. If P1==SQLITE_OK and P2==OE_Abort, auto-rollback.

3.5 Arithmetic & Comparison Opcodes

Opcode P1 P2 P3 P4 Effect
Add r1 r2 result r[P3] = r[P1] + r[P2]
Subtract r1 r2 result r[P3] = r[P1] - r[P2]
Multiply r1 r2 result r[P3] = r[P1] * r[P2]
Divide r1 r2 result r[P3] = r[P1] / r[P2]
Remainder r1 r2 result r[P3] = r[P1] % r[P2]
Eq r1 r2 jump coll Compare r[P1] and r[P2]. Jump to P3 if equal.
Ne r1 r2 jump coll Jump if not equal
Lt r1 r2 jump coll Jump if r[P1] < r[P2]
Le r1 r2 jump coll Jump if r[P1] ≤ r[P2]
Gt r1 r2 jump coll Jump if r[P1] > r[P2]
Ge r1 r2 jump coll Jump if r[P1] ≥ r[P2]
BitAnd r1 r2 result r[P3] = r[P1] & r[P2]
BitOr r1 r2 result r[P3] = r[P1] | r[P2]
BitNot src dest r[P2] = ~r[P1]
Concat r1 r2 result r[P3] = r[P1] || r[P2]

3.6 Control Flow Opcodes

Opcode P1 P2 Effect
Goto target Unconditional jump to P2
Gosub return_reg target Push PC to register P1, jump to P2
Return return_reg Jump to address stored in P1 + 1
If jump_target dest Conditional jump
IfNot jump_target dest Conditional jump (negated)
IfNull reg jump_target Jump if register contains NULL
NotNull reg jump_target Jump if register is not NULL
IfPos reg jump_target Jump if register > 0
IfNeg reg jump_target Jump if register < 0
Once flag NOP if flag was set. Execute body exactly once.
Init flags max_addr First opcode. Sets trace flags. Self-modifying: increments P1 each invocation.
Program sub_program_addr Invoke a subprogram (trigger).
Param param_idx dest_reg Read parameter from caller into destination register.

3.7 Aggregate Function Opcodes

Opcode P1 P2 P3 Effect
AggStep arg_reg accum Execute xStep for aggregate function
AggStep1 step/inverse arg_reg accum Execute xStep (P1=0) or xInverse (P1!=0)
AggInverse arg_reg accum Execute xInverse (window functions)
AggFinal accum Finalize aggregate, store result
AggValue result Store current aggregate value

3.8 VDBE Invariants (to formally specify)

  • V1 (PC Bounds): Program counter must remain within [0, nOp] during execution.
  • V2 (Register Type Safety): Each register holds exactly one type at a time (NULL, int, real, text, blob, RowSet, Frame, Undefined).
  • V3 (Cursor Validity): An opcode must not reference a cursor unless it was previously opened (OpenRead/OpenWrite/OpenEphemeral) and not yet closed.
  • V4 (Transaction Guard): Before any write opcode executes, a transaction must be open. Read opcodes may run outside a transaction but must obtain shared lock.
  • V5 (ResultRow Protocol): ResultRow must be preceded by opcodes that fill the output registers. sqlite3_step() returns SQLITE_ROW; subsequent call resumes after ResultRow.
  • V6 (Halt Termination): Execution terminates only via Halt opcode, PC exceeding last opcode address, or error.
  • V7 (Error Cleanup): On error or HALT with error, all open cursors are closed, memory freed, and active transactions rolled back.
  • V8 (Subprogram Isolation): Each Program invocation gets its own register set. Params are the only communication channel to the caller.
  • V9 (Coroutine Symmetry): Yield stores PC to register; subsequent Yield reads register to PC. Coroutine execution must return to same point.

4. B-Tree Invariants

SQLite uses B+tree for tables, B-tree for indices. Source: btree.c.

4.1 Page Types

Type Value Description
Interior index 0x02 Index b-tree interior page
Interior table 0x05 Table b-tree interior page
Leaf index 0x0a Index b-tree leaf page
Leaf table 0x0d Table b-tree leaf page

4.2 Page Layout

Page header:
  [page_type:1] [first_freeblock:2] [cell_count:2] [cell_content_start:2] [fragmented_free_bytes:1]
  
Followed by:
  [right_child_pointer:4] (interior pages only)
  [cell_pointer_array: cell_count * 2 bytes] — offsets to cells, in key order
  [unallocated space in middle]
  [cells: packed from end of page backwards]

4.3 B-Tree Cell Format

Table leaf cell:

[payload_size: varint] [rowid: varint] [payload: header + columns]

Table interior cell:

[left_child_page:4] [rowid: varint] [payload: partial row data]

Index leaf cell:

[payload_size: varint] [payload: header + index columns + rowid]

Index interior cell:

[left_child_page:4] [payload_size: varint] [payload: header + index columns + rowid]

4.4 Key Ordering Properties

  • Table B-Tree: Ordered by rowid (integer primary key). Interior cells store the rowid of the first entry in each child page.
  • Index B-Tree: Ordered by indexed column(s) + rowid. Interior cells store the key of the first entry in each child page.
  • All keys within a page are in ascending order.
  • For any interior page: all keys in child subtree i are ≤ the key at position i in the interior page, and ≤ all keys in child subtree i+1. (For indices, strict < for key columns, ≤ for rowid as tiebreaker.)

4.5 Page Split Condition

A page splits when inserting a cell would exceed the usable page space.

  1. A new page is allocated.
  2. Cells are distributed (roughly half each) between old and new page.
  3. Parent interior page gets a new cell pointing to the new page.

4.6 Page Merge Condition

When deleting, if a page's content falls below the minimum fill threshold (roughly 25-50% depending on implementation), the page may be merged with a sibling:

  1. Cells are moved to sibling page.
  2. Parent pointer to the empty page is removed.
  3. Empty page is added to freelist.

4.7 B-Tree Cursor Operations

Cursor state: { Btree*, root page, current page, cell index, cache of page stack }

Operations:
  sqlite3BtreeCursor(): Create cursor on B-tree identified by root page + flags
  sqlite3BtreeMoveto(): Position cursor to entry that matches key (or nearest)
  sqlite3BtreeNext(): Advance cursor. Page crossing handled automatically.
  sqlite3BtreePrevious(): Move cursor backwards. Page crossing handled.
  sqlite3BtreeInsert(): Insert cell into current position
  sqlite3BtreeDelete(): Delete entry at cursor position

4.8 B-Tree Invariants (to formally specify)

  • B1 (Page Type Consistency): A page's type field must match its role. Interior pages reference child pages; leaf pages do not.
  • B2 (Key Order Monotonic): For any interior page, keys in child page i are less than or equal to keys in child page i+1. Cells within a page are sorted ascending.
  • B3 (Pointer Integrity): Every child page reference must point to a valid allocated page of the correct type (or overflow page, or freelist page).
  • B4 (Cell Count Consistency): cell_count in the page header must equal the number of entries in the cell pointer array.
  • B5 (Free Space Accounting): fragmented_free_bytes + unallocated space between cell pointer array and cells must sum correctly to the page's available free space.
  • B6 (Page Size Boundary): Usable page size = page_size - reserved_bytes. All cells must fit within the usable page.
  • B7 (Root Page Integrity): The b-tree root page number must be a valid, allocated b-tree page. The schema table's root page mappings must be consistent.
  • B8 (Balance Preserved): After every insert/delete operation, the b-tree must satisfy all internal structural invariants (fill ratio, parent-child pointer correctness, key ordering).
  • B9 (Overflow Chain Integrity): If a cell's payload spills to overflow pages, the chain of overflow pages must: (a) form a proper linked list, (b) be bounded in length by the payload size, (c) contain no cycles.
  • B10 (Freelist Consistency): Freelist pages must not be reachable from any b-tree root. Freelist trunk pages must point to valid leaf pages or 0.

5. Known Bugs and Formal Verification Precedents

5.1 Notable SQLite CVEs

CVE Year Type Module Fixed In Severity
CVE-2022-35737 2022 Integer overflow → buffer overrun → RCE/DoS printf/str_vappendf 3.39.2 High (7.5)
CVE-2025-3277 2025 Write past end of malloc'd array via integer overflow concat_ws() function 3.49.1 Moderate
CVE-2025-6965 2025 Integer overflow → array bounds read SQL injection path 3.50.2 Moderate
CVE-2024-0232 2024 Use-after-free in JSON parser JSON 3.43.2 Moderate
CVE-2023-7104 2023 Corruption in session extension changeset processing Session (ext) 3.43.1 Low
CVE-2025-29088 2025 Crash via bad args to sqlite3_db_config Config API 3.49.1 Low

Key observation from SQLite's CVE page:

Almost all CVEs written against SQLite require the ability to inject and run arbitrary SQL. The core storage layer (pager, WAL, B-tree) has remarkably few logic bugs. Most CVEs are in SQL-level functions, extensions, or edge cases in the printf/logging subsystem.

5.2 22-Year-Old Integer Overflow (CVE-2022-35737)

Introduced October 2000 (version 1.0.12), discovered 2022 by Trail of Bits. Exploitable on 64-bit systems when large strings (>2GB) passed to printf functions with ! unicode flag. Arbitrary code execution possible without stack canaries. Not a pager/journal bug — but illustrates how old, untested edge cases survive.

5.3 Formal Verification Precedents

5.3.1 FoundationDB (Apple)

  • What: Distributed transactional key-value store, formally verified in Flow (their deterministic simulation language)
  • Method: Deterministic simulation testing — run all DB processes as fibers on a single thread with simulated network/disk, inject faults, check invariants. Run millions of random seeds nightly.
  • Result: Found ~70 bugs before release. Flow-based simulation verified correctness of their distributed transaction, replication, and recovery protocols.
  • Relevance to SQLite: The determinism approach parallels what could be done for SQLite's pager. SQLite is single-process by design, making deterministic replay easier.

5.3.2 TigerBeetle

  • What: Distributed financial accounting database. VOPR (Viewstamped Operation Replicator) deterministic simulator.
  • Method: Entire cluster runs on single thread. 1000x speed. Injects network, storage, and process faults. Swarm testing + fuzzer. Continuous verification.
  • Relevance to SQLite: The VOPR approach of running the real implementation under controlled simulation is directly applicable to testing SQLite's recovery paths in the pager.

5.3.3 Amazon (DynamoDB, S3)

  • What: TLA+ specifications for AWS distributed services
  • Method: Model checking (TLC) + manual proofs (TLAPS). Found bugs in replication protocols, quorum membership, leader election.
  • Paper: "Use of Formal Methods at Amazon Web Services" (Newcombe et al., 2014)
  • Relevance to SQLite: TLA+ is ideal for specifying state machines like the pager's locking protocol. The pager state machine is small enough for tractable TLC model checking.

5.3.4 MongoDB Transactions (VLDB 2025)

  • What: "Design and Modular Verification of Distributed Transactions in MongoDB"
  • Method: TLA+ specification + model checking + test-case generation.
  • Relevance to SQLite: Demonstrates that even complex transactional protocols can be verified with TLA+.

5.3.5 Formal Land's Rocq Translation of SQLite B-Tree (2025-2026)

  • What: AI-assisted translation of SQLite C code to Rocq (Coq variant). Translation only (not full proof of equivalence).
  • Result: "Still a big project to run with current technologies." Covered substantial part of B-tree but deemed too unstable for full verification at current AI capability levels.
  • Relevance: Direct precedent that SQLite formal verification is hard. Suggests model-level verification (state machine specs) is more tractable than code-level proofs.

5.3.6 Richard Hipp's Own Attempt (SQLite Lead)

  • Dr. Hipp reports spending "years" trying to write a formal design specification for SQLite but found it never added value and abandoned it. This is telling — both about the difficulty and about the value proposition.

5.4 SQLite's Existing Testing (Non-Formal)

Method Scale Notes
TH3 (Test Harness #3) Proprietary. Billions of test cases. 100% branch coverage. Mutation testing (inject bugs, verify they're caught).
OSS-Fuzz Continuous. Google's fuzzing infrastructure. Catches memory safety bugs.
Release testing ~100M tests per release Run on multiple platforms
Anomaly testing Random SQL/DB operations with crash injection Verify recovery

6. Target Invariants for SpeckDL Specification

Ranked by impact (what would be most impressive/valuable to prove):

Tier 1: Impact/Feasibility Sweet Spot

  1. Pager Commit Atomicity (I8)

    • Prove: For any sequence of pager operations and any crash point, the database file is always in either the pre-commit state (all changes rolled back) or the post-commit state (all changes applied). No partial-commit states.
    • Value: This is the fundamental SQLite guarantee. Proving it formally would be the headline achievement.
    • Feasibility: Medium. The pager state machine has only 5-6 states. The tricky part is modeling disk I/O reordering and crash-at-any-point semantics.
  2. WAL Read Isolation (W6)

    • Prove: A reader with end-mark M sees exactly the database state as of the last commit ≤ M. Writes after M are invisible.
    • Value: WAL mode's key property. With formal proof, SQLite's WAL concurrency model gets a rigorous foundation.
    • Feasibility: Medium. Need to model the WAL-index hash tables and the FindFrame algorithm.
  3. Journal Recovery Correctness

    • Prove: After a crash with a hot journal, recovery restores the database to its state before the incomplete transaction.
    • Value: Crash recovery is the hardest case to test exhaustively. Formal proof would eliminate a whole class of concerns.
    • Feasibility: High. Well-scoped problem with clear success criteria.

Tier 2: High Value, Higher Difficulty

  1. B-Tree Balance Preservation (B8)

    • Prove: After any sequence of insertions and deletions, the b-tree satisfies all structural invariants (page fill ratios, key ordering, pointer integrity, freelist consistency).
    • Value: B-tree corruption is one of the most feared failure modes. Formal proof is the gold standard.
    • Feasibility: Low-Medium. The B-tree is complex (~200KB of C code). A full proof is likely too big, but the core balancing algorithm could be specified and verified in isolation.
  2. Lock Protocol Deadlock Freedom

    • Prove: The lock escalation protocol (shared → reserved → pending → exclusive) is deadlock-free for any number of concurrent readers and writers.
    • Value: Prevents a potential operational nightmare.
    • Feasibility: High. Small state space. Ideal for TLC model checking.
  3. WAL Checkpoint Safety (W8)

    • Prove: Checkpoint never overwrites pages needed by active readers, even when readers and checkpointer operate concurrently.
    • Value: WAL's concurrent reader+writer+checkpointer model is nontrivial. Getting it right formally is impressive.
    • Feasibility: Medium. Requires modeling concurrent processes and the WAL-index header fields.

Tier 3: Ambitious / Future Work

  1. Full Pager FSM Correctness (I1-I8)

    • Prove all eight pager invariants from Section 1.4 simultaneously.
    • Feasibility: Low (large specification, many interacting components). Best done incrementally.
  2. VDBE Safety: No undefined register reads (V2)

    • Prove that the code generator never produces bytecode that reads an Undefined register.
    • Feasibility: Low. Requires modeling the SQL compiler's code generation, which is a much larger problem.

Recommendation: Start with Tier 1 (Items 1-3)

The pager commit atomicity + WAL read isolation + journal recovery form a coherent package:

  • All three involve the pager module
  • All three address crash-safety guarantees
  • Together, they prove that "SQLite transactions are atomic even under arbitrary crash timing" — the single claim that matters most
  • The state spaces are tractable for SpeckDL + model checking

7. Source References

Source Location
SQLite WAL documentation https://www.sqlite.org/wal.html
SQLite Atomic Commit https://www.sqlite.org/atomiccommit.html
SQLite Bytecode Engine https://www.sqlite.org/opcode.html (192 opcodes, v3.53.1)
Database File Format https://www.sqlite.org/fileformat2.html
WAL File Format https://www.sqlite.org/walformat.html
Pager source src/pager.c (~12K lines). States: PAGER_UNLOCK, SHARED, RESERVED, EXCLUSIVE, SYNCED, ERROR, OPEN
VDBE source src/vdbe.c (~8K lines) + helper files
B-Tree source src/btree.c
SQLite CVEs https://sqlite.org/cves.html
Formal Land B-Tree translation https://github.com/formal-land/rocq-of-db
FoundationDB verification "FoundationDB: A Distributed, Unbundled, Transactional Key Value Store" (SIGMOD 2021)
Amazon formal methods "Use of Formal Methods at Amazon Web Services" (2014)
MongoDB TLA+ "Design and Modular Verification of Distributed Transactions in MongoDB" (VLDB 2025)
TigerBeetle VOPR https://tigerbeetle.com/blog/2023-07-11-we-put-a-distributed-database-in-the-browser/
CVE-2022-35737 (Trail of Bits) https://blog.trailofbits.com/2022/10/25/sqlite-vulnerability-july-2022-library-api/
Fly.io B-Tree internals https://fly.io/blog/sqlite-internals-btree/

End of reference document. Ready for SpeckDL specification writing.