aboutsummaryrefslogtreecommitdiff
path: root/internal/apply/apply.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 20:12:35 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 20:12:35 +0200
commit792e6e5416a51fb05f7171aa7ef4f9a284a8cf68 (patch)
treefe306d8517a2f4e6f6d9f4e171973ddee1171dea /internal/apply/apply.go
parentce9f7b10cf5025bdc2dfc08144f7efe011410a67 (diff)
downloadkrino-792e6e5416a51fb05f7171aa7ef4f9a284a8cf68.tar.gz
krino-792e6e5416a51fb05f7171aa7ef4f9a284a8cf68.zip
plan 8: apply refuses a source swapped for a symlink or another file
Diffstat (limited to 'internal/apply/apply.go')
-rw-r--r--internal/apply/apply.go20
1 files changed, 16 insertions, 4 deletions
diff --git a/internal/apply/apply.go b/internal/apply/apply.go
index f177a17..713913e 100644
--- a/internal/apply/apply.go
+++ b/internal/apply/apply.go
@@ -77,17 +77,29 @@ func Chain(c plan.Chain) []StepResult {
}
// checkUnchanged is the guard that matters most: before every step, the
-// source is stat'd and compared against the size and mtime the plan
-// recorded for the whole file. A file rewritten or replaced between
-// planning and applying must never be acted on.
+// source is Lstat'd and compared against what the plan recorded for the
+// whole file. A file rewritten or replaced between planning and applying
+// must never be acted on (spec ยง15.1): its size and mtime must match, it
+// must still be a regular file - not a symlink put in its place - and, at
+// its planned path, it must be the same inode. The inode is not compared
+// once an earlier step has moved the file: a move across filesystems
+// copies it to a new inode, and the chain is still following its own file.
func checkUnchanged(src string, f scan.File) error {
- fi, err := os.Stat(src)
+ fi, err := os.Lstat(src)
if err != nil {
return fmt.Errorf("changed since plan: %w", err)
}
+ if !fi.Mode().IsRegular() {
+ return errors.New("changed since plan: no longer a regular file")
+ }
if fi.Size() != f.Size || !fi.ModTime().Equal(f.ModTime) {
return errors.New("changed since plan")
}
+ if src == f.Path && f.Ino != 0 {
+ if now := scan.NewFile(src, f.Rel, fi); now.Dev != f.Dev || now.Ino != f.Ino {
+ return errors.New("changed since plan: another file is in its place")
+ }
+ }
return nil
}