Repository navigation
apply, apply --check and vendor report noManifest (exit 0) when .socket/manifest.json exists but can't be stat'd #998
Description
[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: bug. Source: new finding; register C56. A #[ignore = "RED: …"] test added in #708 already pins this bug, but no issue tracks it and no CI job runs it.
Problem (main @ 9c43dfc)
Core's read_manifest treats only ErrorKind::NotFound as "no manifest", and its own regression test says any other I/O error must surface as Err. Five CLI commands run their own existence probe before calling it, and that probe treats any stat error as "missing":
if tokio::fs::metadata(&manifest_path).await.is_err() { /* no manifest */ }apply.rs#L835: the cleannoManifestno-op, exit 0.vendor.rs#L756:noManifest, exit 0.repair.rs#L67,remove.rs#L353androllback.rs#L1044:manifest_not_found, or a bare "Manifest not found" string from rollback.
list and vendor --check go through read_manifest and report the contract's manifest_unreadable correctly. The contract (CLI_CONTRACT.md#L1291-L1293) defines manifest_not_found as "doesn't exist" and manifest_unreadable as an I/O error reading the manifest.
Proof by execution. I used a debug build at 9c43dfc, ran every command under env -i … --json, and ran the whole set twice with identical results. There were two fixtures: .socket as a regular file (ENOTDIR), and .socket/manifest.json as a symlink to itself (ELOOP).
| Command | Result |
|---|---|
apply |
status: noManifest, exit 0 |
apply --check |
status: noManifest, exit 0 |
vendor |
status: noManifest, exit 0 |
repair, remove <purl> |
manifest_not_found, exit 1 |
rollback |
{"error":"Manifest not found"}, exit 1 |
list, vendor --check |
manifest_unreadable (Too many levels of symbolic links), exit 1 |
The pinned test, apply_with_unreadable_socket_dir_fails_closed,`` uses a chmod 000 .socket/ (EACCES) fixture. It self-skips as root, which is why I reproduced the bug with ENOTDIR and ELOOP instead.
Symptoms
No open issue. The test's own comment states the impact: install hooks and CI steps run apply --silent and read exit 0 as "patched".
Impact
apply and vendor fail open. Every patch in a project whose .socket/ can't be traversed is silently left unapplied with a success exit; the triggers are a root-owned or ACL-restricted .socket/ on a CI runner, a symlink loop, or .socket checked in as a file. The three other commands fail closed but with the wrong code, and their remedy text points at a missing file. Small fix, real CI consequence.
Proposed change
- Add one core probe next to
read_manifest, for examplemanifest::probe(path) -> Result<Presence, io::Error>(or reuseread_manifest'sOk(None)directly), with the same NotFound-only rule. - Delete the five
metadata(&manifest_path).await.is_err()probes and route those commands through it:- a non-NotFound error becomes
manifest_unreadable(exit 1) onapply,apply --check,vendor,repair,removeandrollback; - a real NotFound keeps today's behavior:
noManifestonapply/vendor, the hosted/vendored-trace fallbacks onrepair/remove/rollback.
- a non-NotFound error becomes
- Un-ignore
apply_with_unreadable_socket_dir_fails_closed, and add a root-proof variant (ENOTDIR or ELOOP) so the test runs in CI containers.
Size and scope
About 40 production lines across apply.rs, vendor.rs, repair.rs, remove.rs, rollback.rs and manifest/operations.rs, plus about 80 test lines. Out of scope:
- unparseable-manifest codes, which are An unparseable .socket/manifest.json is reported under five different --json error codes depending on the command #931 (same family; this issue is the stat/read half);
rollback's bare-string envelope, which is Decide: one shape for the--jsontop-levelerror(scan and get emit both a string and a {code, message} object) #704.
Acceptance criteria
-
grep -rn "metadata(&manifest_path).await.is_err()" crates/socket-patch-cli/srcfinds nothing. - With
.socketas a file, and with a self-referencingmanifest.jsonsymlink,apply,apply --check,vendor,repair,removeandrollbackall exit 1 withmanifest_unreadableunder--json. -
apply_with_unreadable_socket_dir_fails_closedis no longer#[ignore]d, and an ENOTDIR twin runs as root. - The existing no-manifest tests stay green (
applynoManifestexit 0 and its human line;repair's hosted-only skip;remove/rollbackledger-only and hosted-only paths), along with core'sread_manifestNotFound tests.
Dependencies
None blocking. It pairs with #931 (one manifest-load error mapping); whichever lands second reuses the other's helper.
Backlog review — 2026-10-08
Priority: P3 → P2. Permission/stat failures are converted into a successful noManifest no-op. This is a fail-open patching/checking behavior, not a diagnostic-only inconsistency.
Activity
audit-core: C55 decision #1000; file C56 (#998), C57 (#999); fix Part…
refactor: re-rank queue, add #998 (C56)
mikolalysenko commentedon Oct 7, 2026
[agent] Triaged as
priority:p3(general CLI, not tied to one ecosystem). It isn't a duplicate, and no open PR covers it. I confirmed it onmain(9c43dfc): all fivetokio::fs::metadata(&manifest_path).await.is_err()probes are still there (apply.rs:835,vendor.rs:756,repair.rs:67,remove.rs:353,rollback.rs:1044), andapply_with_unreadable_socket_dir_fails_closedis still#[ignore = "RED: …"]attests/apply/apply_invariants.rs:514. #931 is related but doesn't share the cause: #931 is about the codes for a manifest that can't be parsed, and this issue is about the stat probe. I'm leaving them as separate pieces of work.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Re-checked on main @
b762f41. The bug still holds and the code has moved, so here are fresh permalinks. All five probes still treat any stat error as "no manifest":apply.rs#L838vendor.rs#L912repair.rs#L67remove.rs#L353rollback.rs#L1051The containment helper merged in #1042 doesn't touch these probes.
Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
v5 triage: P3, not a release blocker. A metadata/stat failure on an existing agent manifest is exceptional filesystem-error handling. P3, outside the happy-path release gate.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 9, 2026
[agent] I re-checked this on main @
31383f5. It still holds. Six commands still decide "no manifest" withtokio::fs::metadata(..).is_err(), which treats any stat error as absence, not onlyNotFound:apply.rs#L1064vendor.rs#L968repair.rs#L68remove.rs#L362rollback.rs#L932scan/gc.rs#L202Generated by Claude Code