diff --git a/internal/restore/restore.go b/internal/restore/restore.go index eb175d6..e0bfd5f 100644 --- a/internal/restore/restore.go +++ b/internal/restore/restore.go @@ -103,6 +103,24 @@ func slot(s *session.Session, i int) string { return filepath.Join(s.Dir, "data", fmt.Sprintf("undo-%d", i)) } +// modeFromOctal turns a mode as the shim journals it (st_mode & 07777) +// into an os.FileMode. Go keeps the permission bits where POSIX has +// them but encodes setuid, setgid and sticky as flags of its own, and +// os.Chmod only looks at those, so casting drops all three. +func modeFromOctal(m uint64) os.FileMode { + fm := os.FileMode(m & 0o777) + if m&0o4000 != 0 { + fm |= os.ModeSetuid + } + if m&0o2000 != 0 { + fm |= os.ModeSetgid + } + if m&0o1000 != 0 { + fm |= os.ModeSticky + } + return fm +} + // Run replays the journal of s in the given direction. func Run(s *session.Session, dir Direction, opts Options) (*Result, error) { res := &Result{} @@ -325,8 +343,8 @@ func Run(s *session.Session, dir Direction, opts Options) (*Result, error) { if perr != nil { mode = 0o755 } - if err = os.MkdirAll(field(0), os.FileMode(mode)); err == nil { - err = os.Chmod(field(0), os.FileMode(mode)) + if err = os.MkdirAll(field(0), modeFromOctal(mode)); err == nil { + err = os.Chmod(field(0), modeFromOctal(mode)) } } else { if !exists(field(0)) { @@ -359,7 +377,7 @@ func Run(s *session.Session, dir Direction, opts Options) (*Result, error) { if !act() { continue } - err = os.Chmod(field(0), os.FileMode(mode)) + err = os.Chmod(field(0), modeFromOctal(mode)) case journal.OpLost: if dir == Undo { diff --git a/internal/restore/restore_test.go b/internal/restore/restore_test.go index 1d6fa22..4983505 100644 --- a/internal/restore/restore_test.go +++ b/internal/restore/restore_test.go @@ -168,6 +168,76 @@ func TestRmdirUndoRecreatesDirectory(t *testing.T) { } } +func TestModeFromOctalKeepsHighBits(t *testing.T) { + cases := []struct { + journal uint64 + want os.FileMode + }{ + {0o644, 0o644}, + {0o4755, 0o755 | os.ModeSetuid}, + {0o2775, 0o775 | os.ModeSetgid}, + {0o1777, 0o777 | os.ModeSticky}, + {0o7000, os.ModeSetuid | os.ModeSetgid | os.ModeSticky}, + } + for _, c := range cases { + got := modeFromOctal(c.journal) + if got != c.want { + t.Errorf("modeFromOctal(%04o) = %v, want %v", c.journal, got, c.want) + } + if got.Perm() != os.FileMode(c.journal&0o777) { + t.Errorf("modeFromOctal(%04o).Perm() = %04o", c.journal, got.Perm()) + } + } +} + +func TestRmdirUndoRestoresStickyDirectory(t *testing.T) { + work := t.TempDir() + gone := filepath.Join(work, "shared") + s := newSession(t, []journal.Entry{ + {Op: journal.OpRmdir, Fields: []string{gone, "1777"}}, + }) + if _, err := Run(s, Undo, Options{}); err != nil { + t.Fatal(err) + } + fi, err := os.Stat(gone) + if err != nil { + t.Fatal(err) + } + if fi.Mode()&os.ModeSticky == 0 { + t.Errorf("recreated directory lost the sticky bit: %v", fi.Mode()) + } + if fi.Mode().Perm() != 0o777 { + t.Errorf("recreated directory perm = %04o, want 0777", fi.Mode().Perm()) + } +} + +func TestChmodUndoRestoresSetuid(t *testing.T) { + work := t.TempDir() + bin := filepath.Join(work, "helper") + write(t, bin, "#!/bin/sh\n") + // the command ran `chmod 755` over a setuid binary, so the journal + // holds 4755 as the mode to go back to + if err := os.Chmod(bin, 0o755); err != nil { + t.Fatal(err) + } + s := newSession(t, []journal.Entry{ + {Op: journal.OpChmod, Fields: []string{bin, "4755", "755"}}, + }) + if _, err := Run(s, Undo, Options{}); err != nil { + t.Fatal(err) + } + fi, err := os.Stat(bin) + if err != nil { + t.Fatal(err) + } + if fi.Mode()&os.ModeSetuid == 0 { + t.Errorf("restored file lost the setuid bit: %v", fi.Mode()) + } + if fi.Mode().Perm() != 0o755 { + t.Errorf("restored file perm = %04o, want 0755", fi.Mode().Perm()) + } +} + func TestDryRunTouchesNothing(t *testing.T) { work := t.TempDir() victim := filepath.Join(work, "gone.txt")