aboutsummaryrefslogtreecommitdiff
path: root/internal/apply/apply.go
diff options
context:
space:
mode:
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
}