Skip to content

Add sandbox-safe SQLite backup and warn against host-side database access - #172

Merged
akemmanuel merged 1 commit into
akemmanuel:masterfrom
touch2be:fix/sandbox-safe-sqlite-backup
Oct 23, 2026
Merged

akemmanuel merged 1 commit into
akemmanuel:masterfrom
touch2be:fix/sandbox-safe-sqlite-backup

Conversation

@touch2be

Copy link
Copy Markdown
Collaborator

Problem

On a gVisor-hosted Remote Host, account and Session-access writes from about 16 hours silently disappeared after a routine container restart. Chats stayed in opengui-harness-v1.sqlite, but their host_session_access rows were gone, so they vanished from the owner's Session list.

Cause: SQLite coordinates WAL access through POSIX locks and the shared-memory -shm index. gVisor does not share either with processes on the host machine. A host-side connection therefore thinks it is the last connection. When it closes, it checkpoints and deletes the live -wal/-shm. This includes a plain sqlite3 opengui-identity-v1.sqlite "SELECT …". The Host keeps writing to the now-unlinked WAL and still serves its own writes, so nothing looks wrong until the next restart. Then everything since that host-side access is gone.

docs/operations.md and docs/sandbox-hosting.md currently recommend exactly this pattern for online backups ("use SQLite's backup API or VACUUM INTO from an operator-controlled process").

Reproduction (runsc)

  1. A Python writer runs in a --runtime=runsc container with a bind-mounted data dir. It uses WAL mode with wal_autocheckpoint=0 and keeps its connection open.
  2. It inserts row A.
  3. On the host, run sqlite3 t.sqlite "select count(*) from t". Afterwards -wal and -shm are gone from the directory.
  4. The container inserts row B and still sees [A, B].
  5. After the container is killed, the file on disk contains only [A].

If the same snapshot is taken inside the sandbox, nothing is lost.

Change

  • scripts/backup-databases.ts: a new operator command that follows the reset-password.ts pattern:
    docker exec opengui node --experimental-strip-types scripts/backup-databases.ts [target-dir]
    • Uses node:sqlite's online backup() from inside the Host's container/sandbox, so SQLite's locking stays coherent.
    • Writes self-contained snapshots with journal_mode = DELETE, no sidecars, quick_check verified and mode 0600. The default target is $OPENGUI_DATA_DIR/backups/<UTC timestamp>/, so host-side tools only ever copy closed files.
  • Docs (operations.md, sandbox-hosting.md, docker.md): replace the external-process backup advice with this command. Add an explicit warning never to open the live databases from outside the sandbox, read-only CLI queries included, and explain the failure mode.
  • Tests (scripts/backup-databases.test.ts):
    • Snapshots capture commits that exist only in the WAL while a live connection holds it open.
    • Snapshots have no sidecars and use 0600 / journal_mode=delete.
    • The live WAL is left in place, and later writes still persist.
    • A missing database is skipped.

Verified with pnpm run check, pnpm vp test scripts/backup-databases.test.ts and pnpm run slop-check, all passing. I also ran the script inside a production gVisor container: both snapshots were written, and the live -wal inodes on the host were unchanged.

Possible follow-up (not in this PR)

Defense in depth inside the Host: identity writes are small and rare, so they can sit in the WAL for hours before an auto-checkpoint. A PRAGMA wal_checkpoint(PASSIVE) after identity writes, or a periodic one, would get committed rows into the main file promptly. That would greatly reduce what a stray host-side connection can destroy. Happy to send that separately if you want it.

🤖 Generated with Claude Code

Under gVisor, SQLite's POSIX locks and -shm index are not shared with
processes on the host machine. A host-side connection (even a read-only
sqlite3 SELECT) believes it is the last connection, checkpoints and
deletes the live -wal on close, and the Host keeps writing to the
unlinked file. All later writes are lost at the next restart.

The docs recommended exactly that for online backups ("use SQLite's
backup API from an operator-controlled process"). Add
scripts/backup-databases.ts, run via docker exec inside the sandbox
like reset-password.ts, and correct the backup guidance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@akemmanuel
akemmanuel merged commit 0fce71b into akemmanuel:master Oct 23, 2026
2 checks passed
@akemmanuel akemmanuel mentioned this pull request Oct 23, 2026
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.

2 participants