// Copyright 2024 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package os_test import ( "bytes" "errors" "flag" "fmt" "internal/testenv" "io" "io/fs" "iter" "net" "os" "path" "path/filepath" "runtime" "slices" "strings" "testing" "time" ) // testMaybeRooted calls f in two subtests, // one with a Root and one with a nil r. func testMaybeRooted(t *testing.T, f func(t *testing.T, r *os.Root)) { t.Run("NoRoot", func(t *testing.T) { t.Chdir(t.TempDir()) f(t, nil) }) t.Run("InRoot", func(t *testing.T) { t.Chdir(t.TempDir()) r, err := os.OpenRoot(".") if err != nil { t.Fatal(err) } defer r.Close() f(t, r) }) } // makefs creates a test filesystem layout and returns the path to its root. // // Each entry in the slice is a file, directory, or symbolic link to create: // // - "d/": directory d // - "f": file f with contents f // - "a => b": symlink a with target b // // The directory containing the filesystem is always named ROOT. // $ABS is replaced with the absolute path of the directory containing the filesystem. // // Parent directories are automatically created as needed. // // makefs calls t.Skip if the layout contains features not supported by the current GOOS. func makefs(t *testing.T, fs []string) string { root := filepath.Join(t.TempDir(), "ROOT") if err := os.Mkdir(root, 0o777); err != nil { t.Fatal(err) } for _, ent := range fs { ent = strings.ReplaceAll(ent, "$ABS", root) base, link, isLink := strings.Cut(ent, " => ") if isLink { if runtime.GOOS == "wasip1" && path.IsAbs(link) { t.Skip("absolute link targets not supported on " + runtime.GOOS) } if runtime.GOOS == "plan9" { t.Skip("symlinks not supported on " + runtime.GOOS) } ent = base } if err := os.MkdirAll(path.Join(root, path.Dir(base)), 0o777); err != nil { t.Fatal(err) } if isLink { if err := os.Symlink(link, path.Join(root, base)); err != nil { t.Fatal(err) } } else if strings.HasSuffix(ent, "/") { if err := os.MkdirAll(path.Join(root, ent), 0o777); err != nil { t.Fatal(err) } } else { if err := os.WriteFile(path.Join(root, ent), []byte(ent), 0o666); err != nil { t.Fatal(err) } } } return root } // A rootTest is a test case for os.Root. type rootTest struct { name string // fs is the test filesystem layout. See makefs above. fs []string // open is the filename to access in the test. open string // target is the filename that we expect to be accessed, after resolving all symlinks. // For test cases where the operation fails due to an escaping path such as ../ROOT/x, // the target is the filename that should not have been opened. target string // ltarget is the filename that we expect to accessed, after resolving all symlinks // except the last one. This is the file we expect to be removed by Remove or statted // by Lstat. // // If the last path component in open is not a symlink, ltarget should be "". ltarget string // wantError is true if accessing the file should fail. wantError bool // alwaysFails is true if the open operation is expected to fail // even when using non-openat operations. // // This lets us check that tests that are expected to fail because (for example) // a path escapes the directory root will succeed when the escaping checks are not // performed. alwaysFails bool } // run sets up the test filesystem layout, os.OpenDirs the root, and calls f. func (test *rootTest) run(t *testing.T, f func(t *testing.T, target string, d *os.Root)) { t.Run(test.name, func(t *testing.T) { root := makefs(t, test.fs) d, err := os.OpenRoot(root) if err != nil { t.Fatal(err) } defer d.Close() // The target is a file that will be accessed, // or a file that should not be accessed // (because doing so escapes the root). target := test.target if test.target != "" { target = filepath.Join(root, test.target) } f(t, target, d) }) } // errEndsTest checks the error result of a test, // verifying that it succeeded or failed as expected. // // It returns true if the test is done due to encountering an expected error. // false if the test should continue. func errEndsTest(t *testing.T, err error, wantError bool, format string, args ...any) bool { t.Helper() if wantError { if err == nil { op := fmt.Sprintf(format, args...) t.Fatalf("%v = nil; want error", op) } return true } else { if err != nil { op := fmt.Sprintf(format, args...) t.Fatalf("%v = %v; want success", op, err) } return false } } var rootTestCases = []rootTest{{ name: "plain path", fs: []string{}, open: "target", target: "target", }, { name: "path in directory", fs: []string{ "a/b/c/", }, open: "a/b/c/target", target: "a/b/c/target", }, { name: "symlink", fs: []string{ "link => target", }, open: "link", target: "target", ltarget: "link", }, { name: "symlink dotdot slash", fs: []string{ "link => ../", }, open: "link", ltarget: "link", wantError: true, }, { name: "symlink ending in slash", fs: []string{ "dir/", "link => dir/", }, open: "link/target", target: "dir/target", }, { name: "slash after symlink to file", fs: []string{ "link => ../ROOT/target", }, open: "link/", target: "target", wantError: true, }, { name: "slash after symlink to dir", fs: []string{ "link => ../ROOT/target", "target/", }, open: "link/", wantError: true, }, { name: "symlink dotdot dotdot slash", fs: []string{ "dir/link => ../../", }, open: "dir/link", ltarget: "dir/link", wantError: true, }, { name: "symlink chain", fs: []string{ "link => a/b/c/target", "a/b => e", "a/e => ../f", "f => g/h/i", "g/h/i => ..", "g/c/", }, open: "link", target: "g/c/target", ltarget: "link", }, { name: "path with dot", fs: []string{ "a/b/", }, open: "./a/./b/./target", target: "a/b/target", }, { name: "path with dotdot", fs: []string{ "a/b/", }, open: "a/../a/b/../../a/b/../b/target", target: "a/b/target", }, { name: "path with dotdot slash", fs: []string{}, open: "../", wantError: true, }, { name: "path with dotdot dotdot slash", fs: []string{ "a/", }, open: "a/../../", wantError: true, }, { name: "dotdot no symlink", fs: []string{ "a/", }, open: "a/../target", target: "target", }, { name: "dotdot after symlink", fs: []string{ "a => b/c", "b/c/", }, open: "a/../target", target: func() string { if runtime.GOOS == "windows" { // On Windows, the path is cleaned before symlink resolution. return "target" } return "b/target" }(), }, { name: "dotdot before symlink", fs: []string{ "a => b/c", "b/c/", }, open: "b/../a/target", target: "b/c/target", }, { name: "symlink ends in dot", fs: []string{ "a => b/.", "b/", }, open: "a/target", target: "b/target", }, { name: "directory does not exist", fs: []string{}, open: "a/file", wantError: true, alwaysFails: true, }, { name: "empty path", fs: []string{}, open: "", wantError: true, alwaysFails: true, }, { name: "symlink cycle", fs: []string{ "a => a", }, open: "a", ltarget: "a", wantError: true, alwaysFails: true, }, { name: "path escapes", fs: []string{}, open: "../ROOT/target", target: "target", wantError: true, }, { name: "long path escapes", fs: []string{ "a/", }, open: "a/../../ROOT/target", target: "target", wantError: true, }, { name: "absolute symlink", fs: []string{ "link => $ABS/target", }, open: "link", ltarget: "link", target: "target", wantError: true, }, { name: "relative symlink", fs: []string{ "link => ../ROOT/target", }, open: "link", target: "target", ltarget: "link", wantError: true, }, { name: "symlink chain escapes", fs: []string{ "link => a/b/c/target", "a/b => e", "a/e => ../../ROOT", "c/", }, open: "link", target: "c/target", ltarget: "link", wantError: true, }} func TestRootOpen_File(t *testing.T) { want := []byte("target") for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { if target != "" { if err := os.WriteFile(target, want, 0o666); err != nil { t.Fatal(err) } } f, err := root.Open(test.open) if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) { return } defer f.Close() got, err := io.ReadAll(f) if err != nil || !bytes.Equal(got, want) { t.Errorf(`Dir.Open(%q): read content %q, %v; want %q`, test.open, string(got), err, string(want)) } }) } } func TestRootOpen_Directory(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { if target != "" { if err := os.Mkdir(target, 0o777); err != nil { t.Fatal(err) } if err := os.WriteFile(target+"/found", nil, 0o666); err != nil { t.Fatal(err) } } f, err := root.Open(test.open) if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) { return } defer f.Close() got, err := f.Readdirnames(-1) if err != nil { t.Errorf(`Dir.Open(%q).Readdirnames: %v`, test.open, err) } if want := []string{"found"}; !slices.Equal(got, want) { t.Errorf(`Dir.Open(%q).Readdirnames: %q, want %q`, test.open, got, want) } }) } } func TestRootCreate(t *testing.T) { want := []byte("target") for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { f, err := root.Create(test.open) if errEndsTest(t, err, test.wantError, "root.Create(%q)", test.open) { return } if _, err := f.Write(want); err != nil { t.Fatal(err) } f.Close() got, err := os.ReadFile(target) if err != nil { t.Fatalf(`reading file created with root.Create(%q): %v`, test.open, err) } if !bytes.Equal(got, want) { t.Fatalf(`reading file created with root.Create(%q): got %q; want %q`, test.open, got, want) } }) } } func TestRootChmod(t *testing.T) { if runtime.GOOS == "wasip1" { t.Skip("Chmod not supported on " + runtime.GOOS) } for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { if target != "" { // Create a file with no read/write permissions, // to ensure we can use Chmod on an inaccessible file. if err := os.WriteFile(target, nil, 0o000); err != nil { t.Fatal(err) } } if runtime.GOOS == "windows" { // On Windows, Chmod("symlink") affects the link, not its target. // See issue 71492. fi, err := root.Lstat(test.open) if err == nil && !fi.Mode().IsRegular() { t.Skip("https://go.dev/issue/71492") } } want := os.FileMode(0o666) err := root.Chmod(test.open, want) if errEndsTest(t, err, test.wantError, "root.Chmod(%q)", test.open) { return } st, err := os.Stat(target) if err != nil { t.Fatalf("os.Stat(%q) = %v", target, err) } if got := st.Mode(); got != want { t.Errorf("after root.Chmod(%q, %v): file mode = %v, want %v", test.open, want, got, want) } }) } } func TestRootChtimes(t *testing.T) { // Don't check atimes if the fs is mounted noatime, // or on Plan 9 which does not permit changing atimes to arbitrary values. checkAtimes := !hasNoatime() && runtime.GOOS != "plan9" for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { if target != "" { if err := os.WriteFile(target, nil, 0o666); err != nil { t.Fatal(err) } } for _, times := range []struct { atime, mtime time.Time }{{ atime: time.Now().Add(-1 * time.Minute), mtime: time.Now().Add(-1 * time.Minute), }, { atime: time.Now().Add(1 * time.Minute), mtime: time.Now().Add(1 * time.Minute), }, { atime: time.Time{}, mtime: time.Now(), }, { atime: time.Now(), mtime: time.Time{}, }} { switch runtime.GOOS { case "js", "plan9": times.atime = times.atime.Truncate(1 * time.Second) times.mtime = times.mtime.Truncate(1 * time.Second) case "illumos": times.atime = times.atime.Truncate(1 * time.Microsecond) times.mtime = times.mtime.Truncate(1 * time.Microsecond) } err := root.Chtimes(test.open, times.atime, times.mtime) if errEndsTest(t, err, test.wantError, "root.Chtimes(%q)", test.open) { return } st, err := os.Stat(target) if err != nil { t.Fatalf("os.Stat(%q) = %v", target, err) } if got := st.ModTime(); !times.mtime.IsZero() && !got.Equal(times.mtime) { t.Errorf("after root.Chtimes(%q, %v, %v): got mtime=%v, want %v", test.open, times.atime, times.mtime, got, times.mtime) } if checkAtimes { if got := os.Atime(st); !times.atime.IsZero() && !got.Equal(times.atime) { t.Errorf("after root.Chtimes(%q, %v, %v): got atime=%v, want %v", test.open, times.atime, times.mtime, got, times.atime) } } } }) } } func TestRootMkdir(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { wantError := test.wantError if test.ltarget != "" { // This case is trying to mkdir("some symlink"), // which is an error (but not an escape). wantError = true } err := root.Mkdir(test.open, 0o777) if errEndsTest(t, err, wantError, "root.Create(%q)", test.open) { return } fi, err := os.Lstat(target) if err != nil { t.Fatalf(`stat file created with Root.Mkdir(%q): %v`, test.open, err) } if !fi.IsDir() { t.Fatalf(`stat file created with Root.Mkdir(%q): not a directory`, test.open) } if mode := fi.Mode(); mode&0o777 == 0 { // Issue #73559: We're not going to worry about the exact // mode bits (which will have been modified by umask), // but there should be mode bits. t.Fatalf(`stat file created with Root.Mkdir(%q): mode=%v, want non-zero`, test.open, mode) } }) } } func TestRootMkdirAll(t *testing.T) { for _, test := range rootTestCases { if test.name == "directory does not exist" { // Test expects error, mkdirall creates the missing directory. // TestRootMultiMkdirAll covers this case better anyway, just skip. continue } test.run(t, func(t *testing.T, target string, root *os.Root) { wantError := test.wantError if test.ltarget != "" { // This case is trying to mkdir("some symlink"), // which is an error (but not an escape). wantError = true } err := root.MkdirAll(test.open, 0o777) if errEndsTest(t, err, wantError, "root.MkdirAll(%q)", test.open) { return } fi, err := os.Lstat(target) if err != nil { t.Fatalf(`stat file created with Root.MkdirAll(%q): %v`, test.open, err) } if !fi.IsDir() { t.Fatalf(`stat file created with Root.MkdirAll(%q): not a directory`, test.open) } if mode := fi.Mode(); mode&0o777 == 0 { // Issue #73559: We're not going to worry about the exact // mode bits (which will have been modified by umask), // but there should be mode bits. t.Fatalf(`stat file created with Root.MkdirAll(%q): mode=%v, want non-zero`, test.open, mode) } }) } } func TestRootOpenRoot(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { if target != "" { if err := os.Mkdir(target, 0o777); err != nil { t.Fatal(err) } if err := os.WriteFile(target+"/f", nil, 0o666); err != nil { t.Fatal(err) } } rr, err := root.OpenRoot(test.open) if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) { return } defer rr.Close() f, err := rr.Open("f") if err != nil { t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err) } f.Close() }) } } func TestRootRemoveFile(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { wantError := test.wantError if test.ltarget != "" { // Remove doesn't follow symlinks in the final path component, // so it will successfully remove ltarget. wantError = false target = filepath.Join(root.Name(), test.ltarget) } else if target != "" { if err := os.WriteFile(target, nil, 0o666); err != nil { t.Fatal(err) } } err := root.Remove(test.open) if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) { return } _, err = os.Lstat(target) if !errors.Is(err, os.ErrNotExist) { t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err) } }) } } func TestRootRemoveDirectory(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { wantError := test.wantError if test.ltarget != "" { // Remove doesn't follow symlinks in the final path component, // so it will successfully remove ltarget. wantError = false target = filepath.Join(root.Name(), test.ltarget) } else if target != "" { if err := os.Mkdir(target, 0o777); err != nil { t.Fatal(err) } } err := root.Remove(test.open) if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) { return } _, err = os.Lstat(target) if !errors.Is(err, os.ErrNotExist) { t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err) } }) } } func TestRootRemoveAll(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { if strings.HasSuffix(test.open, "/") { // The test is removing a file with a trailing /. // RemoveAll ignores trailing /s // If the file is a symlink, it will remove the symlink. fullname := filepath.Join(root.Name(), test.open) if st, err := os.Lstat(fullname); err == nil && st.Mode().Type() == fs.ModeSymlink { test.ltarget = test.open } } wantError := test.wantError if test.ltarget != "" { // Remove doesn't follow symlinks in the final path component, // so it will successfully remove ltarget. wantError = false target = filepath.Join(root.Name(), test.ltarget) } else if target != "" { if err := os.Mkdir(target, 0o777); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(target, "file"), nil, 0o666); err != nil { t.Fatal(err) } } targetExists := true if _, err := root.Lstat(test.open); errors.Is(err, os.ErrNotExist) { // If the target doesn't exist, RemoveAll succeeds rather // than returning ErrNotExist. targetExists = false wantError = false } err := root.RemoveAll(test.open) if errEndsTest(t, err, wantError, "root.RemoveAll(%q)", test.open) { return } if !targetExists { return } _, err = os.Lstat(target) if !errors.Is(err, os.ErrNotExist) { t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err) } }) } } func TestRootOpenFileAsRoot(t *testing.T) { dir := t.TempDir() target := filepath.Join(dir, "target") if err := os.WriteFile(target, nil, 0o666); err != nil { t.Fatal(err) } r, err := os.OpenRoot(target) if err == nil { r.Close() t.Fatal("os.OpenRoot(file) succeeded; want failure") } r, err = os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer r.Close() rr, err := r.OpenRoot("target") if err == nil { rr.Close() t.Fatal("Root.OpenRoot(file) succeeded; want failure") } } func TestRootStat(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { const content = "content" if target != "" { if err := os.WriteFile(target, []byte(content), 0o666); err != nil { t.Fatal(err) } } fi, err := root.Stat(test.open) if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) { return } if got, want := fi.Name(), filepath.Base(test.open); got != want { t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want) } if got, want := fi.Size(), int64(len(content)); got != want { t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want) } }) } } func TestRootLstat(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { const content = "content" wantError := test.wantError if test.ltarget != "" { // Lstat will stat the final link, rather than following it. wantError = false } else if target != "" { if err := os.WriteFile(target, []byte(content), 0o666); err != nil { t.Fatal(err) } } fi, err := root.Lstat(test.open) if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) { return } if got, want := fi.Name(), filepath.Base(test.open); got != want { t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want) } if test.ltarget == "" { if got := fi.Mode(); got&os.ModeSymlink != 0 { t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got) } if got, want := fi.Size(), int64(len(content)); got != want { t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want) } } else { if got := fi.Mode(); got&os.ModeSymlink == 0 { t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got) } } }) } } func TestRootReadlink(t *testing.T) { for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { const content = "content" wantError := test.wantError if test.ltarget != "" { // Readlink will read the final link, rather than following it. wantError = false } else { // Readlink fails on non-link targets. wantError = true } got, err := root.Readlink(test.open) if errEndsTest(t, err, wantError, "root.Readlink(%q)", test.open) { return } want, err := os.Readlink(filepath.Join(root.Name(), test.ltarget)) if err != nil { t.Fatalf("os.Readlink(%q) = %v, want success", test.ltarget, err) } if got != want { t.Errorf("root.Readlink(%q) = %q, want %q", test.open, got, want) } }) } } // TestRootRenameFrom tests renaming the test case target to a known-good path. func TestRootRenameFrom(t *testing.T) { testRootMoveFrom(t, true) } // TestRootRenameFrom tests linking the test case target to a known-good path. func TestRootLinkFrom(t *testing.T) { testenv.MustHaveLink(t) testRootMoveFrom(t, false) } func testRootMoveFrom(t *testing.T, rename bool) { want := []byte("target") for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { if target != "" { if err := os.WriteFile(target, want, 0o666); err != nil { t.Fatal(err) } } wantError := test.wantError var linkTarget string if test.ltarget != "" { // Rename will rename the link, not the file linked to. wantError = false var err error linkTarget, err = root.Readlink(test.ltarget) if err != nil { t.Fatalf("root.Readlink(%q) = %v, want success", test.ltarget, err) } // When GOOS=js, creating a hard link to a symlink fails. if !rename && runtime.GOOS == "js" { wantError = true } // Windows allows creating a hard link to a file symlink, // but not to a directory symlink. // // This uses os.Stat to check the link target, because this // is easier than figuring out whether the link itself is a // directory link. The link was created with os.Symlink, // which creates directory links when the target is a directory, // so this is good enough for a test. if !rename && runtime.GOOS == "windows" { st, err := os.Stat(filepath.Join(root.Name(), test.ltarget)) if err == nil && st.IsDir() { wantError = true } } } const dstPath = "destination" // Plan 9 doesn't allow cross-directory renames. if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") { wantError = true } var op string var err error if rename { op = "Rename" err = root.Rename(test.open, dstPath) } else { op = "Link" err = root.Link(test.open, dstPath) } if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, test.open, dstPath) { return } origPath := target if test.ltarget != "" { origPath = filepath.Join(root.Name(), test.ltarget) } _, err = os.Lstat(origPath) if rename { if !errors.Is(err, os.ErrNotExist) { t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", origPath, err) } } else { if err != nil { t.Errorf("after linking file, error accessing original: %v", err) } } dstFullPath := filepath.Join(root.Name(), dstPath) if test.ltarget != "" { got, err := os.Readlink(dstFullPath) if err != nil || got != linkTarget { t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstFullPath, got, err, linkTarget) } } else { got, err := os.ReadFile(dstFullPath) if err != nil || !bytes.Equal(got, want) { t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstFullPath, string(got), err, string(want)) } st, err := os.Lstat(dstFullPath) if err != nil || st.Mode()&fs.ModeSymlink != 0 { t.Errorf(`os.Lstat(%q) = %v, %v; want non-symlink`, dstFullPath, st.Mode(), err) } } }) } } // TestRootRenameTo tests renaming a known-good path to the test case target. func TestRootRenameTo(t *testing.T) { testRootMoveTo(t, true) } // TestRootLinkTo tests renaming a known-good path to the test case target. func TestRootLinkTo(t *testing.T) { testenv.MustHaveLink(t) testRootMoveTo(t, true) } func testRootMoveTo(t *testing.T, rename bool) { want := []byte("target") for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { const srcPath = "source" if err := os.WriteFile(filepath.Join(root.Name(), srcPath), want, 0o666); err != nil { t.Fatal(err) } if runtime.GOOS == "windows" && strings.HasSuffix(test.open, "/") { // Windows will ignore trailing slashes in the rename/link target. p := strings.TrimSuffix(test.open, "/") st, err := root.Lstat(p) if err == nil && st.Mode().Type() == fs.ModeSymlink { test.ltarget = p } } target = test.target wantError := test.wantError if test.ltarget != "" { // Rename will overwrite the final link rather than follow it. target = test.ltarget wantError = false } // Plan 9 doesn't allow cross-directory renames. if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") { wantError = true } var err error var op string if rename { op = "Rename" err = root.Rename(srcPath, test.open) } else { op = "Link" err = root.Link(srcPath, test.open) } if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, srcPath, test.open) { return } _, err = os.Lstat(filepath.Join(root.Name(), srcPath)) if rename { if !errors.Is(err, os.ErrNotExist) { t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", srcPath, err) } } else { if err != nil { t.Errorf("after linking file, error accessing original: %v", err) } } got, err := os.ReadFile(filepath.Join(root.Name(), target)) if err != nil || !bytes.Equal(got, want) { t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, target, string(got), err, string(want)) } }) } } func TestRootSymlink(t *testing.T) { testenv.MustHaveSymlink(t) for _, test := range rootTestCases { test.run(t, func(t *testing.T, target string, root *os.Root) { wantError := test.wantError if test.ltarget != "" { // We can't create a symlink over an existing symlink. wantError = true } const wantTarget = "linktarget" err := root.Symlink(wantTarget, test.open) if errEndsTest(t, err, wantError, "root.Symlink(%q)", test.open) { return } got, err := os.Readlink(target) if err != nil || got != wantTarget { t.Fatalf("ReadLink(%q) = %q, %v; want %q, nil", target, got, err, wantTarget) } }) } } // A rootConsistencyTest is a test case comparing os.Root behavior with // the corresponding non-Root function. // // These tests verify that, for example, Root.Open("file/./") and os.Open("file/./") // have the same result, although the specific result may vary by platform. type rootConsistencyTest struct { name string // fs is the test filesystem layout. See makefs above. // fsFunc is called to modify the test filesystem, or replace it. fs []string fsFunc func(t *testing.T, dir string) string // open is the filename to access in the test. open string // detailedErrorMismatch indicates that os.Root and the corresponding non-Root // function return different errors for this test. detailedErrorMismatch func(t *testing.T) bool // check is called before the test starts, and may t.Skip if necessary. check func(t *testing.T) } var rootConsistencyTestCases = []rootConsistencyTest{{ name: "file", fs: []string{ "target", }, open: "target", }, { name: "dir slash dot", fs: []string{ "target/file", }, open: "target/.", }, { name: "dot", fs: []string{ "file", }, open: ".", }, { name: "file slash dot", fs: []string{ "target", }, open: "target/.", detailedErrorMismatch: func(t *testing.T) bool { // FreeBSD returns EPERM in the non-Root case. return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove") }, }, { name: "dir slash", fs: []string{ "target/file", }, open: "target/", }, { name: "dot slash", fs: []string{ "file", }, open: "./", }, { name: "file slash", fs: []string{ "target", }, open: "target/", detailedErrorMismatch: func(t *testing.T) bool { // os.Create returns ENOTDIR or EISDIR depending on the platform. return runtime.GOOS == "js" }, }, { name: "file in path", fs: []string{ "file", }, open: "file/target", }, { name: "directory in path missing", open: "dir/target", }, { name: "target does not exist", open: "target", }, { name: "symlink slash", fs: []string{ "target/file", "link => target", }, open: "link/", check: func(t *testing.T) { if runtime.GOOS == "linux" && strings.HasPrefix(t.Name(), "TestRootConsistencyRename/") { // Linux does not resolve "symlink" in rename("symlink/", "target"). t.Skip("known inconsistency on linux") } if strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") { // Root.RemoveAll and os.RemoveAll are not always consistent here. t.Skip("known inconsistency in RemoveAll") } }, }, { name: "symlink slash dot", fs: []string{ "target/file", "link => target", }, open: "link/.", }, { name: "unresolved symlink", fs: []string{ "link => target", }, open: "link", }, { name: "resolved symlink", fs: []string{ "link => target", "target", }, open: "link", }, { name: "dotdot in path after symlink", fs: []string{ "a => b/c", "b/c/", "b/target", }, open: "a/../target", }, { name: "symlink to dir ends in slash", fs: []string{ "dir/", "link => dir/", }, open: "link", }, { name: "symlink to file ends in slash", fs: []string{ "file", "link => file/", }, open: "link", }, { name: "long file name", open: strings.Repeat("a", 500), }, { name: "unreadable directory", fs: []string{ "dir/target", }, fsFunc: func(t *testing.T, dir string) string { os.Chmod(filepath.Join(dir, "dir"), 0) t.Cleanup(func() { os.Chmod(filepath.Join(dir, "dir"), 0o700) }) return dir }, open: "dir/target", }, { name: "unix domain socket target", fsFunc: func(t *testing.T, dir string) string { return tempDirWithUnixSocket(t, "a") }, open: "a", }, { name: "unix domain socket in path", fsFunc: func(t *testing.T, dir string) string { return tempDirWithUnixSocket(t, "a") }, open: "a/b", detailedErrorMismatch: func(t *testing.T) bool { // On Windows, os.Root.Open returns "The directory name is invalid." // and os.Open returns "The file cannot be accessed by the system.". return runtime.GOOS == "windows" }, check: func(t *testing.T) { if strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") { switch runtime.GOOS { case "windows": // Root.RemoveAll notices that a/ is not a directory, // and returns success. // os.RemoveAll tries to open a/ and fails because // it is not a regular file. // The inconsistency here isn't worth fixing, so just skip this test. t.Skip("known inconsistency on windows") case "js": // GOOS=js behavior varies with what the underlying OS is. t.Skip("known inconsistency with GOOS=js") } } }, }, { name: "question mark", open: "?", }, { name: "nul byte", open: "\x00", }} func tempDirWithUnixSocket(t *testing.T, name string) string { dir, err := os.MkdirTemp("", "") if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := os.RemoveAll(dir); err != nil { t.Error(err) } }) addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name)) if err != nil { t.Skipf("net.ResolveUnixAddr: %v", err) } conn, err := net.ListenUnix("unix", addr) if err != nil { t.Skipf("net.ListenUnix: %v", err) } t.Cleanup(func() { conn.Close() }) return dir } func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) { if runtime.GOOS == "wasip1" { // On wasip, non-Root functions clean paths before opening them, // resulting in inconsistent behavior. // https://go.dev/issue/69509 t.Skip("#69509: inconsistent results on wasip1") } t.Run(test.name, func(t *testing.T) { if test.check != nil { test.check(t) } dir1 := makefs(t, test.fs) dir2 := makefs(t, test.fs) if test.fsFunc != nil { dir1 = test.fsFunc(t, dir1) dir2 = test.fsFunc(t, dir2) } r, err := os.OpenRoot(dir1) if err != nil { t.Fatal(err) } defer r.Close() res1, err1 := f(t, test.open, r) res2, err2 := f(t, dir2+"/"+test.open, nil) if res1 != res2 || ((err1 == nil) != (err2 == nil)) { t.Errorf("with root: res=%v", res1) t.Errorf(" err=%v", err1) t.Errorf("without root: res=%v", res2) t.Errorf(" err=%v", err2) t.Errorf("want consistent results, got mismatch") } if err1 != nil || err2 != nil { underlyingError := func(how string, err error) error { switch e := err1.(type) { case *os.PathError: return e.Err case *os.LinkError: return e.Err default: t.Fatalf("%v, expected PathError or LinkError; got: %v", how, err) } return nil } e1 := underlyingError("with root", err1) e2 := underlyingError("without root", err1) detailedErrorMismatch := false if f := test.detailedErrorMismatch; f != nil { detailedErrorMismatch = f(t) } if runtime.GOOS == "plan9" { // Plan9 syscall errors aren't comparable. detailedErrorMismatch = true } if !detailedErrorMismatch && e1 != e2 { t.Errorf("with root: err=%v", e1) t.Errorf("without root: err=%v", e2) t.Errorf("want consistent results, got mismatch") } } }) } func TestRootConsistencyOpen(t *testing.T) { for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var f *os.File var err error if r == nil { f, err = os.Open(path) } else { f, err = r.Open(path) } if err != nil { return "", err } defer f.Close() fi, err := f.Stat() if err == nil && !fi.IsDir() { b, err := io.ReadAll(f) return string(b), err } else { names, err := f.Readdirnames(-1) slices.Sort(names) return fmt.Sprintf("%q", names), err } }) } } func TestRootConsistencyCreate(t *testing.T) { for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var f *os.File var err error if r == nil { f, err = os.Create(path) } else { f, err = r.Create(path) } if err == nil { f.Write([]byte("file contents")) f.Close() } return "", err }) } } func TestRootConsistencyChmod(t *testing.T) { if runtime.GOOS == "wasip1" { t.Skip("Chmod not supported on " + runtime.GOOS) } for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { chmod := os.Chmod lstat := os.Lstat if r != nil { chmod = r.Chmod lstat = r.Lstat } var m1, m2 os.FileMode if err := chmod(path, 0o555); err != nil { return "chmod 0o555", err } fi, err := lstat(path) if err == nil { m1 = fi.Mode() } if err = chmod(path, 0o777); err != nil { return "chmod 0o777", err } fi, err = lstat(path) if err == nil { m2 = fi.Mode() } return fmt.Sprintf("%v %v", m1, m2), err }) } } func TestRootConsistencyMkdir(t *testing.T) { for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var err error if r == nil { err = os.Mkdir(path, 0o777) } else { err = r.Mkdir(path, 0o777) } return "", err }) } } func TestRootConsistencyMkdirAll(t *testing.T) { for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var err error if r == nil { err = os.MkdirAll(path, 0o777) } else { err = r.MkdirAll(path, 0o777) } return "", err }) } } func TestRootConsistencyRemove(t *testing.T) { for _, test := range rootConsistencyTestCases { if test.open == "." || test.open == "./" { continue // can't remove the root itself } test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var err error if r == nil { err = os.Remove(path) } else { err = r.Remove(path) } return "", err }) } } func TestRootConsistencyRemoveAll(t *testing.T) { for _, test := range rootConsistencyTestCases { if test.open == "." || test.open == "./" { continue // can't remove the root itself } test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var err error if r == nil { err = os.RemoveAll(path) } else { err = r.RemoveAll(path) } return "", err }) } } func TestRootConsistencyStat(t *testing.T) { for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var fi os.FileInfo var err error if r == nil { fi, err = os.Stat(path) } else { fi, err = r.Stat(path) } if err != nil { return "", err } return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil }) } } func TestRootConsistencyLstat(t *testing.T) { for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var fi os.FileInfo var err error if r == nil { fi, err = os.Lstat(path) } else { fi, err = r.Lstat(path) } if err != nil { return "", err } return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil }) } } func TestRootConsistencyReadlink(t *testing.T) { for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { if r == nil { return os.Readlink(path) } else { return r.Readlink(path) } }) } } func TestRootConsistencyRename(t *testing.T) { testRootConsistencyMove(t, true) } func TestRootConsistencyLink(t *testing.T) { testenv.MustHaveLink(t) testRootConsistencyMove(t, false) } func testRootConsistencyMove(t *testing.T, rename bool) { if runtime.GOOS == "plan9" { // This test depends on moving files between directories. t.Skip("Plan 9 does not support cross-directory renames") } // Run this test in two directions: // Renaming the test path to a known-good path (from), // and renaming a known-good path to the test path (to). for _, name := range []string{"from", "to"} { t.Run(name, func(t *testing.T) { for _, test := range rootConsistencyTestCases { if runtime.GOOS == "windows" { // On Windows, Rename("/path/to/.", x) succeeds, // because Windows cleans the path to just "/path/to". // Root.Rename(".", x) fails as expected. // Don't run this consistency test on Windows. if test.open == "." || test.open == "./" { continue } } test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { var move func(oldname, newname string) error switch { case rename && r == nil: move = os.Rename case rename && r != nil: move = r.Rename case !rename && r == nil: move = os.Link case !rename && r != nil: move = r.Link } lstat := os.Lstat if r != nil { lstat = r.Lstat } otherPath := "other" if r == nil { otherPath = filepath.Join(t.TempDir(), otherPath) } var srcPath, dstPath string if name == "from" { srcPath = path dstPath = otherPath } else { srcPath = otherPath dstPath = path } if !rename { // When the source is a symlink, Root.Link creates // a hard link to the symlink. // os.Link does whatever the link syscall does, // which varies between operating systems and // their versions. // Skip running the consistency test when // the source is a symlink. fi, err := lstat(srcPath) if err == nil && fi.Mode()&os.ModeSymlink != 0 { return "", nil } } if err := move(srcPath, dstPath); err != nil { return "", err } fi, err := lstat(dstPath) if err != nil { t.Errorf("stat(%q) after successful copy: %v", dstPath, err) return "stat error", err } return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil }) } }) } } func TestRootConsistencySymlink(t *testing.T) { testenv.MustHaveSymlink(t) for _, test := range rootConsistencyTestCases { test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) { const target = "linktarget" var err error var got string if r == nil { err = os.Symlink(target, path) got, _ = os.Readlink(target) } else { err = r.Symlink(target, path) got, _ = r.Readlink(target) } return got, err }) } } func TestRootRenameAfterOpen(t *testing.T) { switch runtime.GOOS { case "windows": t.Skip("renaming open files not supported on " + runtime.GOOS) case "js", "plan9": t.Skip("openat not supported on " + runtime.GOOS) case "wasip1": if os.Getenv("GOWASIRUNTIME") == "wazero" { t.Skip("wazero does not track renamed directories") } } dir := t.TempDir() // Create directory "a" and open it. if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil { t.Fatal(err) } dirf, err := os.OpenRoot(filepath.Join(dir, "a")) if err != nil { t.Fatal(err) } defer dirf.Close() // Rename "a" => "b", and create "b/f". if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil { t.Fatal(err) } // Open "f", and confirm that we see it. f, err := dirf.OpenFile("f", os.O_RDONLY, 0) if err != nil { t.Fatalf("reading file after renaming parent: %v", err) } defer f.Close() b, err := io.ReadAll(f) if err != nil { t.Fatal(err) } if got, want := string(b), "hello"; got != want { t.Fatalf("file contents: %q, want %q", got, want) } // f.Name reflects the original path we opened the directory under (".../a"), not "b". if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want { t.Errorf("f.Name() = %q, want %q", got, want) } } func TestRootNonPermissionMode(t *testing.T) { r, err := os.OpenRoot(t.TempDir()) if err != nil { t.Fatal(err) } defer r.Close() if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil { t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error") } if err := r.Mkdir("file", 0o1777); err == nil { t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error") } } func TestRootUseAfterClose(t *testing.T) { r, err := os.OpenRoot(t.TempDir()) if err != nil { t.Fatal(err) } r.Close() for _, test := range []struct { name string f func(r *os.Root, filename string) error }{{ name: "Open", f: func(r *os.Root, filename string) error { _, err := r.Open(filename) return err }, }, { name: "Create", f: func(r *os.Root, filename string) error { _, err := r.Create(filename) return err }, }, { name: "OpenFile", f: func(r *os.Root, filename string) error { _, err := r.OpenFile(filename, os.O_RDWR, 0o666) return err }, }, { name: "OpenRoot", f: func(r *os.Root, filename string) error { _, err := r.OpenRoot(filename) return err }, }, { name: "Mkdir", f: func(r *os.Root, filename string) error { return r.Mkdir(filename, 0o777) }, }} { err := test.f(r, "target") pe, ok := err.(*os.PathError) if !ok || pe.Path != "target" || pe.Err != os.ErrClosed { t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err) } } } func TestRootConcurrentClose(t *testing.T) { r, err := os.OpenRoot(t.TempDir()) if err != nil { t.Fatal(err) } ch := make(chan error, 1) go func() { defer close(ch) first := true for { f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666) if err != nil { ch <- err return } if first { ch <- nil first = false } f.Close() if runtime.GOARCH == "wasm" { // TODO(go.dev/issue/71134) can lead to goroutine starvation. runtime.Gosched() } } }() if err := <-ch; err != nil { t.Errorf("OpenFile: %v, want success", err) } r.Close() if err := <-ch; !errors.Is(err, os.ErrClosed) { t.Errorf("OpenFile: %v, want ErrClosed", err) } } // TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse. // // We create a deeply nested directory: // // base/a/a/a/a/ [...] /a // // And a path that descends into the tree, then returns to the top using ..: // // base/a/a/a/a/ [...] /a/../../../ [..] /../a/f // // While opening this file, we rename base/a/a to base/b. // A naive lookup operation will resolve the path to base/f. func TestRootRaceRenameDir(t *testing.T) { dir := t.TempDir() r, err := os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer r.Close() const depth = 4 os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777) path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f" os.WriteFile(dir+"/f", []byte("secret"), 0o666) os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666) // Compute how long it takes to open the path in the common case. const tries = 10 var total time.Duration for range tries { start := time.Now() f, err := r.Open(path) if err != nil { t.Fatal(err) } b, err := io.ReadAll(f) if err != nil { t.Fatal(err) } if string(b) != "public" { t.Fatalf("read %q, want %q", b, "public") } f.Close() total += time.Since(start) } avg := total / tries // We're trying to exploit a race, so try this a number of times. for range 100 { // Start a goroutine to open the file. gotc := make(chan []byte) go func() { f, err := r.Open(path) if err != nil { gotc <- nil } defer f.Close() b, _ := io.ReadAll(f) gotc <- b }() // Wait for the open operation to partially complete, // and then rename a directory near the root. time.Sleep(avg / 4) if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil { // Windows and Plan9 won't let us rename a directory if we have // an open handle for it, so an error here is expected. switch runtime.GOOS { case "windows", "plan9": default: t.Fatal(err) } } got := <-gotc os.Rename(dir+"/b", dir+"/base/a") if len(got) > 0 && string(got) != "public" { t.Errorf("read file: %q; want error or 'public'", got) } } } func TestRootSymlinkToRoot(t *testing.T) { dir := makefs(t, []string{ "d/d => ..", }) root, err := os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer root.Close() if err := root.Mkdir("d/d/new", 0777); err != nil { t.Fatal(err) } f, err := root.Open("d/d") if err != nil { t.Fatal(err) } defer f.Close() names, err := f.Readdirnames(-1) if err != nil { t.Fatal(err) } slices.Sort(names) if got, want := names, []string{"d", "new"}; !slices.Equal(got, want) { t.Errorf("root contains: %q, want %q", got, want) } } func TestOpenInRoot(t *testing.T) { dir := makefs(t, []string{ "file", "link => ../ROOT/file", }) f, err := os.OpenInRoot(dir, "file") if err != nil { t.Fatalf("OpenInRoot(`file`) = %v, want success", err) } f.Close() for _, name := range []string{ "link", "../ROOT/file", dir + "/file", } { f, err := os.OpenInRoot(dir, name) if err == nil { f.Close() t.Fatalf("OpenInRoot(%q) = nil, want error", name) } } } func TestRootRemoveDot(t *testing.T) { dir := t.TempDir() root, err := os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer root.Close() if err := root.Remove("."); err == nil { t.Errorf(`root.Remove(".") = %v, want error`, err) } if err := root.RemoveAll("."); err == nil { t.Errorf(`root.RemoveAll(".") = %v, want error`, err) } if _, err := os.Stat(dir); err != nil { t.Error(`root.Remove(All)?(".") removed the root`) } } func TestRootWriteReadFile(t *testing.T) { dir := t.TempDir() root, err := os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer root.Close() name := "filename" want := []byte("file contents") if err := root.WriteFile(name, want, 0o666); err != nil { t.Fatalf("root.WriteFile(%q, %q, 0o666) = %v; want nil", name, want, err) } got, err := root.ReadFile(name) if err != nil { t.Fatalf("root.ReadFile(%q) = %q, %v; want %q, nil", name, got, err, want) } } func TestRootName(t *testing.T) { dir := t.TempDir() root, err := os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer root.Close() if got, want := root.Name(), dir; got != want { t.Errorf("root.Name() = %q, want %q", got, want) } f, err := root.Create("file") if err != nil { t.Fatal(err) } defer f.Close() if got, want := f.Name(), filepath.Join(dir, "file"); got != want { t.Errorf(`root.Create("file").Name() = %q, want %q`, got, want) } if err := root.Mkdir("dir", 0o777); err != nil { t.Fatal(err) } subroot, err := root.OpenRoot("dir") if err != nil { t.Fatal(err) } defer subroot.Close() if got, want := subroot.Name(), filepath.Join(dir, "dir"); got != want { t.Errorf(`root.OpenRoot("dir").Name() = %q, want %q`, got, want) } } // TestRootNoLstat verifies that we do not use lstat (possibly escaping the root) // when reading directories in a Root. func TestRootNoLstat(t *testing.T) { if runtime.GOARCH == "wasm" { t.Skip("wasm lacks fstatat") } dir := makefs(t, []string{ "subdir/", }) const size = 42 contents := strings.Repeat("x", size) if err := os.WriteFile(dir+"/subdir/file", []byte(contents), 0666); err != nil { t.Fatal(err) } root, err := os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer root.Close() test := func(name string, fn func(t *testing.T, f *os.File)) { t.Run(name, func(t *testing.T) { os.SetStatHook(t, func(f *os.File, name string) (os.FileInfo, error) { if f == nil { t.Errorf("unexpected Lstat(%q)", name) } return nil, nil }) f, err := root.Open("subdir") if err != nil { t.Fatal(err) } defer f.Close() fn(t, f) }) } checkFileInfo := func(t *testing.T, fi fs.FileInfo) { t.Helper() if got, want := fi.Name(), "file"; got != want { t.Errorf("FileInfo.Name() = %q, want %q", got, want) } if got, want := fi.Size(), int64(size); got != want { t.Errorf("FileInfo.Size() = %v, want %v", got, want) } } checkDirEntry := func(t *testing.T, d fs.DirEntry) { t.Helper() if got, want := d.Name(), "file"; got != want { t.Errorf("DirEntry.Name() = %q, want %q", got, want) } if got, want := d.IsDir(), false; got != want { t.Errorf("DirEntry.IsDir() = %v, want %v", got, want) } fi, err := d.Info() if err != nil { t.Fatalf("DirEntry.Info() = _, %v", err) } checkFileInfo(t, fi) } test("Stat", func(t *testing.T, subdir *os.File) { fi, err := subdir.Stat() if err != nil { t.Fatal(err) } if !fi.IsDir() { t.Fatalf(`Open("subdir").Stat().IsDir() = false, want true`) } }) // File.ReadDir, returning []DirEntry test("ReadDirEntry", func(t *testing.T, subdir *os.File) { dirents, err := subdir.ReadDir(-1) if err != nil { t.Fatal(err) } if len(dirents) != 1 { t.Fatalf(`Open("subdir").ReadDir(-1) = {%v}, want {file}`, dirents) } checkDirEntry(t, dirents[0]) }) // File.Readdir, returning []FileInfo test("ReadFileInfo", func(t *testing.T, subdir *os.File) { fileinfos, err := subdir.Readdir(-1) if err != nil { t.Fatal(err) } if len(fileinfos) != 1 { t.Fatalf(`Open("subdir").Readdir(-1) = {%v}, want {file}`, fileinfos) } checkFileInfo(t, fileinfos[0]) }) // File.Readdirnames, returning []string test("Readdirnames", func(t *testing.T, subdir *os.File) { names, err := subdir.Readdirnames(-1) if err != nil { t.Fatal(err) } if got, want := names, []string{"file"}; !slices.Equal(got, want) { t.Fatalf(`Open("subdir").Readdirnames(-1) = %q, want %q`, got, want) } }) } // A rootMultiTest is state for testing an os.Root operation in one configuration among many. // Each execution of a rootMultiTest varies in several ways: // // - With or without an *os.Root, to check consistency between root/non-root operations. // - With a target that may be a file, directory, symlink, or entirely absent. // - With various paths referencing the target: "target", "DIR/../target", etc. // - When the target is a symlink, with various link target paths. // // For example, a single test execution might be: // In an *os.Root, copy "source" to "DIR/../target". // "source" is a file, and "target" is a symlink to "../ROOT/s_target". "s_target" is a directory. // (In this case, we expect the test to fail due to the path escape in the symlink.) type rootMultiTest struct { // dir is the directory containing the test. // dir will always contain a directory named "ROOT" // and a subdir named "ROOT/DIR". dir string // root is the *Root for the test. May be nil. root *os.Root // source and target are files acted on by the test. // target is always set; source is only set for tests which request two files. source testFileDesc target testFileDesc // sourcePath and targetPath are the paths which should be used to acceess // the source/target. sourcePath string targetPath string sourceInfo os.FileInfo targetInfo os.FileInfo // op is the operation being performed, used for reporting errors. op string } var testVerbose = flag.Bool("verbose", false, "verbose") // A rootMultiTest function may return this error to disable // the check that in-root and out-of-root functions have the same outcome. var errSkipRootConsistencyCheck = errors.New("skip root consistency check") // runRootMultiTest runs f in a variety of configurations. // See above. func runRootMultiTest(t *testing.T, f func(*testing.T, *rootMultiTest) (string, error)) { for target := range allTestFileDescs() { t.Run(target.String(), func(t *testing.T) { var source testFileDesc // unused runRootMultiTestDescs(t, source, target, f) }) } } // runRootMultiTest2 runs f in a variety of configurations, // with both source and target files. // See above. func runRootMultiTest2(t *testing.T, f func(*testing.T, *rootMultiTest) (string, error)) { // A "simple" desc is one which contains only direct references. // When not running the comprehensive (but slow) set of test variations, // we only test variations where at least one of source and target is simple. isSimple := func(desc testFileDesc) bool { if desc.ref.template != "BASE" { return false } if desc.kind == testFileSymlink && desc.target.ref.template != "BASE" { return false } return true } for source := range allTestFileDescs() { for target := range allTestFileDescs() { if !*rootComprehensive && !isSimple(source) && !isSimple(target) { continue } name := fmt.Sprintf("%s_to_%s", source, target) t.Run(name, func(t *testing.T) { runRootMultiTestDescs(t, source, target, f) }) } } } // setOp sets the operation performed by the test (logged in errors). // // This currently assumes the operation will be a method of os.Root and a function in os // (e.g., root.Open/os.Open). func (test *rootMultiTest) setOp(format string, a ...any) { if test.root != nil { test.op = "root." } else { test.op = "os." } test.op += fmt.Sprintf(format, a...) } var errAny = errors.New("any error") func (test *rootMultiTest) errorf(t *testing.T, format string, args ...any) { t.Errorf("%v:", test.op) t.Fatalf(" "+format, args...) } // wantError tests whether got matches want. // If want is errAny, got may be any non-nil error. func (test *rootMultiTest) wantError(t *testing.T, got, want error) { t.Helper() if errors.Is(got, want) || (got != nil && want == errAny) { return } t.Fatalf("%v:\ngot error: %v\nwant error: %v", test.op, got, want) } func runRootMultiTestDescs(t *testing.T, source, target testFileDesc, f func(*testing.T, *rootMultiTest) (string, error)) { rootTest := newRootTest(t, source, target, true) osTest := newRootTest(t, source, target, false) initialContent := dirTreeContents(t, rootTest.dir) t.Cleanup(func() { if t.Failed() { t.Log("Initial directory contents:") for _, line := range initialContent { t.Logf(" %v", line) } } }) rootResult, rootErr := f(t, rootTest) if runtime.GOOS == "darwin" { // Darwin appears to have a kernel bug which causes restrictions on paths // with a trailing / to not be applied during uncached path lookups. // These restrictions are applied during cached lookups, so the results // of operating on /-suffixed paths are inconsistent. // // An example of this Darwin behavior (as of 25.4.0) is: // $ mkdir -p test/dir // $ echo hello > test/file // $ ln -s dir/../file test/link // $ cat test/link/ // hello // $ cat test/link/ // cat: test/link/: Not a directory // // Since Darwin isn't consistent with itself, we can't verify that we're // consistent with it. if rootTest.source.anySlashSuffix() || rootTest.target.anySlashSuffix() { return } } if runtime.GOOS == "wasip1" || runtime.GOOS == "js" { // WASI runtimes don't have any consistent behavior for handling paths with // a trailing /, so skip consistency tests for these paths. if rootTest.source.anySlashSuffix() || rootTest.target.anySlashSuffix() { return } } osResult, osErr := f(t, osTest) t.Cleanup(func() { if t.Failed() || !*testVerbose { return } rootContent := dirTreeContents(t, rootTest.dir) osContent := dirTreeContents(t, osTest.dir) t.Log("Initial directory contents:") for _, line := range initialContent { t.Logf(" %v", line) } t.Logf("%v:", rootTest.op) t.Logf(" result: %v", rootResult) t.Logf(" error: %v", rootErr) for _, line := range rootContent { t.Logf(" %v", line) } t.Logf("%v:", osTest.op) t.Logf(" result: %v", osResult) t.Logf(" error: %v", osErr) for _, line := range osContent { t.Logf(" %v", line) } }) if errors.Is(rootErr, os.ErrPathEscapes) { // os.Root forbids this operation (and is therefore not consistent with // the non-root version). return } if rootErr == errSkipRootConsistencyCheck || osErr == errSkipRootConsistencyCheck { return } // Consistency check: Performing the same operation in and out of a root // should produce the same results. if rootResult != osResult { t.Errorf("inconsistent results in/out of root") t.Errorf("%v:", rootTest.op) t.Errorf(" result: %v", rootResult) t.Errorf("%v:", osTest.op) t.Errorf(" result: %v", osResult) } if (rootErr == nil) != (osErr == nil) { t.Errorf("inconsistent errors in/out of root") t.Errorf("%v:", rootTest.op) t.Errorf(" error: %v", rootErr) t.Errorf("%v:", osTest.op) t.Errorf(" error: %v", osErr) } // Filesystem consistency check: Same files in the same places. rootContent := dirTreeContents(t, rootTest.dir) osContent := dirTreeContents(t, osTest.dir) if !slices.Equal(rootContent, osContent) { t.Errorf("inconsistent filesystem after running in/out of root") t.Errorf("%v:", rootTest.op) for _, line := range rootContent { t.Errorf(" %v", line) } t.Errorf("%v:", osTest.op) for _, line := range osContent { t.Errorf(" %v", line) } } } func newRootTest(t *testing.T, source, target testFileDesc, inRoot bool) *rootMultiTest { dir := makefs(t, []string{ "DIR/", }) var root *os.Root if inRoot { var err error root, err = os.OpenRoot(dir) if err != nil { t.Fatal(err) } t.Cleanup(func() { root.Close() }) } test := &rootMultiTest{ dir: dir, root: root, source: source, target: target, } createFile := func(name string, desc testFileDesc) (path string, fi os.FileInfo) { if desc.kind == testFileUnused { return "", nil } fi = desc.create(t, dir, name, name) path = desc.ref.path(dir, name) if !inRoot && !filepath.IsAbs(path) { path = dir + "/" + path } return path, fi } test.sourcePath, test.sourceInfo = createFile("source", source) test.targetPath, test.targetInfo = createFile("target", target) return test } // testFileKind is a kind of file. type testFileKind int const ( testFileUnused = testFileKind(iota) testFileAbsent // file does not exist testFileFile // regular file testFileDir // directory testFileSymlink // symlink testFileMax // testFileError represents a path which fails during resolution, // such as "a/b" where "a" does not exist. testFileError ) func (kind testFileKind) String() string { switch kind { case testFileUnused: return "unused" case testFileAbsent: return "absent" case testFileFile: return "file" case testFileDir: return "dir" case testFileSymlink: return "symlink" case testFileError: return "error" default: return fmt.Sprintf("testFileKind(%d)", kind) } } // testFileRef is a kind of reference to a file. // // Many path names can refer to the same file: f, ./f, /abs/path/to/f, somedir/../f, etc. // A testFileRef describes some form of reference. type testFileRef struct { // name is the name of the reference (not the file name). // These are a bit cryptic to keep test names short: // s (/ slash), p (.. parent), b (base), d (directory), r (root) name string // template is a template path. // // templates assume that the file is contained in a directory named "ROOT", // and that "ROOT/DIR" exists and is a directory. // // The string BASE in the template may be replaced with the file's basename. // // Absolute path templates start with /ROOT. template string // escapes indicates whether the path escapes the current directory. escapes bool } var testFileRefs = []testFileRef{ {escapes: false, name: "b", template: "BASE"}, {escapes: false, name: "bs", template: "BASE/"}, {escapes: false, name: "dpb", template: "DIR/../BASE"}, {escapes: false, name: "dpbs", template: "DIR/../BASE/"}, {escapes: true, name: "prb", template: "../ROOT/BASE"}, {escapes: true, name: "prbs", template: "../ROOT/BASE/"}, {escapes: true, name: "srb", template: "/ROOT/BASE"}, {escapes: true, name: "srbs", template: "/ROOT/BASE/"}, } // testFileLimitedRefs is a smaller set of references which do not exercise path escapes // (see allTestFileDescs). var testFileLimitedRefs = testFileRefs[0:2] // path creates a path using the template. // // dir is the absolute path to the root directory (which must be named "ROOT"). // base is the name of the target file within the root directory. func (ref testFileRef) path(dir, base string) string { p := ref.template p = strings.ReplaceAll(p, "BASE", base) if trim, ok := strings.CutPrefix(p, "/ROOT"); ok { p = dir + trim } return p } // hasSlashSuffix reports whether the file reference ends in a /. func (ref testFileRef) hasSlashSuffix() bool { return strings.HasSuffix(ref.template, "/") } // testFileDesc is a description of a type of file, combining the kind and reference type. // // Some sample testFileDescs: // - "name", a plain file. // - "DIR/../name", a directory // - "name/", where name is a symlink to "DIR/../target/", where target is a plain file. type testFileDesc struct { kind testFileKind ref testFileRef target *testFileDesc // symlink target, nil when kind is not testFileSymlink } var rootComprehensive = flag.Bool("root_comprehensive", false, "run many more os.Root test variations (slow, uncertain value)") // allTestFileDescs returns an iterator over all the testFileDescs we use in tests. func allTestFileDescs() iter.Seq[testFileDesc] { // A testFileDesc contains a reference type ("name", "d/../name", "../r/name", etc.) and // a file kind (file, directory, symlink, etc.). // // When the kind is symlink, the desc contains a reference type and file kind for // the link target as well. We only exercise one level of symlink (although we // could do more), so this means a testFileDesc effectively contains four axes of // variation: ref, kind, symlink ref, symlink kind. // // For example: // // - "name" is a file // - "d/../name" is a directory // - "name" is a symlink to "name2" which is a file // - "d/../name" is a symlink to "d/../name2" which is a directory // - etc. // // It is feasible to test every possible variation of these four axes, // but this is quite a few tests and gets quite slow. So by default we exclude // some variations. We test: // // - every reference to every kind, except symlink // - direct and direct/ references to a symlink to every reference to a file // - a direct reference to a symlink to a direct reference to every kind (except file) // // The full set of variations may be enabled with the -comprehensive_root_tests flag. return func(yield func(testFileDesc) bool) { // Every type of reference to every type of file, except symlink. for _, ref := range testFileRefs { for kind := range testFileMax { if kind == testFileUnused || kind == testFileSymlink { continue } desc := testFileDesc{ kind: kind, ref: ref, } if !yield(desc) { return } } } // Unless we're being comprehensive, only direct references to symlinks. refs := testFileRefs if !*rootComprehensive { refs = testFileLimitedRefs } for _, ref := range refs { for linkKind := range testFileMax { if linkKind == testFileUnused || linkKind == testFileSymlink { continue } linkRefs := testFileRefs if !*rootComprehensive && linkKind != testFileFile && linkKind != testFileDir { linkRefs = testFileLimitedRefs } for _, linkRef := range linkRefs { desc := testFileDesc{ kind: testFileSymlink, ref: ref, target: &testFileDesc{ kind: linkKind, ref: linkRef, }, } if !yield(desc) { return } } } } } } // String returns the target name. // // These are somewhat cryptic to keep test names short. // For example, "bsSdpbD" is: // // bs - "BASE/" // S - symlink // dpb - "DIR/../BASE" // D - directory // // So, open "file1/", where file1 is a symlink to "DIR/../file2", where file2 is a directory. func (desc testFileDesc) String() string { s := desc.ref.name + strings.ToUpper(desc.kind.String()[:1]) if desc.kind == testFileSymlink { s += desc.target.String() } return s } // escapes reports whether accessing this file escapes the root, // either because the file name escapes or because some element of a symlink chain escapes. func (desc testFileDesc) escapes() bool { if desc.ref.escapes { return true } if desc.kind == testFileSymlink { return desc.target.escapes() } return false } func (desc testFileDesc) lescapes() bool { if desc.ref.escapes { return true } if runtime.GOOS == "windows" { // On POSIX filesystems, a trailing slash at the end of a path causes // symlinks in the last path component to be resolved. // On Windows, a trailing slash does not cause symlink resolution. return false } if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink { return desc.target.escapes() } return false } // finalKind reports the kind of the file after following all symlinks. func (desc testFileDesc) finalKind() testFileKind { if desc.kind == testFileSymlink { return desc.target.finalKind() } return desc.kind } func (desc testFileDesc) lfinalKind() testFileKind { switch runtime.GOOS { case "windows": if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink && desc.target.kind != testFileDir { return testFileError } default: if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink { return desc.target.finalKind() } } return desc.kind } func (desc testFileDesc) isError() bool { if runtime.GOOS == "js" { return false } var isError func(desc testFileDesc, hasSuffix bool) bool isError = func(desc testFileDesc, hasSuffix bool) bool { if desc.ref.escapes { return false } if desc.ref.hasSlashSuffix() { hasSuffix = true } switch desc.kind { case testFileDir: return false case testFileSymlink: if runtime.GOOS == "windows" && hasSuffix && desc.target.kind != testFileDir { return true } return isError(*desc.target, hasSuffix) default: return hasSuffix } } return isError(desc, false) } func (desc testFileDesc) isSymlinkToDir() bool { if desc.kind != testFileSymlink { return false } if desc.ref.escapes { return false } if desc.finalKind() == testFileDir { return true } return false } // anySlashSuffix reports whether any of the names in the file // (either the initial name, or a symlink target) // include a trailing /. func (desc testFileDesc) anySlashSuffix() bool { name := desc.ref.template if len(name) > 0 && os.IsPathSeparator(name[len(name)-1]) { return true } if desc.kind == testFileSymlink { return desc.target.anySlashSuffix() } return false } // anySlashSuffix reports whether the name of the file includes a trailing /. func (desc testFileDesc) slashSuffix() bool { name := desc.ref.template if len(name) > 0 && os.IsPathSeparator(name[len(name)-1]) { return true } return false } // create creates the file(s) for this descriptor. // // dir is the test root directory. // base is the base name of the file we will open within the root. // (If there are symlinks, base is the start of the symlink chain.) // // Tests may create, delete, or move files, which makes it useful to have a way to identify // and track the files that existed at the start of the test. The token parameter identifies // which file we're creating. When symlinks are involved, the token is used in creating the // final, non-symlink file. func (desc testFileDesc) create(t *testing.T, dir, base, token string) (fi os.FileInfo) { path := filepath.Join(dir, base) switch desc.kind { case testFileAbsent: // File does not exist. case testFileFile: // Regular file. We use the token as the file contents. if err := os.WriteFile(path, []byte(token), 0o666); err != nil { t.Fatal(err) } case testFileDir: // Directory. We create a subdir within the directory named "c_"+token. // (The "c_" prefix is to distinguish this subdir from any files that may // have the same name as the token.) if err := os.Mkdir(path, 0o777); err != nil { t.Fatal(err) } case testFileSymlink: // Symlink. We create a symlink target named "s_"+base. if runtime.GOOS == "plan9" { t.Skip("symlinks not supported on " + runtime.GOOS) } linktarget := desc.target.ref.path(dir, "s_"+base) if runtime.GOOS == "wasip1" && filepath.IsAbs(linktarget) { t.Skip("absolute link targets not supported on " + runtime.GOOS) } fi = desc.target.create(t, dir, "s_"+base, token) if err := os.Symlink(linktarget, path); err != nil { t.Fatal(err) } default: t.Fatalf("can't create file of kind: %v", desc.kind) } if desc.kind == testFileFile || desc.kind == testFileDir { var err error fi, err = os.Lstat(path) if err != nil { t.Fatal(err) } } return fi } // testRootDescribeFile returns a string identifying a file. // // It returns "" if f is nil. // It returns "source" or "target" if f is the source or target file in the test. // Otherwise, it returns "unknown file". func (test *rootMultiTest) describeFile(t *testing.T, f *os.File) string { if f == nil { return "" } fi, err := f.Stat() if err != nil { t.Fatal(err) } switch { case os.SameFile(fi, test.sourceInfo): return "source" case os.SameFile(fi, test.targetInfo): return "target" default: return "unknown file" } } // dirTreeContents returns a description of the contents of directory. // For example: // // drwxrwxrwx dir/ // -rw-rw-rw- dir/file "file contents" // Lrw-rw-rw- symlink => dir/file func dirTreeContents(t *testing.T, dir string) (contents []string) { root, err := os.OpenRoot(dir) if err != nil { t.Fatal(err) } defer root.Close() fs.WalkDir(root.FS(), ".", func(path string, d fs.DirEntry, err error) error { if path == "." { return nil } info, err := d.Info() if err != nil { t.Fatal(err) } ent := info.Mode().String() + " " + path switch d.Type() { case fs.ModeDir: ent += "/" case fs.ModeSymlink: target, err := root.Readlink(path) if err != nil { t.Fatal(err) } if filepath.IsAbs(target) { relPath, err := filepath.Rel(dir, target) if err == nil && filepath.IsLocal(relPath) { target = "/.../" + relPath } } ent += " => " + target default: f, err := root.Open(path) if err != nil { ent += " (unreadable)" } else { content, err := io.ReadAll(f) if err != nil { t.Fatal(err) } ent += fmt.Sprintf(" %q", content) } } contents = append(contents, ent) return nil }) return contents } // TestRootMultiOpen tests os.Root.Open. // // This also serves as a prototypical example of using rootMultiTest // (see also the doc comment on rootMultiTest above). func TestRootMultiOpen(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { // This function will be run many times, with different inputs: // - in and out of a Root // - opening a file, directory, symlink, or nothing at all // - opening various names: target, DIR/../target, /abs/path/to/target, etc. // // The test function should perform the requested operation // (for example: open "target" in a Root), // verify that the result is consistent with expectations, // and then return a description of the result. // // The returned description is used to validate consistent behavior // between operations in and out of a Root. var open = os.Open if test.root != nil { open = test.root.Open } test.setOp("Open(%q)", test.targetPath) // test's operation, for errors f, gotErr := open(test.targetPath) if gotErr == nil { defer f.Close() } // testRootDescribeFile returns a string identifying a file. // // This is always "source" or "target" for the source/target files in a test, // or "" if f is nil. // (Note that most tests use only a target file, no source.) got := test.describeFile(t, f) switch { case test.root != nil && test.target.escapes(): // The operation escapes the root. test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileAbsent: // The file does not exist ("absent"). test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): // The file name or a symlink target contain a trailing slash. // Trailing slashes are handled differently on different platforms, // so we won't try to assert an outcome when they are present. // runRootMultiTest will verify that root.Open and os.Open // produce consistent results. default: // We should have successfully opened the file. test.wantError(t, gotErr, nil) if want := "target"; got != want { t.Fatalf("opened file %q, want %q", got, want) } } // Return the name of the file opened (possibly "" for nothing) and the error. // runRootMultiTest will compare the results for in-a-root and out-of-a-root // to validate that they are the same. return got, gotErr }) } func TestRootMultiChmod(t *testing.T) { if runtime.GOOS == "wasip1" { t.Skip("Chmod not supported on " + runtime.GOOS) } runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var ( chmod = os.Chmod stat = os.Stat lstat = os.Lstat ) if test.root != nil { chmod = test.root.Chmod stat = test.root.Stat lstat = test.root.Lstat } // Using the wrong mode here can cause problems during test cleanup, // if we leave a temp dir with a mode that prevents listing or removing // its contents. // // read+execute permissions let us list directory contents, // and we restore writability before deleting the temp dir. wantMode := os.FileMode(0o500) // readable, executable if runtime.GOOS == "windows" { // On Windows, the only modes we support are the default (777/rwx) // or read-only (444/r-x). Making a directory read-only doesn't prevent // listing its contents, so we can use 444 here. wantMode = 0o444 // readable } t.Cleanup(func() { chmod(test.targetPath, 0o700) }) test.setOp("Chmod(%q, %o)", test.targetPath, wantMode) gotErr := chmod(test.targetPath, wantMode) escapes := test.target.escapes() targetKind := test.target.finalKind() if runtime.GOOS == "windows" { // On Windows, Chmod("symlink") affects the link, not its target. // See issue #71492. stat = lstat escapes = test.target.ref.escapes targetKind = test.target.kind } var gotMode fs.FileMode switch { case test.root != nil && escapes: test.wantError(t, gotErr, os.ErrPathEscapes) case targetKind == testFileAbsent: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): // Don't expect anything, just be consistent with the OS. default: test.wantError(t, gotErr, nil) fi, err := stat(test.targetPath) if err != nil { t.Fatalf("could not stat target: %v", err) } if runtime.GOOS == "windows" && !fi.Mode().IsRegular() { // See issue #71492. break } gotMode = fi.Mode() & fs.ModePerm if gotMode != wantMode { t.Fatalf("file %q:\ngot mode: %v\nwant mode: %v", test.targetPath, gotMode, wantMode) } } if runtime.GOOS == "windows" && test.root == nil && gotErr != nil { // On Windows, os.Chmod calls GetFileAttributes on the target. // This seems to fail in a number of situations where the os.Root // chmod path works. For now, just skip the consistency check // when os.Chmod fails. return "", errSkipRootConsistencyCheck } return gotMode.String(), gotErr }) } func TestRootMultiCreate(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var create = os.Create if test.root != nil { create = test.root.Create } test.setOp("Create(%q)", test.targetPath) // test's operation, for errors f, gotErr := create(test.targetPath) if gotErr == nil { defer f.Close() } switch { case test.target.isError(): test.wantError(t, gotErr, errAny) case runtime.GOOS == "windows" && test.target.isSymlinkToDir(): // The error here is because the link is a Windows directory link, // not because the link target is a directory. test.wantError(t, gotErr, errAny) case test.root != nil && test.target.escapes(): // The operation escapes the root. test.wantError(t, gotErr, os.ErrPathEscapes) default: } return "", gotErr }) } func TestRootMultiLink(t *testing.T) { if runtime.GOOS == "wasip1" { switch os.Getenv("GOWASIRUNTIME") { case "", "wasmtime": // This test fails when run with wasmtime, because os.RemoveAll fails // to remove the test tempdir. t.Skip("test seems to tickle a wasmtime bug") } } testenv.MustHaveLink(t) runRootMultiTest2(t, func(t *testing.T, test *rootMultiTest) (string, error) { var ( rename = os.Link ) if test.root != nil { rename = test.root.Link } test.setOp("Link(%q, %q)", test.sourcePath, test.targetPath) gotErr := rename(test.sourcePath, test.targetPath) switch { case test.root != nil && test.source.lescapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.source.lfinalKind() == testFileAbsent: test.wantError(t, gotErr, errAny) case test.source.kind == testFileSymlink: // os.Link(old, new) may or may not deference old when it is a symlink. // POSIX says that link(2) should deference the source, but implementations // are inconsistent. return "", errSkipRootConsistencyCheck case test.source.slashSuffix() && test.source.lfinalKind() != testFileDir: test.wantError(t, gotErr, errAny) } return "", gotErr }) } func TestRootMultiLstat(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var ( lstat = os.Lstat ) if test.root != nil { lstat = test.root.Lstat } test.setOp("Lstat(%q)", test.targetPath) gotStat, gotErr := lstat(test.targetPath) result := "" if gotStat != nil { result = gotStat.Mode().String() } escapes := test.target.lescapes() finalKind := test.target.lfinalKind() if runtime.GOOS == "windows" && test.target.ref.hasSlashSuffix() { // When the target of lstat has a trailing slash, // Windows follows it. escapes = test.target.escapes() finalKind = test.target.finalKind() } switch { case test.root != nil && escapes: test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.kind == testFileAbsent: // Target does not exist. test.wantError(t, gotErr, errAny) case finalKind == testFileSymlink: test.wantError(t, gotErr, nil) if got, want := gotStat.Mode().Type(), fs.ModeSymlink; got != want { test.errorf(t, "got mode %v, want %v", got, want) } case gotErr != nil: default: if !os.SameFile(gotStat, test.targetInfo) { test.errorf(t, "stat result is not for target file; want it to be") } } return result, gotErr }) } func TestRootMultiMkdir(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var ( mkdir = os.Mkdir stat = os.Stat ) if test.root != nil { mkdir = test.root.Mkdir stat = test.root.Stat } test.setOp("Mkdir(%q, 0o777)", test.targetPath) gotErr := mkdir(test.targetPath, 0o777) switch { case test.root != nil && test.target.ref.escapes: // "mkdir ../target", or equivalent escaping path. test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.slashSuffix() && test.target.kind == testFileSymlink: // "mkdir symlink/", inconsistent behavior across platforms // as to whether this follows the symlink or not. // // If the symlink escapes, this needs to be some kind of error though. if test.root != nil && test.target.escapes() { test.wantError(t, gotErr, errAny) } if runtime.GOOS == "openbsd" { // Known inconsistency: OpenBSD doesn't resolve the final // symlink when creating a directory. return "", errSkipRootConsistencyCheck } case test.target.kind != testFileAbsent: // "mkdir target", where target exists. test.wantError(t, gotErr, errAny) default: test.wantError(t, gotErr, nil) fi, err := stat(test.targetPath) if err != nil { t.Fatalf("could not stat target: %v", err) } if !fi.IsDir() { t.Fatalf("%q: not a directory, expected it to be", test.targetPath) } } return "", gotErr }) } func TestRootMultiMkdirAllShallow(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { return testRootMultiMkdirAll(t, test, test.targetPath) }) } func TestRootMultiMkdirAllDeep(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { targetPath := test.targetPath if len(targetPath) > 0 && os.IsPathSeparator(targetPath[len(targetPath)-1]) { targetPath += "a/b/" } else { targetPath += "/a/b" } return testRootMultiMkdirAll(t, test, targetPath) }) } func testRootMultiMkdirAll(t *testing.T, test *rootMultiTest, targetPath string) (string, error) { var mkdirAll = os.MkdirAll if test.root != nil { mkdirAll = test.root.MkdirAll } test.setOp("MkdirAll(%q, 0o777)", targetPath) gotErr := mkdirAll(targetPath, 0o777) switch { case test.root != nil && test.target.lescapes(): // "mkdir ../target", or equivalent escaping path. test.wantError(t, gotErr, os.ErrPathEscapes) case test.root != nil && test.target.escapes(): // "mkdir ../target", or equivalent escaping path. test.wantError(t, gotErr, errAny) return "", errSkipRootConsistencyCheck case test.root != nil && test.target.kind == testFileSymlink && test.target.target.kind == testFileAbsent && targetPath != test.targetPath: // A minor inconsistency between Root.MkdirAll and os.MkdirAll: // When an intermediate component of the tree being constructed is a // dangling symlink, Root.MkdirAll will follow the symlink and create // its target directory, while os.MkdirAll will fail with an error. return "", errSkipRootConsistencyCheck default: } return "", gotErr } func TestRootMultiRename(t *testing.T) { if runtime.GOOS == "wasip1" { switch os.Getenv("GOWASIRUNTIME") { case "", "wasmtime": // This test fails when run with wasmtime, because os.RemoveAll fails // to remove the test tempdir. t.Skip("test seems to tickle a wasmtime bug") } } runRootMultiTest2(t, func(t *testing.T, test *rootMultiTest) (string, error) { var ( rename = os.Rename ) if test.root != nil { rename = test.root.Rename } // TODO: target directory (if any) should be empty test.setOp("Rename(%q, %q)", test.sourcePath, test.targetPath) gotErr := rename(test.sourcePath, test.targetPath) if runtime.GOOS == "windows" && (test.source.finalKind() != test.target.finalKind() || test.source.kind == testFileSymlink || test.target.kind == testFileSymlink) { // os.Rename on Windows is implemented using MoveFileEx, // while Root.Rename is implemented using NtSetInformationFileEx // with an explicit request for POSIX semantics. // // This means the two do not behave the same when renaming // a file onto a directory or vice-versa. // // We should make this consistent, but for now just skip // the consistency checks in this case. return "", errSkipRootConsistencyCheck } switch { case test.root != nil && test.source.lescapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.source.lfinalKind() == testFileAbsent: test.wantError(t, gotErr, errAny) case test.source.slashSuffix() && test.source.lfinalKind() != testFileDir && runtime.GOOS != "js": test.wantError(t, gotErr, errAny) case test.root != nil && test.target.lescapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case runtime.GOOS == "plan9": // Plan9 rename behaves differently. // Just rely on consistency checks. case test.target.lfinalKind() == testFileDir: // POSIX rename() will replace an empty target directory, // but os.Rename will not. test.wantError(t, gotErr, errAny) case test.source.lfinalKind() == testFileDir && test.target.lfinalKind() != testFileAbsent: test.wantError(t, gotErr, errAny) case test.source.anySlashSuffix() || test.target.anySlashSuffix(): if runtime.GOOS == "openbsd" { // Known inconsistency: OpenBSD doesn't resolve the final // symlink when creating a directory. return "", errSkipRootConsistencyCheck } default: test.wantError(t, gotErr, nil) // TODO: check that the file is in its new location } if runtime.GOOS == "linux" && (test.source.slashSuffix() || test.target.slashSuffix()) { return "", errSkipRootConsistencyCheck } return "", gotErr }) } func TestRootMultiReadFile(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var readFile = os.ReadFile if test.root != nil { readFile = test.root.ReadFile } test.setOp("ReadFile(%q)", test.targetPath) data, gotErr := readFile(test.targetPath) var got string if gotErr == nil { got = string(data) } switch { case test.root != nil && test.target.escapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileAbsent: test.wantError(t, gotErr, errAny) case runtime.GOOS == "plan9": // Plan9 lets you read from directories. // Just rely on consistency checks. case runtime.GOOS == "netbsd": // See https://go.dev/issue/80322: // NetBSD builder appears to be succeeding on read-from-dir as well. return "", gotErr case test.target.finalKind() == testFileDir: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): // Trailing slashes are handled differently on different platforms, // so we won't try to assert an outcome when they are present. // runRootMultiTest will verify that root.ReadFile and os.ReadFile // produce consistent results. default: test.wantError(t, gotErr, nil) if want := "target"; got != want { t.Fatalf("read file content %q, want %q", got, want) } } return got, gotErr }) } func TestRootMultiStat(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var stat = os.Stat if test.root != nil { stat = test.root.Stat } test.setOp("Stat(%q)", test.targetPath) gotStat, gotErr := stat(test.targetPath) switch { case test.target.isError(): test.wantError(t, gotErr, errAny) case test.root != nil && test.target.escapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileAbsent: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): default: test.wantError(t, gotErr, nil) if !os.SameFile(gotStat, test.targetInfo) { test.errorf(t, "stat result is not for target file; want it to be") } } return "", gotErr }) } func TestRootMultiRemove(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var remove = os.Remove if test.root != nil { remove = test.root.Remove } test.setOp("Remove(%q)", test.targetPath) gotErr := remove(test.targetPath) switch { case test.root != nil && test.target.lescapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.kind == testFileAbsent: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): if runtime.GOOS == "linux" { // Linux treats rmdir("symlink/") as an error when // "symlink" is a symlink to a directory. // Root.Remove prefers the POSIX interpretation // of resolving the symlink. return "", errSkipRootConsistencyCheck } default: test.wantError(t, gotErr, nil) } return "", gotErr }) } func TestRootMultiRemoveAll(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var removeAll = os.RemoveAll if test.root != nil { removeAll = test.root.RemoveAll } test.setOp("RemoveAll(%q)", test.targetPath) gotErr := removeAll(test.targetPath) switch { case test.root != nil && test.target.ref.escapes: // This is only checking target.ref.escapes, // not target.lescapes(), because RemoveAll strips // terminal slashes. test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.anySlashSuffix(): // We are inconsistent on some platforms on whether // RemoveAll("symlink/") removes the link or the link target. // Something worth addressing, but for now skip the check. return "", errSkipRootConsistencyCheck default: test.wantError(t, gotErr, nil) } return "", gotErr }) } func TestRootMultiChtimes(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var chtimes = os.Chtimes if test.root != nil { chtimes = test.root.Chtimes } now := time.Now() test.setOp("Chtimes(%q, %v, %v)", test.targetPath, now, now) gotErr := chtimes(test.targetPath, now, now) switch { case test.target.isError(): test.wantError(t, gotErr, errAny) case test.root != nil && test.target.escapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileAbsent: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): default: test.wantError(t, gotErr, nil) } return "", gotErr }) } func TestRootMultiReadlink(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var readlink = os.Readlink if test.root != nil { readlink = test.root.Readlink } test.setOp("Readlink(%q)", test.targetPath) got, gotErr := readlink(test.targetPath) if suffix, ok := strings.CutPrefix(got, test.dir); ok { // Replace absolute path prefix with /.../ got = "/..." + suffix } switch { case test.root != nil && test.target.lescapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.kind != testFileSymlink: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): default: test.wantError(t, gotErr, nil) } return got, gotErr }) } func TestRootMultiWriteFile(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var writeFile = os.WriteFile if test.root != nil { writeFile = test.root.WriteFile } test.setOp("WriteFile(%q, ...)", test.targetPath) gotErr := writeFile(test.targetPath, []byte("data"), 0o666) switch { case test.target.isError(): test.wantError(t, gotErr, errAny) case runtime.GOOS == "windows" && test.target.isSymlinkToDir(): test.wantError(t, gotErr, errAny) case test.root != nil && test.target.escapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileDir: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): default: test.wantError(t, gotErr, nil) } return "", gotErr }) } func TestRootMultiOpenFile(t *testing.T) { runRootMultiTest(t, func(t *testing.T, test *rootMultiTest) (string, error) { var openFile = os.OpenFile if test.root != nil { openFile = test.root.OpenFile } test.setOp("OpenFile(%q, O_RDONLY, 0)", test.targetPath) f, gotErr := openFile(test.targetPath, os.O_RDONLY, 0) if gotErr == nil { defer f.Close() } got := test.describeFile(t, f) switch { case test.root != nil && test.target.escapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileAbsent: test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): default: test.wantError(t, gotErr, nil) if want := "target"; got != want { t.Fatalf("opened file %q, want %q", got, want) } } return got, gotErr }) }