Repository navigation
Description
Priority: P1
Area: Updater
Repository / target branch: wavect/semaprax / wavect/v090
Reviewed commit: 5428b0805fa3cf35859c843a53e30c3252ef378c
Review date: 6 October 2026
Failure and impact
A successful revocation can be erased by an update check that started earlier. check loads the entire update state, performs potentially slow fetches, and finally saves its original in-memory state. A concurrent revoke writes its revocation successfully while that fetch is outstanding. When check resumes, even with a network error, its final save overwrites the revocation and changes the source from unavailable back to available.
This defeats the documented rule that revoked revisions are never reactivated. The reproduction demonstrates the persisted authority state changing; it does not claim that a real revoked package was subsequently executed. A newly started session uses this state to determine its effective revisions, so losing the revocation is consequential even though existing sessions intentionally retain their own pins.
Exact source and cause
updates/ops.rs:393–432:checkloads before fetching and saves the whole snapshot afterward, including on the bounded network-error path.updates/ops.rs:552–568:revokeindependently loads, records the target, applies withdrawal/fallback, and saves.updates/state.rs:314–317andprofile/installations.rs:201–213: the save uses atomic replacement but has no transaction lock or revision comparison covering the read/modify/write operation. Atomic replacement alone does not detect stale writers.updates/ops.rs:587–622: policy approval and source registration use the same independently loaded state. The fix must cover all writers rather than only one pairing.HARNESS-UPDATES-V1.md:65–78: session pinning, rollback holds, and permanent revocation requirements.
Executed reproduction
The probe links against the unmodified semaprax_harness library built from the reviewed commit with Rust 1.88.0. It uses an empty temporary harness home and the public State, ops::check, and ops::revoke APIs.
- Save a valid source
demowith one active revision and no safe previous revision. - Enter
ops::check; let it load this state. - In the injected
Fetcher::releasesseam, call the realops::revokeagainst the same home. Verify the revocation is on disk andunavailable == true. - Return
SPX-HPU009from the fetcher, representing an outage. - Let
checkcomplete its error report and final save, then reload the state.
revocation_committed=true unavailable_after_revoke=true
check_status=error revoked_after_check=[] unavailable_after_check=false
The scheduling seam deterministically creates the same ordering as a second process issuing a revoke during a network fetch. Both mutations use actual production APIs. No remote fetch, real revocation, or installed package is needed. See repros/updates/repro_updates.rs and evidence/updates-evidence.txt in the review bundle. Its assertions intentionally demonstrate the current defect.
Focused fix
Give update-state mutations one transaction owner: an operating-system-backed lock or a revision-checked commit with safe reconciliation. Network staging may happen outside the lock, but publication must reload/revalidate current policy, revocations, active revision, pending changes, and rollback holds before changing state. Do not publish an entire stale snapshot over a newer successful operation.
Keep revocations monotonic across ordinary update checks. Preserve independent source and policy changes. Reuse the existing update pipeline and immutable artifact store; this does not require a new database or a second updater. Define bounded contention/failure behavior and release ownership on process exit. Merely choosing a unique temporary filename does not fix this lost-update bug.
Acceptance criteria
- A check paused after load cannot erase a successful concurrent revocation, including when the fetch later fails.
- A revoked active revision remains withdrawn or safely rolled back after either operation completes; a new session cannot select it.
- Check/apply/rollback/policy/source-registration writers share the same transaction discipline, and unrelated successful changes survive.
- Publication revalidates changes made during network staging and reports a controlled conflict or safely reconciles them.
- Add a deterministic two-writer regression and a bounded multi-process test with explicit barriers; verify crash/lock cleanup.
- Offline/frozen behavior, session pins, immutable staging, and existing approval boundaries remain intact.
Existing issue boundary and implementation handoff
#442 (HN-05) introduced the updater and its atomic publication requirement. This is a concrete stale-writer defect in that implementation, with a successful revocation demonstrably lost; no matching issue was found in the retrieved open/closed backlog. Read AGENTS.md and the update/identity specifications, then extend the existing update test module. Record the fixing commit and nonzero executed regression counts.
Activity
harness updates: lock state writers and parse ranges strictly
wsdt commentedon Oct 6, 2026
Fixed in a028b4d (pushed to main). Root cause: check/apply/rollback/revoke/approve_policy/add_source did unlocked load-modify-save, so a check that fetched over the network saved a stale snapshot over a concurrent revocation. All writers now hold an exclusive bounded flock on updates/state.lock (SPX-HPU017 when busy); check stages unlocked, then reloads under the lock, keeps any source that changed meanwhile, merges learned revocations and reports SPX-HPU018. Evidence:
cargo test --locked -p semaprax-harness(lib 165 pass; harness_v1 incl. new hp_dv03_* tests: revoke injected mid-check survives, 4 processes x 6 revocations all retained, lock released on owner death) and clippy -D warnings clean.