diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 20:12:35 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 20:12:35 +0200 |
| commit | 792e6e5416a51fb05f7171aa7ef4f9a284a8cf68 (patch) | |
| tree | fe306d8517a2f4e6f6d9f4e171973ddee1171dea /internal/apply/apply.go | |
| parent | ce9f7b10cf5025bdc2dfc08144f7efe011410a67 (diff) | |
| download | krino-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.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 } |
