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
The core invariant aw-sync's whole conflict-free story rests on — stated in aw-sync/README.md as "each device only writes to files in the sync folder they own, and other devices may not modify them" — is violated on every pull.
What happens
create_datastore opens a peer database read-write:
and Datastore::new (aw-datastore/src/worker.rs) unconditionally:
let journal_mode:String = conn
.pragma_update_and_check(None,"journal_mode","WAL", |row| row.get(0)).expect("Failed to query journal_mode");
...conn.pragma_update(None,"synchronous","FULL")
...
let mut ds = DatastoreInstance::new(&conn,true).unwrap();
So reading a peer's file:
Flips its journal_mode to WAL, creating -wal and -shm sidecars inside a directory owned by another device.
Runs schema migrations on it — DatastoreInstance::new(&conn, true) upgrades user_version toward the local binary's version.
Writes synchronous = FULL.
All three are writes to a file this device does not own, performed by a read-only operation, in a directory an external file syncer is actively replicating.
Why it matters concretely
Version skew makes it a real migration, not a no-op. Peer databases in a live sync folder sit at different user_versions, below current master's. An older peer's file gets silently upgraded by whichever device pulls it first — and that device may be running a newer aw-server-rust than the peer that owns the file. The owner then reopens a database migrated by someone else's binary.
It manufactures sync conflicts. Two devices pulling the same peer, or a pull racing the owner's push, are concurrent writers to one file. My folder already contains the fingerprints: erb-main3/5a5df0f8-…/test.sync-conflict-20241125-052022-GRUSU5T.db plus a second from the same day.
The -wal/-shm sidecars are themselves replicated, and a syncer that delivers a main file and its WAL at different moments produces exactly the torn snapshot the sidecars were meant to prevent.
Open peer databases read-only and side-effect-free:
sqlite3_open_v2 with SQLITE_OPEN_READONLY, or a file:…?mode=ro URI
never run migrations on a file this device does not own — if user_version is unrecognised, refuse that peer and report it (there is already a // TODO: Check for compatible remote db version before opening in sync.rs), rather than upgrading it
do not set journal_mode or synchronous on peer files
if a read-only open is impractical for a WAL-mode peer, copy the file to a scratch location first and open the copy — never the original
Note today's Datastore::new also spawns a worker thread and min/max-scans every bucket on open, which is heavy for what a pull needs; a lightweight read-only open path would help #684's status command too.
Credit: found in a design review of aw-sync; the code path above is verified against origin/master, the conflict files are from a live sync folder.
Related: #689, #691 (sqlite as wire format), #682.
The core invariant aw-sync's whole conflict-free story rests on — stated in
aw-sync/README.mdas "each device only writes to files in the sync folder they own, and other devices may not modify them" — is violated on every pull.What happens
create_datastoreopens a peer database read-write:and
Datastore::new(aw-datastore/src/worker.rs) unconditionally:So reading a peer's file:
journal_modeto WAL, creating-waland-shmsidecars inside a directory owned by another device.DatastoreInstance::new(&conn, true)upgradesuser_versiontoward the local binary's version.synchronous = FULL.All three are writes to a file this device does not own, performed by a read-only operation, in a directory an external file syncer is actively replicating.
Why it matters concretely
user_versions, below current master's. An older peer's file gets silently upgraded by whichever device pulls it first — and that device may be running a newer aw-server-rust than the peer that owns the file. The owner then reopens a database migrated by someone else's binary.erb-main3/5a5df0f8-…/test.sync-conflict-20241125-052022-GRUSU5T.dbplus a second from the same day.-wal/-shmsidecars are themselves replicated, and a syncer that delivers a main file and its WAL at different moments produces exactly the torn snapshot the sidecars were meant to prevent.Fix
Open peer databases read-only and side-effect-free:
sqlite3_open_v2withSQLITE_OPEN_READONLY, or afile:…?mode=roURIuser_versionis unrecognised, refuse that peer and report it (there is already a// TODO: Check for compatible remote db version before openinginsync.rs), rather than upgrading itjournal_modeorsynchronouson peer filesNote today's
Datastore::newalso spawns a worker thread and min/max-scans every bucket on open, which is heavy for what a pull needs; a lightweight read-only open path would help #684'sstatuscommand too.Credit: found in a design review of aw-sync; the code path above is verified against
origin/master, the conflict files are from a live sync folder.Related: #689, #691 (sqlite as wire format), #682.
cc @TimeToBuildBob