package safefile import ( "fmt" "os" "path/filepath" "strings" "testing" "time" "golang.org/x/sys/unix" ) func reapTestStage(t *testing.T, parent, name string, data bool) string { t.Helper() path := filepath.Join(parent, name) if err := os.MkdirAll(path, 0o700); err != nil { t.Fatal(err) } if data { if err := os.WriteFile(filepath.Join(path, "data"), []byte("abandoned"), 0o600); err != nil { t.Fatal(err) } reapTestAge(t, filepath.Join(path, "data")) } reapTestAge(t, path) return path } func reapTestAge(t *testing.T, path string) { t.Helper() old := time.Now().Add(-2 * time.Hour) if err := os.Chtimes(path, old, old); err != nil { t.Fatal(err) } } func TestReapStaleWriterStages(t *testing.T) { root := t.TempDir() var removed, retained []string for i, parent := range []string{root, filepath.Join(root, "versions", "v1"), filepath.Join(root, "staging")} { removed = append(removed, reapTestStage(t, parent, fmt.Sprintf(".carlos-write-%032x", i+1), true)) } removed = append(removed, reapTestStage(t, root, ".carlos-write-"+strings.Repeat("e", 32), false)) fresh := reapTestStage(t, root, ".carlos-write-"+strings.Repeat("f", 32), true) now := time.Now() if err := os.Chtimes(fresh, now, now); err != nil { t.Fatal(err) } retained = append(retained, fresh) active := reapTestStage(t, root, ".carlos-write-"+strings.Repeat("a", 32), true) if err := os.Chtimes(filepath.Join(active, "data"), now, now); err != nil { t.Fatal(err) } retained = append(retained, active) for _, name := range []string{"ordinary", ".carlos-write-abc", ".carlos-write-" + strings.Repeat("A", 32), ".carlos-write-" + strings.Repeat("0", 33), ".carlos-write-" + strings.Repeat("g", 32)} { retained = append(retained, reapTestStage(t, root, name, true)) } if err := Reap(root); err != nil { t.Fatal(err) } for _, path := range removed { if _, err := os.Lstat(path); !os.IsNotExist(err) { t.Errorf("stale stage remains: %s: %v", path, err) } } for _, path := range retained { if _, err := os.Stat(filepath.Join(path, "data")); err != nil { t.Errorf("retained stage changed: %s: %v", path, err) } } if err := Reap(filepath.Join(root, "missing")); err != nil { t.Fatal(err) } } func TestReapRefusesUnsafeStages(t *testing.T) { for _, kind := range []string{"stage-symlink", "data-symlink", "hardlink", "fifo", "nested", "extra", "writable", "foreign-owner"} { t.Run(kind, func(t *testing.T) { root := t.TempDir() outside := t.TempDir() victim := filepath.Join(outside, "victim") if err := os.WriteFile(victim, []byte("untouched"), 0o600); err != nil { t.Fatal(err) } name := ".carlos-write-" + strings.Repeat("b", 32) path := reapTestStage(t, root, name, false) var err error switch kind { case "stage-symlink": if err = os.Remove(path); err == nil { err = os.Symlink(outside, path) } case "data-symlink": err = os.Symlink(victim, filepath.Join(path, "data")) case "hardlink": err = os.Link(victim, filepath.Join(path, "data")) case "fifo": err = unix.Mkfifo(filepath.Join(path, "data"), 0o600) case "nested": reapTestStage(t, path, name, true) case "extra": err = os.WriteFile(filepath.Join(path, "unknown"), nil, 0o600) case "writable": err = os.Chmod(path, 0o777) case "foreign-owner": if os.Geteuid() != 0 { t.Skip("requires root to make a foreign-owned stage") } err = os.Chown(path, 65534, 65534) } if err != nil { t.Fatal(err) } if kind != "stage-symlink" { reapTestAge(t, path) } if err := Reap(root); err != nil { t.Fatal(err) } if _, err := os.Lstat(path); err != nil { t.Fatalf("unsafe stage removed: %v", err) } if kind == "nested" { if _, err := os.Stat(filepath.Join(path, name, "data")); err != nil { t.Fatalf("unexpected recursive deletion inside stage: %v", err) } } if body, err := os.ReadFile(victim); err != nil || string(body) != "untouched" { t.Fatalf("victim changed: %q %v", body, err) } }) } } func TestReapPinsRootAndRefusesSwappedStage(t *testing.T) { root := t.TempDir() parent := filepath.Join(root, "parent") out := filepath.Join(root, "outside") name := ".carlos-write-" + strings.Repeat("c", 32) stage := reapTestStage(t, parent, name, true) victim := reapTestStage(t, out, name, true) dir, err := OpenDir(parent) if err != nil { t.Fatal(err) } defer dir.Close() pinned, err := OpenDir(stage) if err != nil { t.Fatal(err) } defer pinned.Close() if err := os.Rename(stage, stage+"-moved"); err != nil { t.Fatal(err) } if err := os.Symlink(victim, stage); err != nil { t.Fatal(err) } if err := reapStage(int(dir.Fd()), name, pinned, time.Now().Add(-time.Hour)); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(stage+"-moved", "data")); err != nil { t.Fatalf("replaced entry was not refused: %v", err) } if err := os.Rename(parent, parent+"-moved"); err != nil { t.Fatal(err) } if err := os.Symlink(out, parent); err != nil { t.Fatal(err) } if err := reapTree(dir, time.Now().Add(-time.Hour), 0); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(victim, "data")); err != nil { t.Fatalf("followed swapped parent: %v", err) } if err := Reap(parent); err == nil { t.Fatal("accepted symlink root") } } func TestReapRefusesBindMount(t *testing.T) { root := t.TempDir() out := t.TempDir() name := ".carlos-write-" + strings.Repeat("d", 32) victim := reapTestStage(t, out, name, true) mount := filepath.Join(root, "mounted") if err := os.Mkdir(mount, 0o700); err != nil { t.Fatal(err) } if err := unix.Mount(out, mount, "", unix.MS_BIND, ""); err != nil { if err == unix.EPERM || err == unix.EACCES { t.Skip("bind mounts unavailable") } t.Fatal(err) } defer func() { if err := unix.Unmount(mount, 0); err != nil { t.Errorf("unmount: %v", err) } }() if err := Reap(root); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(victim, "data")); err != nil { t.Fatalf("traversed bind mount: %v", err) } } func TestReapContinuesPastUnreadableDirectory(t *testing.T) { if os.Geteuid() == 0 { t.Skip("root bypasses directory permissions") } root := t.TempDir() unreadable := filepath.Join(root, "unreadable") if err := os.Mkdir(unreadable, 0o700); err != nil { t.Fatal(err) } if err := os.Chmod(unreadable, 0); err != nil { t.Fatal(err) } defer os.Chmod(unreadable, 0o700) stale := reapTestStage(t, root, ".carlos-write-"+strings.Repeat("9", 32), true) if err := Reap(root); err == nil { t.Fatal("unreadable directory was not reported") } if _, err := os.Lstat(stale); !os.IsNotExist(err) { t.Fatalf("unreadable sibling prevented cleanup: %v", err) } }