Skip to content

The "copy may still hold the patch" rule is written twice and drifted: cargo GC keeps a missing-blob modified copy, rollback's other-scope check drops it #1926

Open

Description

[agent] Filed by the scheduled architecture audit (audit-cli-tests). Register: T44.

Kind: refactor (with a latent drift). Register ID: T44.

Problem

Two CLI sites each turn a core verify_file_rollback result into "this installed copy may still hold the patched bytes, so keep the manifest record".

Copy A: ecosystem_dispatch.rs L64-L84 (cargo copies still patched):

let still = match v.status {
    VerifyRollbackStatus::Ready | VerifyRollbackStatus::MissingBlob => true,
    VerifyRollbackStatus::NotFound => !v.is_absent(),
    VerifyRollbackStatus::AlreadyOriginal | VerifyRollbackStatus::HashMismatch => false,
};

Copy B: rollback.rs L324-L345 (rollback's other-scope check):

let unverifiable = v.current_hash.is_none()
    && v.status != VerifyRollbackStatus::AlreadyOriginal
    && !v.is_absent();
if unverifiable || v.status == VerifyRollbackStatus::Ready
    || v.current_hash.as_deref() == Some(info.after_hash.as_str())

The drift. Core verify_file_rollback L229-L320 returns MissingBlob after the AlreadyOriginal check but before the current == after check. A MissingBlob result therefore says nothing about whether the copy is patched, and the two copies read it differently:

  • A file whose hash is neither before nor after, with the before-blob gone:
    • copy A says still patched (keep);
    • copy B says not patched.
  • npm's LF-shebang form of a patched bin (where source_hash == after_hash but current_hash != after_hash), with the blob gone:
    • copy B misses it;
    • copy A never sees npm.

I verified this by reading both copies and the core function. The rule is deterministic, so the two answers follow from the code. I did not run it end to end.

Proposed change

  1. Add VerifyRollbackResult::may_hold_patch(&self, info: &PatchFileInfo) -> bool in core patch/rollback.rs, next to the function whose status order it depends on. Pick one meaning for MissingBlob. Keeping the copy (copy A) is the fail-safe choice, because dropping the record strands patched bytes.
  2. Use it from both sites.
  3. Optionally, carry the npm-bin source_hash match in the result, so the predicate needs no CLI knowledge.

Size

3 files, roughly -25/+20 lines.

Acceptance criteria

  • Tests that guard the change:
    • the cargo_copies_still_patched callers (scan --prune, rollback);
    • rollback's other-scope tests.
  • Add one core unit test of may_hold_patch over every VerifyRollbackStatus, including MissingBlob with a third hash.
  • The PR states which MissingBlob reading it chose.

Activity

  1. added
    arch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)
    refactorStructural change: duplicated code or logic, missing abstraction, layering, dead code
    on Nov 1, 2026
  2. mikolalysenko commented on Nov 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged: priority:p3 (cross-cutting rollback/GC refactor; latent drift in the MissingBlob reading between ecosystem_dispatch.rs and rollback.rs). No duplicate or open fix PR found.


    Generated by Claude Code

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

    agent:triagedarch-auditFiled by a scheduled architecture audit routine (see the architecture review discussion)priority:p3refactorStructural change: duplicated code or logic, missing abstraction, layering, dead code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions