Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions internal/restore/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 {
Expand Down
70 changes: 70 additions & 0 deletions internal/restore/restore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down