Skip to content

Windows update recovery marker is attacker-deletable, silencing the tamper refusal #868

Description

@Vasanthdev2004

Follow-up from #751, which closes the #742 overwrite primitive but leaves this narrower gap.

After a failed promotion where the restore is blocked, the only record that <target>.old holds the last verified binary is a .keep file sitting beside it in the install directory:

func oldBinaryPreserved(oldPath string) bool {
	_, err := os.Lstat(oldPath + oldBinaryPreservedSuffix)
	return err == nil || !errors.Is(err, os.ErrNotExist)
}

replace_windows.go:271. Pure pathname existence, no identity binding, nothing outside the install directory.

Under the same threat model as #742, a principal who can write that directory deletes the marker. On the next zero upgrade, preflightRecoveryStateLocked finds no marked recovery, os.Lstat(targetPath) succeeds because the attacker's file is there, and the run proceeds with no refusal. The operator is never told the target holds unverified bytes.

The recovery copy itself survives, so this is not the arbitrary-overwrite primitive. What is lost is the warning.

#751 already builds the mechanism to fix it. appendRecoveryCleanupRecord writes identity-bound records to a trusted per-user store under UserConfigDir()/zero/update-recovery, outside the attacker's reach. It is only reached on promote's success path (stage_windows.go:161); the failure branch returns first at :155. Writing an unresolved-state record there on failure too, and consulting it alongside the marker, closes this without new infrastructure.

Raised by @jatmn as a P2 on #751. Splitting it out so #751 can land on its own merits.

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