You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(backup): switch to binary SQLite snapshot, eliminate cross-table data smearing
The previous dump-conf-db produced gzipped text SQL via `sqlite3 .dump`
to a shared `tmp.dmp` with no locking. Concurrent cron invocations
interleaved their stdout into the same file, and the resulting archive
parsed as valid (but semantically wrong) SQL — values from one table
smeared into INSERTs of unrelated tables on restore. Symptom reported
from the field: rows from m_CustomFiles surfacing inside m_ConferenceRooms
and garbage in m_OutgoingRoutingTable after restoration.
dump-conf-db
- Use the SQLite online backup API (.backup) for a page-consistent
binary snapshot instead of textual .dump. Eliminates the SQL
parseability that made interleaving silently dangerous.
- Atomic mkdir-lock with PID published via tmp+rename inside the
owned lock directory. Stale detection combines kill -0 with a
/proc/<pid>/comm + /proc/<pid>/cmdline two-stage identity check
to defeat PID reuse on long-uptime appliances.
- Stale-lock reclaim uses `mv lockDir lockDir.dead.\$\$` as the atomic
arbiter — racing reclaimers can no longer both win and double-rm.
- Per-PID temporary artefacts; .publish.*.gz staged on the same FS
as the final target and atomically renamed.
- WAL checkpointed before .backup; PRAGMA quick_check gates publish.
- Content dedup via md5 of logical .dump (binary pages differ each
run due to internal counters). Dedup is bypassed when no archive
exists on disk to prevent a poisoned .last_hash from permanently
disabling backups.
- Defensive sweep of orphaned .publish.*.gz older than 60 minutes.
- Single timestamp captured once to avoid hourly/daily date drift.
SystemConfiguration::tryRestoreConf
- Magic-byte detection (SQLite format 3\0) on the gunzipped payload:
binary snapshots are restored via atomic file copy, legacy SQL
dumps via sqlite3 replay (backward compat for pre-fix archives).
- pickFreshestBackup prefers the newest BINARY archive across all
mounted storage; legacy archives are chosen only when no binary
is available — prevents regression to the smearing path on boxes
with mixed-age archive directories.
- isBinaryArchive probes via popen+fread with a full pipe drain
before pclose to avoid SIGPIPE-induced false "probe failed" warnings
on every successful binary archive.
- Legacy SQL replay path emits LOG_WARNING so operators see the
fallback in syslog and can audit data after boot.
- purgeConfDb removes the live DB and -wal/-shm/-journal sidecars
via PHP glob+unlink (no shell glob, no rm).
- failRestore funnel: every failure path falls back to
DEFAULT_CONFIG_DB with an explicit syslog reason; if the fallback
copy itself fails it surfaces LOG_CRIT.
- Reboot loop guard via /var/run/conf-restored marker; @touch return
checked and reboot is refused if the marker cannot be created.
- foreign_key_check runs as a WARN-only diagnostic post-restore.
- Hard prerequisite check on Util::which('gzip')/('sqlite3'); the
guard uses str_contains('/') because Util::which falls back to the
bare command name rather than an empty string when nothing is found.
- Operator breadcrumb (RESULT_SKIPPED + LOG_NOTICE) when no backup
is found on any mounted storage.
Verified functionally in Docker (mikopbx/mikopbx-arm64:2026.1.21):
- Binary snapshot creation, dedup, atomic publish, lock contention
(5 parallel runs), stale-lock reclaim (synthetic PID), and
rotation all behave correctly.
- restoreFromArchive replays a real legacy SQL archive from
boffart.miko.ru (2026-05-07_mikopbx.db.gz, 39 tables) and
produces a quick_check-clean DB.
- pickFreshestBackup correctly picks an older binary archive over
a newer legacy archive — confirms prefer-binary protection
against re-introducing the smearing path on mixed shelves.
- Zero "gzip probe failed" entries in syslog after the drain-loop
fix; LEGACY SQL warning emitted as expected on the legacy path.
Also sets executable bit on dump-conf-db (mode 100755 in index).
0 commit comments