Summary
Opening a path that is not a LibreDB WAL file (typo, wrong extension, or any foreign file) currently silently truncates that file to zero bytes with no error. This is a data-destruction bug reachable by any user running open({ path }) on a normal filesystem path.
Audit ID: B1 (Critical) — pre-announcement blocker
Source: docs/pre-announcement-notes.md §1, verified by 69-agent audit (2026-07-02/03)
Impact
libredb set notes.txt k v or a typo'd DB path permanently erases arbitrary user files.
- No error, no undo, no recovery path.
- Violates the project's honesty/durability positioning and is a launch blocker.
Root cause
In recover() (src/core.ts), when the file cannot be parsed as a valid WAL:
- The first record fails CRC/length checks at offset 0.
- The loop exits with
offset === 0.
if (offset < log.length) file.truncate(offset) runs and wipes the entire file.
function recover(file: WalFile): StoredEntry[] {
// ...
if (offset < log.length) file.truncate(offset);
return entries;
}
There is no magic/version header to distinguish a LibreDB file from arbitrary bytes.
Related issues
Proposed fix (agent implementation guide)
1. On-disk format change (do before announcement)
Add a file-level magic + version header at offset 0, written on first durable open/create:
- Example:
[4-byte magic "LRDB"][u16 formatVersion][u16 reserved] then existing records.
- New files always get the header before the first record append.
2. Refuse-don't-truncate semantics
| Scenario |
Behavior |
| Empty file (new DB) |
Write header, proceed |
| Valid headerless v0.1.x WAL (migration) |
Read-path accepts legacy format only when records validate; see below |
| Foreign/corrupt file at offset 0 |
Throw with clear libredb: error; never call truncate |
| Valid header, recoverable torn tail |
Truncate tail only (existing behavior) |
3. Migration read-path for headerless v0.1.x
Files written by current releases have no header but are valid WALs. Recovery must:
- Try header parse; if absent, attempt legacy record replay from offset 0.
- If legacy replay finds at least one valid record OR file is empty → treat as v0.1.x DB.
- If legacy replay fails at offset 0 on a non-empty file → throw (foreign file).
- Optionally rewrite header on next commit (lazy upgrade).
4. Tests (mandatory)
- Foreign file (e.g. UTF-8 text
hello) → open() throws; file bytes unchanged (read back from disk).
- Empty path creates valid DB with header.
- Headerless v0.1.x fixture → opens and reads correctly.
- Torn tail on valid DB still truncates only the tail.
Acceptance criteria
Agent constraints
- Read
CLAUDE.md, docs/DESIGN.md, ARCHITECTURE.md before editing src/core.ts.
- Kernel changes are guarded-core: minimal diff, one mechanism per concern.
- English only; no emoji; conventional commits.
- Do not re-litigate DESIGN.md decisions silently.
Suggested files to touch
src/core.ts — header constants, recover(), openLog(), possibly encodeRecord first-write path
src/core.test.ts — foreign-file, migration, header tests
src/sim/ — fixtures if needed for DST
docs/RELIABILITY.md — note format version if user-visible
Verification command
Summary
Opening a path that is not a LibreDB WAL file (typo, wrong extension, or any foreign file) currently silently truncates that file to zero bytes with no error. This is a data-destruction bug reachable by any user running
open({ path })on a normal filesystem path.Audit ID: B1 (Critical) — pre-announcement blocker
Source:
docs/pre-announcement-notes.md§1, verified by 69-agent audit (2026-07-02/03)Impact
libredb set notes.txt k vor a typo'd DB path permanently erases arbitrary user files.Root cause
In
recover()(src/core.ts), when the file cannot be parsed as a valid WAL:offset === 0.if (offset < log.length) file.truncate(offset)runs and wipes the entire file.There is no magic/version header to distinguish a LibreDB file from arbitrary bytes.
Related issues
Proposed fix (agent implementation guide)
1. On-disk format change (do before announcement)
Add a file-level magic + version header at offset 0, written on first durable open/create:
[4-byte magic "LRDB"][u16 formatVersion][u16 reserved]then existing records.2. Refuse-don't-truncate semantics
libredb:error; never calltruncate3. Migration read-path for headerless v0.1.x
Files written by current releases have no header but are valid WALs. Recovery must:
4. Tests (mandatory)
hello) →open()throws; file bytes unchanged (read back from disk).Acceptance criteria
open().not a libredb database fileor similar).bun run gategreen; 100% coverage maintained.Agent constraints
CLAUDE.md,docs/DESIGN.md,ARCHITECTURE.mdbefore editingsrc/core.ts.Suggested files to touch
src/core.ts— header constants,recover(),openLog(), possiblyencodeRecordfirst-write pathsrc/core.test.ts— foreign-file, migration, header testssrc/sim/— fixtures if needed for DSTdocs/RELIABILITY.md— note format version if user-visibleVerification command