Skip to content

[P0][kernel] Refuse opening non-LibreDB files — WAL magic header + no silent truncate #13

Description

@cevheri

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:

  1. The first record fails CRC/length checks at offset 0.
  2. The loop exits with offset === 0.
  3. 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:

  1. Try header parse; if absent, attempt legacy record replay from offset 0.
  2. If legacy replay finds at least one valid record OR file is empty → treat as v0.1.x DB.
  3. If legacy replay fails at offset 0 on a non-empty file → throw (foreign file).
  4. 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

  • Non-LibreDB files are never truncated by open().
  • Clear error message names the path and reason (not a libredb database file or similar).
  • Headerless v0.1.x databases still open (migration path).
  • New databases write the header.
  • bun run gate green; 100% coverage maintained.
  • Changeset required (on-disk format + observable open behavior).

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

bun run gate

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions