diff options
Diffstat (limited to 'internal/apply/apply.go')
| -rw-r--r-- | internal/apply/apply.go | 20 |
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 } |
