Expand description
The deleter: the half that cannot be undone.
§Why there is a plan
Every safety check happens while the Plan is being built, and removal executes the
plan without re-deciding anything. That is what makes --dry-run honest rather than
approximate: the thing printed is the same object the deleter consumes, so a preview
cannot disagree with the run it previews.
§Resolving a path without following it
Proving a target is under the scan root means resolving .. and any symlinked ancestor,
which is what fs::canonicalize does — except that canonicalising the target would
also resolve the target itself, and a symlinked claim (Bazel’s bazel-*) must be unlinked
as a link rather than followed to whatever it points at. So the parent is canonicalised
and the final component is joined back on. Nothing can hide in the ancestry, and the leaf
is left alone.
§The checks that fail toward “keep”
Tier two’s review (#588) found three bugs of the same shape: a check whose failure mode is
silence, so an unreadable or unseen subtree reads as a cleared one. The deleter inherits
that discipline. A directory it could not read is a failure, not an empty directory; a
subtree it refused to enter leaves every ancestor standing, because the rmdir is only
attempted when every child is known to be gone.
§The under-root check is not enough on its own, and why nothing is removed by name
The plan’s check is defence in depth against a malformed, stale or hostile path arriving
from a caller, a config file or a scan of a tree someone else can write to. But what it
proves, it proves about a path, and a path is a name that something else can re-point.
A check like that is worth what it is worth at the moment of the unlink, not at the
moment it ran — and in between sit a printed plan and a confirmation prompt.
So the removal never re-walks a target by name. It opens the scan root once and then
descends by descriptor: every component is opened from its already-open parent with
openat(fd, name, O_DIRECTORY | O_NOFOLLOW), and every removal is an unlinkat against
the descriptor of the directory that holds the entry. A component swapped for a symlink
fails the open with ELOOP rather than redirecting it, because the kernel resolves one
name against one held descriptor and there is no path left for anything to re-point.
“Under the root” becomes a property of how the syscall was issued.
cap-primitives supplies those calls. It is the same machinery cap-std is built from,
and it is why this does not need libc and therefore does not need the unsafe this
crate forbids: the descent this module always wanted turns out to be reachable in safe
Rust. The root is opened once per batch rather than once per target, so the root itself
cannot be swapped mid-run either.
One path still has to be resolved by name, and it cannot be avoided: the scan root has to
be opened from somewhere. That makes it the most dangerous name in the program rather than
an exempt one, because every descriptor descends from that handle — get it wrong and the
whole batch is misdirected, not one target. So [open_root] opens the root’s final
component with O_NOFOLLOW from its own parent, and then checks the descriptor’s
(device, inode) against the pair recorded while the plan was built. The second check is
the one that matters: a root renamed away and replaced by an ordinary directory on the same
filesystem offers no symlink to refuse and crosses no boundary, so nothing about the name
distinguishes it from the directory the planner validated. Only the inode does.
§Fan-out
unlink and rmdir are latency-bound rather than CPU-bound, so the pool is deliberately
oversubscribed — the same conclusion the Node predecessor reached empirically. The unit of
work is one target, not one directory: a sweep, which is the mode this exists for, has
hundreds of targets, and per-target parallelism would need a join counter per directory to
know when its rmdir is safe. Removing a single target is therefore single-threaded, as
rm -rf is.
Structs§
- Deleter
- Removes what a
Plansays to remove, and nothing else. - Failure
- Something that went wrong. Collected rather than fatal.
- Freeing
- How far into one target a sweep has got.
- Plan
- A resolved, checked list of directories to remove.
- Plan
Target - A target that survived every check, with its path resolved.
- Planner
- Builds a
Planunder a fixed policy. - Refused
- One directory that was left alone, and why.
- Removal
- What a removal did.
- Removed
- One target that was removed, in whole or in part.
- Target
- A directory offered for removal.
Enums§
Functions§
- confirm
- Asks a yes/no question whose answer defaults to no.