Config and State saves have no file locking (watch races one-shot commands) #11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/crumb-10-config-state-lock"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Config::save()andState::save()serialised and thenfs::write()with no lock.breadcrumbs watchruns continuously alongside one-shot commands (add,forget,scan,profile set, andflow::run's own credential clearing), each doing a load, modify, write cycle over the samebreadcrumbs.toml/networks.toml/state.toml. Two genuinely concurrent cycles could interleave and the later write silently drop the earlier one's change.Approach
An exclusive
flock(2)on a sidecar.lockfile, held for the whole load, modify, write cycle, so concurrent cycles queue instead of clobbering each other. Plus atomic writes so a lock-free reader never sees a half-written file.src/lock.rs(new):acquire(dir) -> Guard, an exclusivestd::fs::File::lock()on<dir>/.lock. No new dependency (File::lockis stable;bread-utils' ownsingletonmodule already usesFile::try_lock). The.lockfile is left on disk between acquisitions (unlinking it per use would race the path).app::real_maintakes the config-dir lock for the whole run of every command except:watch(long-lived; it locks per tick instead, see below)cd(no cycle) andedit(opens$EDITOR; holding the lock for an interactive session would stall the daemon, and the editor does its own write)watchloop takes the config-dir lock for the working part of each tick (reload + schedule switch + anyflow::run_quiet) and releases it before the idle wait, so one-shot commands are not blocked between ticks. The pre-loop initial flow is covered too.State::savetakes the state-dir lock internally. It never callsState::load, so it cannot self-deadlock, and every caller that also holds the config lock takes it in config-then-state order.Config::save/State::savenow write atomically (temp file then rename;0o600set on the temp file up front for the two config files) viabread_utils::atomic. This replaces the post-writesecure_permissions()chmod, which had a brief window where the file existed with default perms.Config::load/saveandState::loadthemselves stay lock-free; only the small number of cycle-owning call sites lock, so the same process never takes the config lock twice and deadlocks.Tradeoff
A slow command that holds the config lock (
init --wait N) can stall the watch daemon's next tick by up to that long. That is deliberate: a brief stall is better than a corrupt or dropped write, and the lock acquire failing at all is treated as non-fatal (log and proceed unlocked) so a weird filesystem never bricks the daemon.Verification
cargo build --release --locked,cargo clippy --all-targets --locked -- -D warnings: cleancargo test --release --locked: 69 lib (incl. 3 newlocktests, one of which spawns a thread and asserts the secondacquireblocks until the firstGuarddrops) + 42 cli + 36 flow_watch, all passcargo fmt: my changes add zero new diffs (the tree has pre-existing fmt drift that this repo's CI does not gate; left untouched)Closes #10