SQLite WAL journal mode corrupts runtime DBs (logs_2, state_5, …) when CODEX_HOME is on a network filesystem (NFS)

Open 💬 2 comments Opened Jul 2, 2026 by dkhokhlov
💡 Likely answer: A maintainer (dkhokhlov, contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

codex-cli 0.137.0

What subscription do you have?

Business

Which model were you using?

gpt-5.4-mini

What platform is your computer?

Linux 6.12.94+deb13-amd64 x86_64 unknown

What terminal emulator and version are you using (if applicable)?

ssh

Codex doctor report

report.json

What issue are you seeing?

When CODEX_HOME (~/.codex) is on an NFSv4.2 mount, codex's runtime SQLite databases periodically corrupt. logs_2.sqlite is hit most often (it is the high-volume trace/feedback writer), but the same root cause affects all four runtime DBs (state_5.sqlite, logs_2.sqlite, goals_1.sqlite, memories_1.sqlite) because they all open with PRAGMA journal_mode=WAL.

Observed: PRAGMA integrity_check returns database disk image is malformed (DB header corruption); codex's auto-recovery moves the damaged DB into ~/.codex/db-backups/ and rebuilds a fresh one. With logs_2 this is merely annoying (ephemeral, recreatable); with state_5 it degrades the resume index (codex falls back to the JSONL rollout scan, so data isn't lost, but the index is rebuilt repeatedly).

Relevant mount: mini7:/mnt/home on /home type nfs4 (rw,relatime,vers=4.2,hard,proto=tcp,local_lock=none,sec=sys). CODEX_HOME is shared across multiple hosts (codex run from different SSH sessions/machines against the same home).

What steps can reproduce the bug?

  1. Put CODEX_HOME on NFSv4 (e.g. ~/.codex under an NFSv4.2 mount, local_lock=none, shared by ≥1 host).
  2. Run codex normally over time / from more than one host against the same CODEX_HOME.
  3. After some uptime, inspect the runtime DBs:

sqlite3 ~/.codex/logs_2.sqlite 'PRAGMA integrity_check;'
sqlite3 ~/.codex/state_5.sqlite 'PRAGMA integrity_check;'

  1. Expected to eventually return database disk image is malformed (header) rather than ok.
  2. Equivalently, observe codex auto-creating ~/.codex/db-backups/<ts>/ and rebuilding the DB on startup.

Root cause is in codex-rs/state/src/runtime.rs::base_sqlite_options, which hardcodes SqliteJournalMode::Wal for every runtime DB.

What is the expected behavior?

Runtime SQLite DBs should not corrupt when CODEX_HOME is on a network filesystem. SQLite's own documentation states WAL must not be used on network filesystems (mmap'd -shm is not coherent across
NFS clients — https://www.sqlite.org/wal.html, §"Use of WAL with Network Filesystems"). Codex should use a rollback journal mode (e.g. Truncate) on NFS and keep Wal only on local filesystems.

Additional information

Root cause. SQLite WAL coordinates readers/writers through a mmap'd shared-memory index file (*-shm). On NFS each client has its own page-cache view of -shm; mmap coherence is not guaranteed
across clients, so a WAL checkpoint can write wrong data back to the DB → header corruption. This is independent of fcntl locking — SQLite's POSIX byte-range locks work on NFSv4; the failure is
the separate mmap'd index. Rollback journal modes (Delete/Truncate) create no -shm, so they don't have this exposure.

Empirical verification (NFSv4.2 host, sqlite 3.46.1):

  • fcntl byte-range locks are enforced on this mount (local_lock=none → stateful server-side NFSv4 locks); a conflicting LOCK_EX|LOCK_NB gets EAGAIN.
  • WAL creates -shm while a connection is open; Delete/Truncate create no -shm.
  • Delete/Truncate on NFS: 4 concurrent writers × 200 rows = 800 rows, 0 database is locked errors, integrity_check=ok.
  • Latency on NFS, batched 1000 rows/txn (codex's log_db.rs flush path): WAL 0.0041 s, Truncate 0.0041 s (== WAL), Delete 0.0055 s. Rollback modes are effectively free vs WAL on the batched path.

Proposed fix. In base_sqlite_options, choose journal mode from the DB path's filesystem instead of hardcoding Wal:

  • Linux: statfs(2), compare f_type to NFS_SUPER_MAGIC (0x6969).
  • macOS: statfs, check MNT_NFS in f_flags.
  • Platforms without a portable statfs (Windows, BSD, …): fall back to Wal (current behavior — never worse than today).
  • statfs error or an otherwise-unclassifiable mount: bias toward the NFS-safe mode (Truncate) rather than silently keeping WAL, since this is a corruption-prevention fix.

On NFS use SqliteJournalMode::Truncate (rollback journal, no -shm, NFS-safe; == WAL on the batched path, less metadata churn than Delete because it truncates the journal instead of create+unlink per commit). On local filesystems keep Wal. The decision can be factored into a pure helper for unit testing (the statfs call can't be exercised in CI without an NFS mount). This fixes the corruption for all runtime DBs, not just logs_2.

Current workaround. A wrapper deletes logs_2.sqlite{,-wal,-shm} on each codex startup so every session starts with a clean WAL state. This works only because logs_2 is ephemeral; it does not protect state_5/goals_1/memories_1, and is not a source-level fix.

View original on GitHub ↗

2 Comments

dkhokhlov contributor · 1 month ago

I have a tested patch for this on a branch in my fork: dkhokhlov:codex state/sqlite-truncate-journal-on-nfs (tree view
(https://github.com/dkhokhlov/codex/tree/state/sqlite-truncate-journal-on-nfs) · compare against main
(https://github.com/openai/codex/compare/main...dkhokhlov:codex:state/sqlite-truncate-journal-on-nfs)).

Approach (minimal)

In codex-rs/state/src/runtime.rs::base_sqlite_options, replace the hardcoded SqliteJournalMode::Wal with a per-path choice — Truncate when the DB path is on NFS, Wal otherwise. The decision is factored into a pure pick_journal_mode(bool) helper so the policy is unit-testable without an NFS mount.

NFS detection

  • Linux: statfs(2), compare f_type to NFS_SUPER_MAGIC (0x6969).
  • macOS: statfs, check MNT_NFS in f_flags.
  • The DB file may not exist yet on first open (create_if_missing), so the probe inspects the path's parent directory (a bare filename falls back to Path::new(".")).
  • statfs error / interior-NUL path / otherwise-unclassifiable mount: bias toward the NFS-safe mode (Truncate) rather than silently keeping WAL — this is a corruption-prevention fix, so the failure mode fails closed.
  • Platforms without a portable statfs (the cfg(not(...)) catch-all: Windows, BSD, …): fall back to Wal, so non-NFS behavior on those platforms is unchanged from today.

libc is added as a target-gated dependency (cfg(any(target_os = "linux", target_os = "macos"))).

Why Truncate (not Delete or Persist)

  • Truncate/Delete/Persist are rollback-journal modes: reader/writer coordination lives in fcntl byte-range locks + a plain journal file — no mmap'd *-shm — so they are NFS-safe.
  • Truncate over Delete: amortizes journal-file creation (one create + truncate(0) per commit instead of create+unlink), less metadata churn to the NFS server.
  • Truncate over Persist: lower churn, and the more widely exercised rollback mode.
  • On codex's batched write path it measured at parity with WAL latency (numbers in the issue body).
  • On local filesystems mmap is coherent, so Wal stays the fast default — no change for the common case.

Known detection limitations (documented in the patch's doc comment)

  • A container overlay whose upperdir is on an NFS-backed volume reports the overlay magic, not NFS → Wal is kept there.
  • Windows SMB/UNC shares have the same mmap-coherence problem but aren't detected (catch-all assumes local).
  • BSDs also expose statfs + an NFS flag but aren't handled here.

Verified

  • cargo fmt -p codex-state --check — clean
  • cargo clippy -p codex-state --all-targets — clean
  • cargo test -p codex-state — 154 passed (incl. new tests for the statfs magic comparison, the local-path probe, and the parent-directory fallback for a not-yet-created DB file)
  • Manual end-to-end: real NFSv4.2 home (stat -f → type_hex=6969 type=nfs) → Truncate; local ext4 path → Wal.

Scope

All four runtime DBs (state_5, logs_2, goals_1, memories_1) since the root cause is shared — they all route options through base_sqlite_options. If the team prefers a logs-only initial change, the helper makes that a one-line call-site difference.

Notes

  • An already-corrupt DB on NFS won't be repaired by this — it will fail to open. The fix is preventive; users hitting the corruption still need to delete the DB (consistent with the local workaround described in the issue).
  • Happy to open a PR if the team would like to invite one. I've already accepted the CLA (recorded by CLA Assistant on #6719 (https://github.com/openai/codex/pull/6719)), so there's no CLA blocker.
vladak · 3 days ago

Also impacts Codex 0.146.0 CLI running on Solaris 11.4 wih home directories mounted from Solaris server using NFSv4.1 with the following symptom:

Codex couldn't start because its local database appears to be damaged.
Run `codex doctor` to check your setup and get next-step guidance.
If this keeps happening, share the technical details below when asking for help.
Technical details:
  Location: /home/XXX/.codex/state_5.sqlite
  Cause: failed to initialize state runtime at /home/XXX/.codex: failed to open state DB at /home/XXX/.codex/state_5.sqlite: error returned from database: (code: 5386) disk I/O error: error returned from database: (code: 5386) disk I/O error: (code: 5386) disk I/O error
ERROR: failed to initialize sqlite local db at /home/XXX/.codex/state_5.sqlite: failed to initialize state runtime at /home/XXX/.codex: failed to open state DB at /home/XXX/.codex/state_5.sqlite: error returned from database: (code: 5386) disk I/O error: error returned from database: (code: 5386) disk I/O error: (code: 5386) disk I/O error