//go:build linux package instancefs import ( "errors" "os" "path/filepath" "syscall" "testing" ) // A new directory takes its mode from mkdirat and its group and setgid // from its parent, the way the kernel hands them out; the stat returned is // what is on disk. func TestEnsureDirBeneathCreatesAndInheritsSetgid(t *testing.T) { dir := t.TempDir() if err := os.Chmod(dir, 0o750|os.ModeSetgid); err != nil { t.Fatal(err) } st, err := EnsureDirBeneath(dir, "run", 0o700) if err != nil { t.Fatalf("EnsureDirBeneath: %v", err) } fi, err := os.Lstat(filepath.Join(dir, "run")) if err != nil { t.Fatal(err) } if !fi.IsDir() || fi.Mode().Perm() != 0o700 || fi.Mode()&os.ModeSetgid == 0 { t.Fatalf("mode = %v, want a directory 0700 with setgid inherited", fi.Mode()) } if st.Mode != fi.Mode() || st.UID != os.Geteuid() || st.GID != int(fi.Sys().(*syscall.Stat_t).Gid) { t.Fatalf("stat = %+v, disk says %v", st, fi.Mode()) } } // An existing directory is reported as it stands and never changed: a // chmod by a caller outside the directory's group clears setgid, so the // caller verifies instead. func TestEnsureDirBeneathLeavesAnExistingDirectoryAlone(t *testing.T) { dir := t.TempDir() d := filepath.Join(dir, "d") if err := os.Mkdir(d, 0o700); err != nil { t.Fatal(err) } if err := os.Chmod(d, 0o777|os.ModeSetgid); err != nil { t.Fatal(err) } st, err := EnsureDirBeneath(dir, "d", 0o700) if err != nil { t.Fatalf("EnsureDirBeneath over an existing directory: %v", err) } want := os.ModeDir | os.ModeSetgid | 0o777 if st.Mode != want { t.Errorf("stat mode = %v, want %v", st.Mode, want) } if fi, _ := os.Lstat(d); fi.Mode() != want { t.Errorf("mode changed to %v", fi.Mode()) } } func TestEnsureDirBeneathRefusesASymlinkAndLeavesItsTargetAlone(t *testing.T) { dir, elsewhere := t.TempDir(), t.TempDir() if err := os.Chmod(elsewhere, 0o755); err != nil { t.Fatal(err) } if err := os.Symlink(elsewhere, filepath.Join(dir, "d")); err != nil { t.Fatal(err) } if _, err := EnsureDirBeneath(dir, "d", 0o700); !errors.Is(err, ErrSymlink) { t.Fatalf("err = %v, want ErrSymlink", err) } fi, _ := os.Stat(elsewhere) if fi.Mode().Perm() != 0o755 { t.Fatalf("the symlink's target was changed to %v", fi.Mode()) } } func TestEnsureDirBeneathRefusesAFileAndAPathName(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "f"), nil, 0o600); err != nil { t.Fatal(err) } if _, err := EnsureDirBeneath(dir, "f", 0o700); err == nil { t.Fatal("a regular file accepted as a directory") } if _, err := EnsureDirBeneath(dir, "a/b", 0o700); !errors.Is(err, ErrEscapes) { t.Fatalf("a multi-component name: err = %v, want ErrEscapes", err) } if _, err := EnsureDirBeneath(dir, "..", 0o700); !errors.Is(err, ErrEscapes) { t.Fatalf("..: err = %v, want ErrEscapes", err) } } func TestEnsureDirBeneathRefusesASymlinkedParent(t *testing.T) { base, real := t.TempDir(), t.TempDir() link := filepath.Join(base, "link") if err := os.Symlink(real, link); err != nil { t.Fatal(err) } if _, err := EnsureDirBeneath(link, "d", 0o700); !errors.Is(err, ErrSymlink) { t.Fatalf("err = %v, want ErrSymlink", err) } if _, err := os.Lstat(filepath.Join(real, "d")); !errors.Is(err, os.ErrNotExist) { t.Fatal("a directory was created through the symlinked parent") } } func TestStatDirReportsModeAndGroupAndRefusesASymlink(t *testing.T) { base := t.TempDir() d := filepath.Join(base, "d") if err := os.Mkdir(d, 0o700); err != nil { t.Fatal(err) } if err := os.Chmod(d, 0o750|os.ModeSetgid); err != nil { t.Fatal(err) } st, err := StatDir(d) if err != nil { t.Fatal(err) } if st.Mode != os.ModeDir|os.ModeSetgid|0o750 || st.GID != os.Getegid() { t.Fatalf("stat = %+v", st) } if err := os.Symlink(d, filepath.Join(base, "link")); err != nil { t.Fatal(err) } if _, err := StatDir(filepath.Join(base, "link")); !errors.Is(err, ErrSymlink) { t.Fatalf("err = %v, want ErrSymlink", err) } if _, err := StatDir(filepath.Join(base, "absent")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("err = %v, want ErrNotExist", err) } } func TestRenameBetweenMovesFilesAndDirectories(t *testing.T) { src, dst := t.TempDir(), t.TempDir() if err := os.WriteFile(filepath.Join(src, "h.db"), []byte("db"), 0o600); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(src, "shadow"), 0o700); err != nil { t.Fatal(err) } if err := RenameBetween(src, "h.db", dst, "h.db"); err != nil { t.Fatalf("file: %v", err) } if err := RenameBetween(src, "shadow", dst, "shadow"); err != nil { t.Fatalf("directory: %v", err) } if b, err := os.ReadFile(filepath.Join(dst, "h.db")); err != nil || string(b) != "db" { t.Fatalf("moved file: %q %v", b, err) } if fi, err := os.Lstat(filepath.Join(dst, "shadow")); err != nil || !fi.IsDir() { t.Fatalf("moved directory: %v %v", fi, err) } } // A symlink at the source name is moved as the link it is, never followed: // rename works on directory entries. func TestRenameBetweenMovesASymlinkAsALink(t *testing.T) { src, dst, elsewhere := t.TempDir(), t.TempDir(), t.TempDir() target := filepath.Join(elsewhere, "secret") if err := os.WriteFile(target, []byte("s"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(target, filepath.Join(src, "h.env")); err != nil { t.Fatal(err) } if err := RenameBetween(src, "h.env", dst, "h.env"); err != nil { t.Fatal(err) } if fi, err := os.Lstat(filepath.Join(dst, "h.env")); err != nil || fi.Mode()&os.ModeSymlink == 0 { t.Fatalf("destination is not the link: %v %v", fi, err) } if _, err := os.Lstat(target); err != nil { t.Fatalf("the link's target moved: %v", err) } } func TestRenameBetweenRefusesASymlinkedDirectoryOnEitherSide(t *testing.T) { base, real := t.TempDir(), t.TempDir() link := filepath.Join(base, "link") if err := os.Symlink(real, link); err != nil { t.Fatal(err) } plain := t.TempDir() if err := os.WriteFile(filepath.Join(plain, "h.db"), []byte("db"), 0o600); err != nil { t.Fatal(err) } if err := RenameBetween(plain, "h.db", link, "h.db"); !errors.Is(err, ErrSymlink) { t.Fatalf("symlinked destination: err = %v, want ErrSymlink", err) } if _, err := os.Lstat(filepath.Join(real, "h.db")); !errors.Is(err, os.ErrNotExist) { t.Fatal("the file landed through the symlinked destination") } if err := os.WriteFile(filepath.Join(real, "x"), nil, 0o600); err != nil { t.Fatal(err) } if err := RenameBetween(link, "x", plain, "x"); !errors.Is(err, ErrSymlink) { t.Fatalf("symlinked source: err = %v, want ErrSymlink", err) } } func TestRenameBetweenRefusesPathNames(t *testing.T) { a, b := t.TempDir(), t.TempDir() for _, bad := range [][2]string{{"x/y", "y"}, {"y", "../y"}, {"", "y"}, {"y", "."}} { if err := RenameBetween(a, bad[0], b, bad[1]); !errors.Is(err, ErrEscapes) { t.Errorf("%q -> %q: err = %v, want ErrEscapes", bad[0], bad[1], err) } } } func TestRemoveBeneathRemovesTheEntryNotItsTarget(t *testing.T) { dir, elsewhere := t.TempDir(), t.TempDir() target := filepath.Join(elsewhere, "keep") if err := os.WriteFile(target, []byte("k"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(target, filepath.Join(dir, "link")); err != nil { t.Fatal(err) } if err := RemoveBeneath(dir, "link"); err != nil { t.Fatal(err) } if _, err := os.Lstat(target); err != nil { t.Fatalf("the link's target was removed: %v", err) } if err := RemoveBeneath(dir, "link"); !errors.Is(err, os.ErrNotExist) { t.Fatalf("second remove: err = %v, want ErrNotExist", err) } } func TestRemoveAllBeneathRemovesATree(t *testing.T) { dir := t.TempDir() for _, p := range []string{"run/a", "run/sub/b", "run/sub/deeper/c"} { full := filepath.Join(dir, p) if err := os.MkdirAll(filepath.Dir(full), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte("x"), 0o600); err != nil { t.Fatal(err) } } if err := RemoveAllBeneath(dir, "run"); err != nil { t.Fatal(err) } if _, err := os.Lstat(filepath.Join(dir, "run")); !os.IsNotExist(err) { t.Fatalf("run survived: %v", err) } if err := RemoveAllBeneath(dir, "run"); err != nil { t.Fatalf("a missing name must be nil: %v", err) } } func TestRemoveAllBeneathNeverFollowsASymlink(t *testing.T) { dir, outside := t.TempDir(), t.TempDir() keep := filepath.Join(outside, "keep") if err := os.WriteFile(keep, []byte("x"), 0o600); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(dir, "run"), 0o700); err != nil { t.Fatal(err) } if err := os.Symlink(outside, filepath.Join(dir, "run", "link")); err != nil { t.Fatal(err) } if err := os.Symlink(outside, filepath.Join(dir, "top")); err != nil { t.Fatal(err) } if err := RemoveAllBeneath(dir, "run"); err != nil { t.Fatal(err) } if err := RemoveAllBeneath(dir, "top"); err != nil { t.Fatal(err) } if _, err := os.Stat(keep); err != nil { t.Fatalf("a symlink's target was removed: %v", err) } if _, err := os.Lstat(filepath.Join(dir, "top")); !os.IsNotExist(err) { t.Fatalf("the top-level symlink itself survived: %v", err) } } func TestRemoveAllBeneathRefusesPathsAndSymlinkedParents(t *testing.T) { dir, outside := t.TempDir(), t.TempDir() if err := RemoveAllBeneath(dir, "a/b"); !errors.Is(err, ErrEscapes) { t.Errorf("a/b: %v, want ErrEscapes", err) } link := filepath.Join(dir, "parent") if err := os.Symlink(outside, link); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(outside, "run"), 0o700); err != nil { t.Fatal(err) } if err := RemoveAllBeneath(link, "run"); !errors.Is(err, ErrSymlink) { t.Errorf("symlinked parent: %v, want ErrSymlink", err) } if _, err := os.Stat(filepath.Join(outside, "run")); err != nil { t.Errorf("removed through a symlinked parent: %v", err) } }