Skip to content

[DV-03] [P1] Prevent stale update-state writers from erasing successful revocations #563

Closed

Description

@wsdt

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

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.

  1. Save a valid source demo with one active revision and no safe previous revision.
  2. Enter ops::check; let it load this state.
  3. In the injected Fetcher::releases seam, call the real ops::revoke against the same home. Verify the revocation is on disk and unavailable == true.
  4. Return SPX-HPU009 from the fetcher, representing an outage.
  5. Let check complete 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

  1. wsdt commented on Oct 6, 2026

    @wsdt
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions