Skip to content

high(backup): live shared-SQLite backup/restore is unsafe — torn copies on backup, stale-WAL corruption on restore #635

Description

@xe-nvdk

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions