Update documentation with 39 changed files (#4810) - #4826
Conversation
Automatic checkpoint to preserve work in progress. Tests and implementation saved before refactoring phase.
…n guard The registered build-cache leaves are already canonical by contract (build_cache_leaf_dirs canonicalizes every entry), so vet_candidate's exact-match check can compare the freshly-canonicalized candidate path directly against the leaf list instead of re-resolving each leaf on every call. The candidate-side symlink_metadata + canonicalize already provides the symlink-swap defense; the per-leaf re-canonicalization added redundant filesystem syscalls with no behavioral effect. Behavior-equivalent: all guard rails, the symlink-swap and sibling-rejection tests, and the 13-test integration suite stay green. No public API change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
run_disk_reclaim walked the managed repos' build-cache leaves twice per routine tick: once via build_cache_candidates (for the merged candidate list) and again inside reclaim_candidates (for the guard allowlist). Each walk does read_dir over every repo's worktrees plus symlink_metadata + canonicalize per evictable leaf, so the duplication doubled the reclaim path's filesystem syscalls for no behavioral difference. Split reclaim_candidates into a public wrapper (computes the leaves, so CLI/daemon entry points still inherit the allowlist invariant) and a private reclaim_candidates_with_leaves that accepts them. run_disk_reclaim now walks the leaves once and reuses that single Vec for both the candidate list (build_cache_candidates_from_leaves) and the guard allowlist. Behavior-identical; leaf computation is a deterministic pure function of on-disk layout. Validation: disk_reclaim lib 78/78, integration 13/13, clippy + fmt clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the two actionable items from the Step 11 review synthesis on the issue #4810 build-cache producer. Item B (philosophy — lock the behavior-identical invariant): - Add `from_leaves_matches_full_walk_producer`: asserts `build_cache_candidates` (full filesystem walk) and `build_cache_candidates_from_leaves` (fed the same leaves the guard allowlist reuses on the production path) yield identical candidate sets across a multi-repo / multi-worktree layout. Converts the perf-split "one function calls the other" assumption into an enforced invariant on a file-deleting path — the candidate set can never diverge from the guard allowlist. Item A (security — TOCTOU pre-unlink re-stat, previously deferred): - The reference doc documented a `RemoveDir` type/owner re-stat as a committed safety property, but the executor only did canonicalize + under_any_root — a zero-BS doc/code discrepancy on a destructive path. Implement the re-stat (`restat_real_dir_owned`): immediately before `remove_dir_all`, the resolved canonical path must still be a real, non-symlink directory owned by the effective UID, else the candidate is aborted (one abort skips only that leaf, never fails the run). Closes the residual race where the canonical target is swapped to a symlink / re-owned in the window between vetting and unlink. - Tests: `restat_rejects_symlink_and_accepts_real_dir` (symlink + non-dir rejected, real euid-owned dir accepted) and `real_remover_deletes_real_dir_under_allow_root` (the new rail does not block legitimate deletion). Docs: add `build_cache_candidates_from_leaves` to the producer API reference, reflect the single-walk reuse in the orchestration flow, and correct the TOCTOU safety property to match the implemented re-stat. Validation: lib 81/81, integration 13/13, clippy (dev + release -D warnings) clean, fmt clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rysweet
left a comment
There was a problem hiding this comment.
Step 17b — Comprehensive Code Review
Reviewed the 9-file diff (core: src/disk_reclaim/{build_cache,executor,guard,mod}.rs + integration tests). Scope: deterministic build-cache producer for routine reclaim (issue #4810), leaf-only allow-scope widening, exact-canonical deny-set exemption, and pre-unlink TOCTOU re-stat.
Verdict: APPROVE — merge-ready. No blocking issues. Logic is correct, fail-closed throughout, and defense-in-depth is well applied. A few non-blocking observations below.
Strengths (verified, not assumed)
- Correct minimal widening.
ProtectedDenySet::containsis subtree-based (starts_with), so a<repo>/target/debug/*leaf inside a deny-protected managed repo would otherwise be rejected. The exemption is gated on exact-canonical match againstbuild_cache_leaves(is_registered_build_cache_leaf), so parents (target/debug) and siblings (target/debug/simard,target/debug/some-other-dir) are never exempted. Testsparent_target_debug_is_never_admittedandunregistered_sibling_outside_leaves_is_outside_allow_rootlock this. - Rails 3–4 still apply to exempted leaves.
live_process_interlock_still_vetoes_a_registered_leafproves an in-flightcargo buildis never deleted from under itself. Rail 4's.gitre-routing is untouched. - Fail-closed producer.
vetted_leafrequires a real, non-symlink, euid-owned dir (viasymlink_metadata, not following the final component) before canonicalizing; anything else is silently dropped.leaf_dirs_reject_symlinked_leafcovers the symlink-swap defense. - Trust boundary is tight. The entire widening rests on
build_cache_leaf_dirs(managed_repos())× the hardcodedEVICTABLE_CACHE_DIRSallowlist —debug/itself and final binaries are deliberately excluded (evictable_dirs_exclude_debug_root_and_binary). - Root refusal preserved.
effective_apply_modedowngrades apply→dry-run at the guarded core (euid 0), so every entry point inherits it, not just the CLI pre-check. - Perf split is behavior-preserving.
from_leaves_matches_full_walk_producerlocksbuild_cache_candidates≡build_cache_candidates_from_leaves, so the single-walk optimization inrun_disk_reclaimcan never diverge the candidate set from the guard allowlist. - Zero-BS clean: no TODOs/stubs/
unimplemented!/swallowed exceptions, noprint!/println!(structured tracing only), noBridgeidentifiers. The only.unwrap()s in scope are the pre-existingMutex::lock().unwrap()inCachingSizeMeasurer(not part of this diff).
Non-blocking observations
- Residual TOCTOU window (acknowledged, acceptable). Between the guard's
is_registered_build_cache_leafcanonicalize and the executor'srestat_real_dir_ownedthere remains a small window, and a second window between the re-stat andremove_dir_all. The re-stat meaningfully narrows it andstd::fs::remove_dir_alldoes not follow symlinks (CVE-2022-21658 fixed in Rust ≥1.58; toolchain is 1.95). Fully closing this would requireopenat/O_NOFOLLOW-based unlink — reasonably out of scope. Worth a one-line code comment noting the remaining window is bounded by the non-symlink-following removal, so a future reader doesn't assume it's race-free. - Triplicated "real, non-symlink, euid-owned dir" check. The same predicate now lives in
build_cache::vetted_leaf,guard::is_registered_build_cache_leaf, andexecutor::restat_real_dir_owned(each with a slightly different return shape). Correct and defense-in-depth by intent, but a sharedfn is_real_owned_dir(&Path) -> boolhelper would reduce drift risk. Non-blocking. - Per-candidate stat cost. When
build_cache_leavesis non-empty,is_registered_build_cache_leafaddssymlink_metadata+canonicalizeper candidate (and a second canonicalize in Rail 2 when it's not a leaf). Bounded byMAX_CANDIDATES; negligible in practice.
Checklist
- Code quality and standards — idiomatic, well-documented rationale on every non-obvious branch
- Test coverage adequate — 81 lib + 13 integration; all security-critical paths (parent, sibling, symlink, live-process, deny-set exemption, dry-run/apply, determinism) covered
- No TODOs, stubs, or swallowed exceptions
- No unimplemented functions
- Logic correctness — exact-canonical exemption + fail-closed producer verified against
containssubtree semantics - Edge case handling — symlink swap, non-dir, foreign owner, missing leaf, root euid, empty leaves all handled fail-closed
CI at review time: coverage / cargo-audit / cargo-deny / cargo-vet / npm-audit / scripts-tests / GitGuardian all green; pre-commit pending (no failures).
rysweet
left a comment
There was a problem hiding this comment.
Step 17c — Security Review (MANDATORY)
Scope: Destructive filesystem subsystem (disk reclaim, issue #4810). This code DELETES directories via remove_dir_all during routine daemon operation, so the review targeted data-loss / path-escape / TOCTOU classes on src/disk_reclaim/{executor,guard,build_cache,mod}.rs.
Verdict: APPROVE — no exploitable security vulnerabilities found (no Critical/High/Medium). Every relevant threat class is mitigated with concrete code evidence. CI green: cargo-audit, cargo-deny, cargo-vet, npm-audit, GitGuardian all pass.
Threat-class findings
1. Path safety / prefix-boundary — MITIGATED. under_any_root, is_safe_to_delete, and ProtectedDenySet::contains compare canonicalized absolute paths with Path::starts_with (component-wise, not string-wise), so /a/bc can never match root /a/b and target/debug can never match a target/debug/incremental leaf. Rail‑2 requires real != r && real.starts_with(&r) (strictly inside). No prefix-boundary bug.
2. Symlink / TOCTOU — MITIGATED (residual window is same-user, non-escalating). All producer/vetting gates use symlink_metadata (final component not followed) and fail-closed on symlink or stat error. The new pre-unlink restat_real_dir_owned (executor.rs) re-verifies real / non-symlink / euid-owned at the syscall boundary. Residual restat→unlink micro-window is neutralized because remove_dir_all on a final-component symlink unlinks the link without following it (Rust ≥1.95, post CVE‑2022‑21658 openat/O_NOFOLLOW traversal), and exploiting even that requires write access to the euid-owned parent — no cross-user or privilege escalation.
3. Deny-set exemption scope — MITIGATED (correctly minimal). is_registered_build_cache_leaf matches via exact PathBuf equality against a set containing only …/target/debug/{incremental,deps,build} (+ llvm-cov mirror). No parent/sibling/substring/pattern match — a protected managed repo or target/debug can never be admitted by the exemption. Rails 3–4 (live-process veto, .git re-routing) still apply to exempted leaves.
4. Ownership / privilege — MITIGATED (fail-closed). euid ownership enforced at production (build_cache.rs) and re-enforced at the syscall boundary for every RemoveDir. Apply path refused entirely under euid 0 (two independent guards). A root-owned or foreign-owned dir cannot be deleted.
5. Injection / command execution — MITIGATED. No shell anywhere. git_hardened uses an argv vector with env_clear() (blocks GIT_*/LD_PRELOAD), a -- separator, and rejects leading-dash paths. du/gh are argv-invoked with no interpolation.
6. Input trust — MITIGATED. Agent-proposed kind/reason/size are advisory only; the guard re-derives the primitive, re-measures size, and forces tracked-worktree vetting whenever a .git marker exists. Worktree enumeration uses read_dir + non-symlink file_type().is_dir().
Wiring consistency (verified)
The sole production construction of GuardContext/remover (mod.rs) threads the identical build_cache_leaves set into both allow_roots and the exemption set, and the executor independently re-checks under_any_root — double-locked. reclaim_candidates and run_disk_reclaim compute leaves from the hardcoded managed_repos(), so candidate set and allowlist cannot diverge (locked by from_leaves_matches_full_walk_producer).
Residual risk (non-blocking, accepted)
- restat→unlink micro-window: neutralized by non-following
remove_dir_all+ same-user euid confinement. A clarifying comment on the executor deletion branch would aid future maintainers.
No security remediation required. Branch is merge-ready from a security standpoint.
Step 17d — Philosophy Guardian ReviewReviewed the disk-reclaim changeset ( Compliance Checklist
Non-blocking observations (do not block merge)
VerdictPHILOSOPHY-COMPLIANT — merge-ready. The changeset embodies ruthless simplicity (minimal split, no speculative abstraction), respects brick/stud boundaries, and is zero-BS (every rail tested, no stubs/swallowed errors). The single actionable review item (producer equivalence test) and the discovered doc/impl discrepancy (TOCTOU re-stat) were both resolved with tested, non-over-engineered implementations. |
…al restat window Address Step 16 review feedback on PR #4826 (all three reviews APPROVE, non-blocking suggestions): - Item B: centralize the triplicated "real / non-symlink / euid-owned dir" check into `disk_reclaim::fs_predicates`. `is_real_dir` (used by the guard's build-cache-leaf membership check) and `is_real_dir_owned_by_euid` (used by `vetted_leaf` and the executor's pre-unlink re-stat) are now one fail-closed definition, so the symlink-swap / ownership-swap defenses cannot drift apart. Guard semantics are preserved (it still checks real-dir only, not euid). - Item A: document why the residual restat -> remove_dir_all micro-window is safe — `std::fs::remove_dir_all` unlinks a final-component symlink without following it, and the euid-ownership check confines any swap to same-user paths, so the blast radius is nil (worst case: one skipped candidate). Verified: cargo build --lib, clippy --lib, fmt --check all clean; disk_reclaim lib tests 81 passed, integration producer suite 13 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📊 Coverage Summary
Coverage data from CI run. Test files matching |
Summary
Concise workflow-generated PR for documentation.
Issue
Closes #4810
Changed files
Diff stat
Behavior
Implemented behavior through these branch commits:
dc52fbd harden(disk-reclaim): pre-unlink re-stat + producer equivalence test
3096483 perf(disk-reclaim): compute build-cache leaves once per routine reclaim
e4826f1 refactor(disk-reclaim): drop redundant per-leaf re-canonicalization in guard
91fbf32 wip: checkpoint after implementation (steps 7-8)
Validation
ℹ NODE_OPTIONS=--max-old-space-size=32768 (saved preference). To change: /home/azureuser/.amplihack/config
I'll start by getting the branch name and understanding the changes made in this PR.
● Get branch name and changed files (shell)
│ cd /home/azureuser/src/Simard-deploy-4049/worktrees/feat/issue-4810-simard-process-health-defect
│ -the-ooda-daemon-rides && git branch --show-current && echo "---CHANGED FILES---" && git status
│ --short && echo "---DIFF STAT vs main---" && git --no-pager diff --stat origin/main...HEAD
│ 2>/dev/null || git --no-pager diff --stat main...HEAD 2>/dev/null
└ 14 lines…
Let me examine the changes and understand the toolchain.
Risk
No high-risk subsystem pattern detected from changed paths.
Checklist
This PR was created as a draft for review before merging.
Step 16b: Outside-In Testing Results
Tested from the PR branch (
feat/issue-4810-simard-process-health-defect-the-ooda-daemon-rides) as a consumer of the disk-reclaim library boundary.Detected toolchain: Rust / Cargo (
Cargo.tomlat repo root,cargo 1.95.0), edition 2024. No Node/Python test surface for this change. Changed files scoped tosrc/disk_reclaim/**+tests/disk_reclaim_build_cache_producer.rs.Environment note: The host root FS (
/) was at 100% (0 bytes free) — the exact defect this PR fixes (ooda.logshowed the routinedisk reclaim: 98% -> 98% used, freed 0 bytes ... skipped for review+ recurringEMERGENCY disk cleanuploop). To validate without touching the running daemon's active build target, tests were compiled into an externalCARGO_TARGET_DIRon a separate volume (/mnt/home-data, 209G free). The root disk was not modified.Chosen strategy: Native
cargo test(per qa-team Rust-CLI guidance — no gadugi-agentic-test), one simple unit scenario + one edge/integration scenario, both exercising the reclaim candidate selection and the guarded executor boundary.build_cache,guard,executor,modproducers/rails)CARGO_TARGET_DIR=… cargo test --lib disk_reclaim::test result: ok. 81 passed; 0 failed— incl.build_cache::tests::leaf_dirs_reject_symlinked_leaf,from_leaves_matches_full_walk_producer,merge_dedup_by_path_appends_only_new_pathstarget/debug, emergency net & interlocks intactCARGO_TARGET_DIR=… cargo test --test disk_reclaim_build_cache_producertest result: ok. 13 passed; 0 failed— incl.parent_target_debug_is_never_admitted,apply_frees_nonzero_and_stops_at_target_minimum_necessary,registered_leaf_is_exempt_from_deny_set_but_only_by_exact_canonical_match,live_process_interlock_still_vetoes_a_registered_leaf,unregistered_sibling_outside_leaves_is_outside_allow_root,producer_is_deterministic_and_env_independentAdditional checks: No stray
print!/println!/eprintln!added to non-testsrc/disk_reclaim/**(structured tracing/OTel only). CI at time of testing:coverage,cargo-audit,cargo-deny,cargo-vet,npm-audit,scripts-tests, GitGuardian all green;pre-commitrunning.Fix count: 0 — both scenarios passed on the first run; no diagnose/fix/commit iterations were required.