Skip to content

distclean's removal pass and survivor scan are both blind to a dangling-symlink output #376

Description

@typeless

Found while implementing #373 (PR #375), and independent of it: the fix there is sound, and this
gap is older than it.

The gap

clean and distclean decide whether a recorded output is on disk with pup::platform::exists,
which is stat-based and follows symlinks (src/platform/file_io-posix.cpp, the exists
implementation). A dangling symlink — a generated symlink whose target no longer exists —
therefore reads as absent.

Two consequences, in the same direction:

  1. The removal pass skips it (src/cli/cmd_clean.cpp, the if (!exists(abs_path_sv)) continue;
    guard in remove_indexed_outputs), so the symlink stays on disk after a clean.
  2. The survivor scan added by Keep the build record when a file it owns survives the reset #375 inherits the same blindness, so distclean counts zero
    survivors and discards the record — leaving exactly the unattributable state Keep the build record when a file it owns survives the reset #375 exists to
    prevent, for this one class of path.

The two passes are at least consistent with each other — they use the same predicate, so the scan
never disagrees with the removal about what is present. That is why this does not undermine #375's
invariant in general: the record is still discarded only when the scan believes nothing survives.
The defect is that the belief is wrong for dangling symlinks.

Open question, and it should be answered first

Can a build actually record a symlink output? That was not determined. If no rule can produce
one, this is theoretical and the issue should be closed as such — the cost of the fix is not worth
paying for an unreachable state. If a rule can (a ln -s recipe, or a tool that emits symlinks),
the state is reachable by removing the target afterwards, which is ordinary enough.

If it is reachable

The fix is a presence predicate that does not follow symlinks — lstat-shaped rather than
stat-shaped — used by both the removal pass and the survivor scan, so the two stay consistent.
That is a change to a platform primitive's contract (or a second primitive beside it), so it wants
a design-gate pass rather than a local patch: exists following symlinks is the right default for
most callers, and only the ownership paths want the other answer.

A dangling symlink is also the one case where "remove it" is unambiguously safe — there is no
target to lose.

Not urgent

No user has reported this. It is filed because it was found with evidence in hand, and because the
first question above is cheap to answer and decides whether anything else is worth doing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions