Found during the 2026-08-20 adversarial audit (Iceberg lens, but the defect is in backup/restore of the shared SQLite DB — auth tokens, audit, tiering, MQTT, and the Iceberg catalog). Two halves, one fix domain.
Half 1 — restore overwrites the live, open, WAL-mode DB
internal/backup/restore.go:241 does a plain os.WriteFile(destPath, data, 0600) over the live database, on a running server (POST /api/v1/backup/restore, detached goroutine), with no writer pause and no removal of the stale -wal/-shm sidecars. The auth manager holds the file open in WAL mode (internal/auth/auth.go:188); with Iceberg enabled the reconciler is a periodic writer to the same handle every reconcile_interval.
Open connections hold page caches and the shm-index for the old content, and the pre-restore -wal (frames salted against the old DB) survives beside the restored file. The next write or checkpoint — an iceberg catalog commit mid-reconcile, an auth last_used update — mixes stale WAL frames / cached pages into the restored database, corrupting it. Docs never say to stop/restart the server for a metadata restore (only restoreConfig mentions restart).
Half 2 — backup copies the live DB without a lock
internal/backup/backup.go:440-477 checkpoints (wal_checkpoint(TRUNCATE)) on a private connection, closes it, then copies the file with plain os.Open + ReadTo. Between checkpoint and end-of-copy, live writers (auth, audit, iceberg catalog commits) keep committing; once the WAL passes the auto-checkpoint threshold (~1000 pages), a checkpoint writes pages into the main file mid-copy → silently corrupt arc.db / iceberg-catalog.db in the backup, discovered only at restore time.
Fix shape
- Backup: use the SQLite Online Backup API or
VACUUM INTO (or hold a read transaction across the copy).
- Restore: at minimum remove
destPath+"-wal"/-shm after the write, log + document "restart required"; the real fix is refusing metadata restore while subsystems hold the DB, or restoring to a sidecar path applied at next boot.
Found during the 2026-08-20 adversarial audit (Iceberg lens, but the defect is in backup/restore of the shared SQLite DB — auth tokens, audit, tiering, MQTT, and the Iceberg catalog). Two halves, one fix domain.
Half 1 — restore overwrites the live, open, WAL-mode DB
internal/backup/restore.go:241does a plainos.WriteFile(destPath, data, 0600)over the live database, on a running server (POST /api/v1/backup/restore, detached goroutine), with no writer pause and no removal of the stale-wal/-shmsidecars. The auth manager holds the file open in WAL mode (internal/auth/auth.go:188); with Iceberg enabled the reconciler is a periodic writer to the same handle everyreconcile_interval.Open connections hold page caches and the shm-index for the old content, and the pre-restore
-wal(frames salted against the old DB) survives beside the restored file. The next write or checkpoint — an iceberg catalog commit mid-reconcile, an authlast_usedupdate — mixes stale WAL frames / cached pages into the restored database, corrupting it. Docs never say to stop/restart the server for a metadata restore (onlyrestoreConfigmentions restart).Half 2 — backup copies the live DB without a lock
internal/backup/backup.go:440-477checkpoints (wal_checkpoint(TRUNCATE)) on a private connection, closes it, then copies the file with plainos.Open+ReadTo. Between checkpoint and end-of-copy, live writers (auth, audit, iceberg catalog commits) keep committing; once the WAL passes the auto-checkpoint threshold (~1000 pages), a checkpoint writes pages into the main file mid-copy → silently corruptarc.db/iceberg-catalog.dbin the backup, discovered only at restore time.Fix shape
VACUUM INTO(or hold a read transaction across the copy).destPath+"-wal"/-shmafter the write, log + document "restart required"; the real fix is refusing metadata restore while subsystems hold the DB, or restoring to a sidecar path applied at next boot.