8000 fix: make on-disk header and WAL creation crash-safe under SIGKILL (#308) by adityamukho · Pull Request #349 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

fix: make on-disk header and WAL creation crash-safe under SIGKILL (#308) - #349

Merged
adityamukho merged 3 commits into
mainfrom
worktree-fix-308-header-atomicity
Aug 24, 2026
Merged

adityamukho merged 3 commits into
mainfrom
worktree-fix-308-header-atomicity

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Summary

  • Fixes SIGKILL during an active transact has a ~26% chance of permanently corrupting the entire database (header checksum, no recovery path) #308: a SIGKILL landing mid-checkpoint had a ~26% chance of permanently corrupting the database with no recovery path. FileBackend::write_page rewrote the header page on every single fact/index page append (to bump page_count) but left header_checksum stale, so a kill before the real commit in PersistentFactStorage::save left an on-disk header whose checksum didn't match its own content. Fix: recompute header_checksum every time write_page persists a page_count bump, so the header is self-consistent at every write, not just at the deliberate final commit.
  • The new SIGKILL fuzz test also caught a second, related bug: WalWriter::open_or_create creates the .wal file and writes its header in two separate steps, so a kill in between left a too-short WAL that validate_wal_header's read_exact choked on, equally blocking reopen. Since no entry can be appended before a WalWriter fully exists, a WAL shorter than its own header is always safe to treat as empty; WalReader::open and open_or_create now do so instead of erroring.
  • Adds tests/crash_kill_test.rs: spawns a child that loops transact + checkpoint() on a fresh .graph file and kills it with a real SIGKILL at a random point mid-loop, then confirms a fresh process can still open the file. A small round count (5) runs unconditionally in the per-PR suite; .github/workflows/crash-kill.yml runs 100 rounds nightly for statistical confidence (following the existing nfs-lock.yml/llvm-cov.yml pattern, including filing/commenting on a tracking issue on failure).
  • Adds 3 deterministic unit tests in src/wal.rs and 1 in src/storage/backend/file.rs locking in each invariant without relying on timing.

Test plan

  • cargo test — full suite green (694 lib tests + all integration suites)
  • cargo fmt --check, cargo clippy --all-features -- -D warnings — clean
  • Reverted the file.rs fix and confirmed crash_kill_test.rs reproduces the exact original failure (Header checksum mismatch: possible file corruption)
  • MINIGRAF_CRASH_KILL_ROUNDS=200+ against the fix, run 5 times (1000+ total rounds), zero corruption
  • Pre-push hook (fmt + clippy + test) passed

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

https://claude.ai/code/session_01Nsh3ZnmoK6PTaUbp4HcqHY

adityamukho and others added 3 commits August 24, 2026 17:28
)

A SIGKILL landing mid-checkpoint had a ~26% chance of permanently
corrupting the database with no recovery path. Root cause:
FileBackend::write_page rewrote the header page on every single
fact/index page append (to bump page_count) but left header_checksum
stale, so a kill before the real commit in PersistentFactStorage::save
left an on-disk header whose checksum didn't match its own content.

Fix: recompute header_checksum every time write_page persists a
page_count bump, so the header is self-consistent at every write, not
just at the deliberate final commit.

The new SIGKILL fuzz test (tests/crash_kill_test.rs) also caught a
second, related bug: WalWriter::open_or_create creates the .wal file
and writes its header in two separate steps, so a kill in between left
a too-short WAL that validate_wal_header's read_exact choked on,
equally blocking reopen. Since no entry can be appended before a
WalWriter fully exists, a WAL shorter than its own header is always
safe to treat as empty; WalReader::open and open_or_create now do so
instead of erroring.

Test coverage: a small round count runs unconditionally in the per-PR
suite; .github/workflows/crash-kill.yml runs 100 rounds nightly for
statistical confidence, following the existing nfs-lock.yml/
llvm-cov.yml pattern (including filing/commenting on a tracking issue
on failure).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsh3ZnmoK6PTaUbp4HcqHY
test_write_page_keeps_disk_header_checksum_consistent_on_page_count_bump
opened the file a second time via std::fs::read() while `backend` still
held it locked. Unix tolerates a second open from the same process;
Windows does not, failing CI with "another process has locked a portion
of the file". Read page 0 back through backend.read_page() instead,
which uses the already-open, already-locked handle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsh3ZnmoK6PTaUbp4HcqHY
#308's root cause was ordering of our own synchronous write calls, not
an OS-specific durability boundary -- it affected Windows and macOS
exactly as much as Linux. The per-PR smoke run already covers all three
(tests/crash_kill_test.rs isn't OS-gated, and rust.yml's build matrix
already runs it there), but the nightly 100-round confidence run was
Linux-only, copied from nfs-lock.yml/llvm-cov.yml whose Linux-only scope
is for real infra reasons (NFS server, coverage tooling) that don't
apply here. Matrix it across ubuntu/windows/macos instead, with a
per-OS tracking issue title to avoid races between concurrently
failing legs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsh3ZnmoK6PTaUbp4HcqHY
@codecov
codecov Bot commented Aug 24, 2026
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@adityamukho
adityamukho merged commit ef3996c into main Aug 24, 2026
17 checks passed
@adityamukho
adityamukho deleted the worktree-fix-308-header-atomicity branch August 24, 2026 12:53
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.

SIGKILL during an active transact has a ~26% chance of permanently corrupting the entire database (header checksum, no recovery path)

1 participant

0