Problem
projection.BuildPlan validates the output root and all expected targets, but Plan.Apply revalidates only stale removals. Creates and updates proceed through replaceFile without confirming that the output root and target still have the type and ownership observed during planning.
Between planning and applying, another process can:
- create an unmanaged file at a target that was previously missing, which is then overwritten; or
- replace the validated output directory with a symlink, allowing writes outside the originally canonicalized root.
This breaks the filesystem-safety guarantees under a time-of-check/time-of-use race.
Relevant code
internal/projection/writer.go: BuildPlan, Plan.Apply, and replaceFile
tests/projection/writer_test.go: removal revalidation exists, but write/root revalidation does not
Expected behavior
Immediately before each write, verify that the output root still resolves to the planned root and that the target still satisfies the planned create/update precondition. Abort without overwriting an unmanaged or redirected path.
Acceptance criteria
- A planned create refuses an unmanaged file introduced before
Apply.
- A planned update refuses a target whose ownership or type changes before
Apply.
- Replacing the output root with a symlink before
Apply cannot write outside the planned root.
- Existing stale-removal revalidation continues to pass.
Problem
projection.BuildPlanvalidates the output root and all expected targets, butPlan.Applyrevalidates only stale removals. Creates and updates proceed throughreplaceFilewithout confirming that the output root and target still have the type and ownership observed during planning.Between planning and applying, another process can:
This breaks the filesystem-safety guarantees under a time-of-check/time-of-use race.
Relevant code
internal/projection/writer.go:BuildPlan,Plan.Apply, andreplaceFiletests/projection/writer_test.go: removal revalidation exists, but write/root revalidation does notExpected behavior
Immediately before each write, verify that the output root still resolves to the planned root and that the target still satisfies the planned create/update precondition. Abort without overwriting an unmanaged or redirected path.
Acceptance criteria
Apply.Apply.Applycannot write outside the planned root.