Source file src/os/root_test.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package os_test
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"internal/testenv"
    12  	"io"
    13  	"io/fs"
    14  	"net"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"runtime"
    19  	"slices"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  )
    24  
    25  // testMaybeRooted calls f in two subtests,
    26  // one with a Root and one with a nil r.
    27  func testMaybeRooted(t *testing.T, f func(t *testing.T, r *os.Root)) {
    28  	t.Run("NoRoot", func(t *testing.T) {
    29  		t.Chdir(t.TempDir())
    30  		f(t, nil)
    31  	})
    32  	t.Run("InRoot", func(t *testing.T) {
    33  		t.Chdir(t.TempDir())
    34  		r, err := os.OpenRoot(".")
    35  		if err != nil {
    36  			t.Fatal(err)
    37  		}
    38  		defer r.Close()
    39  		f(t, r)
    40  	})
    41  }
    42  
    43  // makefs creates a test filesystem layout and returns the path to its root.
    44  //
    45  // Each entry in the slice is a file, directory, or symbolic link to create:
    46  //
    47  //   - "d/": directory d
    48  //   - "f": file f with contents f
    49  //   - "a => b": symlink a with target b
    50  //
    51  // The directory containing the filesystem is always named ROOT.
    52  // $ABS is replaced with the absolute path of the directory containing the filesystem.
    53  //
    54  // Parent directories are automatically created as needed.
    55  //
    56  // makefs calls t.Skip if the layout contains features not supported by the current GOOS.
    57  func makefs(t *testing.T, fs []string) string {
    58  	root := path.Join(t.TempDir(), "ROOT")
    59  	if err := os.Mkdir(root, 0o777); err != nil {
    60  		t.Fatal(err)
    61  	}
    62  	for _, ent := range fs {
    63  		ent = strings.ReplaceAll(ent, "$ABS", root)
    64  		base, link, isLink := strings.Cut(ent, " => ")
    65  		if isLink {
    66  			if runtime.GOOS == "wasip1" && path.IsAbs(link) {
    67  				t.Skip("absolute link targets not supported on " + runtime.GOOS)
    68  			}
    69  			if runtime.GOOS == "plan9" {
    70  				t.Skip("symlinks not supported on " + runtime.GOOS)
    71  			}
    72  			ent = base
    73  		}
    74  		if err := os.MkdirAll(path.Join(root, path.Dir(base)), 0o777); err != nil {
    75  			t.Fatal(err)
    76  		}
    77  		if isLink {
    78  			if err := os.Symlink(link, path.Join(root, base)); err != nil {
    79  				t.Fatal(err)
    80  			}
    81  		} else if strings.HasSuffix(ent, "/") {
    82  			if err := os.MkdirAll(path.Join(root, ent), 0o777); err != nil {
    83  				t.Fatal(err)
    84  			}
    85  		} else {
    86  			if err := os.WriteFile(path.Join(root, ent), []byte(ent), 0o666); err != nil {
    87  				t.Fatal(err)
    88  			}
    89  		}
    90  	}
    91  	return root
    92  }
    93  
    94  // A rootTest is a test case for os.Root.
    95  type rootTest struct {
    96  	name string
    97  
    98  	// fs is the test filesystem layout. See makefs above.
    99  	fs []string
   100  
   101  	// open is the filename to access in the test.
   102  	open string
   103  
   104  	// target is the filename that we expect to be accessed, after resolving all symlinks.
   105  	// For test cases where the operation fails due to an escaping path such as ../ROOT/x,
   106  	// the target is the filename that should not have been opened.
   107  	target string
   108  
   109  	// ltarget is the filename that we expect to accessed, after resolving all symlinks
   110  	// except the last one. This is the file we expect to be removed by Remove or statted
   111  	// by Lstat.
   112  	//
   113  	// If the last path component in open is not a symlink, ltarget should be "".
   114  	ltarget string
   115  
   116  	// wantError is true if accessing the file should fail.
   117  	wantError bool
   118  
   119  	// alwaysFails is true if the open operation is expected to fail
   120  	// even when using non-openat operations.
   121  	//
   122  	// This lets us check that tests that are expected to fail because (for example)
   123  	// a path escapes the directory root will succeed when the escaping checks are not
   124  	// performed.
   125  	alwaysFails bool
   126  }
   127  
   128  // run sets up the test filesystem layout, os.OpenDirs the root, and calls f.
   129  func (test *rootTest) run(t *testing.T, f func(t *testing.T, target string, d *os.Root)) {
   130  	t.Run(test.name, func(t *testing.T) {
   131  		root := makefs(t, test.fs)
   132  		d, err := os.OpenRoot(root)
   133  		if err != nil {
   134  			t.Fatal(err)
   135  		}
   136  		defer d.Close()
   137  		// The target is a file that will be accessed,
   138  		// or a file that should not be accessed
   139  		// (because doing so escapes the root).
   140  		target := test.target
   141  		if test.target != "" {
   142  			target = filepath.Join(root, test.target)
   143  		}
   144  		f(t, target, d)
   145  	})
   146  }
   147  
   148  // errEndsTest checks the error result of a test,
   149  // verifying that it succeeded or failed as expected.
   150  //
   151  // It returns true if the test is done due to encountering an expected error.
   152  // false if the test should continue.
   153  func errEndsTest(t *testing.T, err error, wantError bool, format string, args ...any) bool {
   154  	t.Helper()
   155  	if wantError {
   156  		if err == nil {
   157  			op := fmt.Sprintf(format, args...)
   158  			t.Fatalf("%v = nil; want error", op)
   159  		}
   160  		return true
   161  	} else {
   162  		if err != nil {
   163  			op := fmt.Sprintf(format, args...)
   164  			t.Fatalf("%v = %v; want success", op, err)
   165  		}
   166  		return false
   167  	}
   168  }
   169  
   170  var rootTestCases = []rootTest{{
   171  	name:   "plain path",
   172  	fs:     []string{},
   173  	open:   "target",
   174  	target: "target",
   175  }, {
   176  	name: "path in directory",
   177  	fs: []string{
   178  		"a/b/c/",
   179  	},
   180  	open:   "a/b/c/target",
   181  	target: "a/b/c/target",
   182  }, {
   183  	name: "symlink",
   184  	fs: []string{
   185  		"link => target",
   186  	},
   187  	open:    "link",
   188  	target:  "target",
   189  	ltarget: "link",
   190  }, {
   191  	name: "symlink dotdot slash",
   192  	fs: []string{
   193  		"link => ../",
   194  	},
   195  	open:      "link",
   196  	ltarget:   "link",
   197  	wantError: true,
   198  }, {
   199  	name: "symlink ending in slash",
   200  	fs: []string{
   201  		"dir/",
   202  		"link => dir/",
   203  	},
   204  	open:   "link/target",
   205  	target: "dir/target",
   206  }, {
   207  	name: "symlink dotdot dotdot slash",
   208  	fs: []string{
   209  		"dir/link => ../../",
   210  	},
   211  	open:      "dir/link",
   212  	ltarget:   "dir/link",
   213  	wantError: true,
   214  }, {
   215  	name: "symlink chain",
   216  	fs: []string{
   217  		"link => a/b/c/target",
   218  		"a/b => e",
   219  		"a/e => ../f",
   220  		"f => g/h/i",
   221  		"g/h/i => ..",
   222  		"g/c/",
   223  	},
   224  	open:    "link",
   225  	target:  "g/c/target",
   226  	ltarget: "link",
   227  }, {
   228  	name: "path with dot",
   229  	fs: []string{
   230  		"a/b/",
   231  	},
   232  	open:   "./a/./b/./target",
   233  	target: "a/b/target",
   234  }, {
   235  	name: "path with dotdot",
   236  	fs: []string{
   237  		"a/b/",
   238  	},
   239  	open:   "a/../a/b/../../a/b/../b/target",
   240  	target: "a/b/target",
   241  }, {
   242  	name:      "path with dotdot slash",
   243  	fs:        []string{},
   244  	open:      "../",
   245  	wantError: true,
   246  }, {
   247  	name:      "path with dotdot dotdot slash",
   248  	fs:        []string{},
   249  	open:      "a/../../",
   250  	wantError: true,
   251  }, {
   252  	name: "dotdot no symlink",
   253  	fs: []string{
   254  		"a/",
   255  	},
   256  	open:   "a/../target",
   257  	target: "target",
   258  }, {
   259  	name: "dotdot after symlink",
   260  	fs: []string{
   261  		"a => b/c",
   262  		"b/c/",
   263  	},
   264  	open: "a/../target",
   265  	target: func() string {
   266  		if runtime.GOOS == "windows" {
   267  			// On Windows, the path is cleaned before symlink resolution.
   268  			return "target"
   269  		}
   270  		return "b/target"
   271  	}(),
   272  }, {
   273  	name: "dotdot before symlink",
   274  	fs: []string{
   275  		"a => b/c",
   276  		"b/c/",
   277  	},
   278  	open:   "b/../a/target",
   279  	target: "b/c/target",
   280  }, {
   281  	name: "symlink ends in dot",
   282  	fs: []string{
   283  		"a => b/.",
   284  		"b/",
   285  	},
   286  	open:   "a/target",
   287  	target: "b/target",
   288  }, {
   289  	name:        "directory does not exist",
   290  	fs:          []string{},
   291  	open:        "a/file",
   292  	wantError:   true,
   293  	alwaysFails: true,
   294  }, {
   295  	name:        "empty path",
   296  	fs:          []string{},
   297  	open:        "",
   298  	wantError:   true,
   299  	alwaysFails: true,
   300  }, {
   301  	name: "symlink cycle",
   302  	fs: []string{
   303  		"a => a",
   304  	},
   305  	open:        "a",
   306  	ltarget:     "a",
   307  	wantError:   true,
   308  	alwaysFails: true,
   309  }, {
   310  	name:      "path escapes",
   311  	fs:        []string{},
   312  	open:      "../ROOT/target",
   313  	target:    "target",
   314  	wantError: true,
   315  }, {
   316  	name: "long path escapes",
   317  	fs: []string{
   318  		"a/",
   319  	},
   320  	open:      "a/../../ROOT/target",
   321  	target:    "target",
   322  	wantError: true,
   323  }, {
   324  	name: "absolute symlink",
   325  	fs: []string{
   326  		"link => $ABS/target",
   327  	},
   328  	open:      "link",
   329  	ltarget:   "link",
   330  	target:    "target",
   331  	wantError: true,
   332  }, {
   333  	name: "relative symlink",
   334  	fs: []string{
   335  		"link => ../ROOT/target",
   336  	},
   337  	open:      "link",
   338  	target:    "target",
   339  	ltarget:   "link",
   340  	wantError: true,
   341  }, {
   342  	name: "symlink chain escapes",
   343  	fs: []string{
   344  		"link => a/b/c/target",
   345  		"a/b => e",
   346  		"a/e => ../../ROOT",
   347  		"c/",
   348  	},
   349  	open:      "link",
   350  	target:    "c/target",
   351  	ltarget:   "link",
   352  	wantError: true,
   353  }}
   354  
   355  func TestRootOpen_File(t *testing.T) {
   356  	want := []byte("target")
   357  	for _, test := range rootTestCases {
   358  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   359  			if target != "" {
   360  				if err := os.WriteFile(target, want, 0o666); err != nil {
   361  					t.Fatal(err)
   362  				}
   363  			}
   364  			f, err := root.Open(test.open)
   365  			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
   366  				return
   367  			}
   368  			defer f.Close()
   369  			got, err := io.ReadAll(f)
   370  			if err != nil || !bytes.Equal(got, want) {
   371  				t.Errorf(`Dir.Open(%q): read content %q, %v; want %q`, test.open, string(got), err, string(want))
   372  			}
   373  		})
   374  	}
   375  }
   376  
   377  func TestRootOpen_Directory(t *testing.T) {
   378  	for _, test := range rootTestCases {
   379  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   380  			if target != "" {
   381  				if err := os.Mkdir(target, 0o777); err != nil {
   382  					t.Fatal(err)
   383  				}
   384  				if err := os.WriteFile(target+"/found", nil, 0o666); err != nil {
   385  					t.Fatal(err)
   386  				}
   387  			}
   388  			f, err := root.Open(test.open)
   389  			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
   390  				return
   391  			}
   392  			defer f.Close()
   393  			got, err := f.Readdirnames(-1)
   394  			if err != nil {
   395  				t.Errorf(`Dir.Open(%q).Readdirnames: %v`, test.open, err)
   396  			}
   397  			if want := []string{"found"}; !slices.Equal(got, want) {
   398  				t.Errorf(`Dir.Open(%q).Readdirnames: %q, want %q`, test.open, got, want)
   399  			}
   400  		})
   401  	}
   402  }
   403  
   404  func TestRootCreate(t *testing.T) {
   405  	want := []byte("target")
   406  	for _, test := range rootTestCases {
   407  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   408  			f, err := root.Create(test.open)
   409  			if errEndsTest(t, err, test.wantError, "root.Create(%q)", test.open) {
   410  				return
   411  			}
   412  			if _, err := f.Write(want); err != nil {
   413  				t.Fatal(err)
   414  			}
   415  			f.Close()
   416  			got, err := os.ReadFile(target)
   417  			if err != nil {
   418  				t.Fatalf(`reading file created with root.Create(%q): %v`, test.open, err)
   419  			}
   420  			if !bytes.Equal(got, want) {
   421  				t.Fatalf(`reading file created with root.Create(%q): got %q; want %q`, test.open, got, want)
   422  			}
   423  		})
   424  	}
   425  }
   426  
   427  func TestRootChmod(t *testing.T) {
   428  	if runtime.GOOS == "wasip1" {
   429  		t.Skip("Chmod not supported on " + runtime.GOOS)
   430  	}
   431  	for _, test := range rootTestCases {
   432  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   433  			if target != "" {
   434  				// Create a file with no read/write permissions,
   435  				// to ensure we can use Chmod on an inaccessible file.
   436  				if err := os.WriteFile(target, nil, 0o000); err != nil {
   437  					t.Fatal(err)
   438  				}
   439  			}
   440  			if runtime.GOOS == "windows" {
   441  				// On Windows, Chmod("symlink") affects the link, not its target.
   442  				// See issue 71492.
   443  				fi, err := root.Lstat(test.open)
   444  				if err == nil && !fi.Mode().IsRegular() {
   445  					t.Skip("https://go.dev/issue/71492")
   446  				}
   447  			}
   448  			want := os.FileMode(0o666)
   449  			err := root.Chmod(test.open, want)
   450  			if errEndsTest(t, err, test.wantError, "root.Chmod(%q)", test.open) {
   451  				return
   452  			}
   453  			st, err := os.Stat(target)
   454  			if err != nil {
   455  				t.Fatalf("os.Stat(%q) = %v", target, err)
   456  			}
   457  			if got := st.Mode(); got != want {
   458  				t.Errorf("after root.Chmod(%q, %v): file mode = %v, want %v", test.open, want, got, want)
   459  			}
   460  		})
   461  	}
   462  }
   463  
   464  func TestRootChtimes(t *testing.T) {
   465  	// Don't check atimes if the fs is mounted noatime,
   466  	// or on Plan 9 which does not permit changing atimes to arbitrary values.
   467  	checkAtimes := !hasNoatime() && runtime.GOOS != "plan9"
   468  	for _, test := range rootTestCases {
   469  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   470  			if target != "" {
   471  				if err := os.WriteFile(target, nil, 0o666); err != nil {
   472  					t.Fatal(err)
   473  				}
   474  			}
   475  			for _, times := range []struct {
   476  				atime, mtime time.Time
   477  			}{{
   478  				atime: time.Now().Add(-1 * time.Minute),
   479  				mtime: time.Now().Add(-1 * time.Minute),
   480  			}, {
   481  				atime: time.Now().Add(1 * time.Minute),
   482  				mtime: time.Now().Add(1 * time.Minute),
   483  			}, {
   484  				atime: time.Time{},
   485  				mtime: time.Now(),
   486  			}, {
   487  				atime: time.Now(),
   488  				mtime: time.Time{},
   489  			}} {
   490  				switch runtime.GOOS {
   491  				case "js", "plan9":
   492  					times.atime = times.atime.Truncate(1 * time.Second)
   493  					times.mtime = times.mtime.Truncate(1 * time.Second)
   494  				case "illumos":
   495  					times.atime = times.atime.Truncate(1 * time.Microsecond)
   496  					times.mtime = times.mtime.Truncate(1 * time.Microsecond)
   497  				}
   498  
   499  				err := root.Chtimes(test.open, times.atime, times.mtime)
   500  				if errEndsTest(t, err, test.wantError, "root.Chtimes(%q)", test.open) {
   501  					return
   502  				}
   503  				st, err := os.Stat(target)
   504  				if err != nil {
   505  					t.Fatalf("os.Stat(%q) = %v", target, err)
   506  				}
   507  				if got := st.ModTime(); !times.mtime.IsZero() && !got.Equal(times.mtime) {
   508  					t.Errorf("after root.Chtimes(%q, %v, %v): got mtime=%v, want %v", test.open, times.atime, times.mtime, got, times.mtime)
   509  				}
   510  				if checkAtimes {
   511  					if got := os.Atime(st); !times.atime.IsZero() && !got.Equal(times.atime) {
   512  						t.Errorf("after root.Chtimes(%q, %v, %v): got atime=%v, want %v", test.open, times.atime, times.mtime, got, times.atime)
   513  					}
   514  				}
   515  			}
   516  		})
   517  	}
   518  }
   519  
   520  func TestRootMkdir(t *testing.T) {
   521  	for _, test := range rootTestCases {
   522  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   523  			wantError := test.wantError
   524  			if !wantError {
   525  				fi, err := os.Lstat(filepath.Join(root.Name(), test.open))
   526  				if err == nil && fi.Mode().Type() == fs.ModeSymlink {
   527  					// This case is trying to mkdir("some symlink"),
   528  					// which is an error.
   529  					wantError = true
   530  				}
   531  			}
   532  
   533  			err := root.Mkdir(test.open, 0o777)
   534  			if errEndsTest(t, err, wantError, "root.Create(%q)", test.open) {
   535  				return
   536  			}
   537  			fi, err := os.Lstat(target)
   538  			if err != nil {
   539  				t.Fatalf(`stat file created with Root.Mkdir(%q): %v`, test.open, err)
   540  			}
   541  			if !fi.IsDir() {
   542  				t.Fatalf(`stat file created with Root.Mkdir(%q): not a directory`, test.open)
   543  			}
   544  			if mode := fi.Mode(); mode&0o777 == 0 {
   545  				// Issue #73559: We're not going to worry about the exact
   546  				// mode bits (which will have been modified by umask),
   547  				// but there should be mode bits.
   548  				t.Fatalf(`stat file created with Root.Mkdir(%q): mode=%v, want non-zero`, test.open, mode)
   549  			}
   550  		})
   551  	}
   552  }
   553  
   554  func TestRootMkdirAll(t *testing.T) {
   555  	for _, test := range rootTestCases {
   556  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   557  			wantError := test.wantError
   558  			if !wantError {
   559  				fi, err := os.Lstat(filepath.Join(root.Name(), test.open))
   560  				if err == nil && fi.Mode().Type() == fs.ModeSymlink {
   561  					// This case is trying to mkdir("some symlink"),
   562  					// which is an error.
   563  					wantError = true
   564  				}
   565  			}
   566  
   567  			err := root.Mkdir(test.open, 0o777)
   568  			if errEndsTest(t, err, wantError, "root.MkdirAll(%q)", test.open) {
   569  				return
   570  			}
   571  			fi, err := os.Lstat(target)
   572  			if err != nil {
   573  				t.Fatalf(`stat file created with Root.MkdirAll(%q): %v`, test.open, err)
   574  			}
   575  			if !fi.IsDir() {
   576  				t.Fatalf(`stat file created with Root.MkdirAll(%q): not a directory`, test.open)
   577  			}
   578  			if mode := fi.Mode(); mode&0o777 == 0 {
   579  				// Issue #73559: We're not going to worry about the exact
   580  				// mode bits (which will have been modified by umask),
   581  				// but there should be mode bits.
   582  				t.Fatalf(`stat file created with Root.MkdirAll(%q): mode=%v, want non-zero`, test.open, mode)
   583  			}
   584  		})
   585  	}
   586  }
   587  
   588  func TestRootOpenRoot(t *testing.T) {
   589  	for _, test := range rootTestCases {
   590  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   591  			if target != "" {
   592  				if err := os.Mkdir(target, 0o777); err != nil {
   593  					t.Fatal(err)
   594  				}
   595  				if err := os.WriteFile(target+"/f", nil, 0o666); err != nil {
   596  					t.Fatal(err)
   597  				}
   598  			}
   599  			rr, err := root.OpenRoot(test.open)
   600  			if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) {
   601  				return
   602  			}
   603  			defer rr.Close()
   604  			f, err := rr.Open("f")
   605  			if err != nil {
   606  				t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err)
   607  			}
   608  			f.Close()
   609  		})
   610  	}
   611  }
   612  
   613  func TestRootRemoveFile(t *testing.T) {
   614  	for _, test := range rootTestCases {
   615  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   616  			wantError := test.wantError
   617  			if test.ltarget != "" {
   618  				// Remove doesn't follow symlinks in the final path component,
   619  				// so it will successfully remove ltarget.
   620  				wantError = false
   621  				target = filepath.Join(root.Name(), test.ltarget)
   622  			} else if target != "" {
   623  				if err := os.WriteFile(target, nil, 0o666); err != nil {
   624  					t.Fatal(err)
   625  				}
   626  			}
   627  
   628  			err := root.Remove(test.open)
   629  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   630  				return
   631  			}
   632  			_, err = os.Lstat(target)
   633  			if !errors.Is(err, os.ErrNotExist) {
   634  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   635  			}
   636  		})
   637  	}
   638  }
   639  
   640  func TestRootRemoveDirectory(t *testing.T) {
   641  	for _, test := range rootTestCases {
   642  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   643  			wantError := test.wantError
   644  			if test.ltarget != "" {
   645  				// Remove doesn't follow symlinks in the final path component,
   646  				// so it will successfully remove ltarget.
   647  				wantError = false
   648  				target = filepath.Join(root.Name(), test.ltarget)
   649  			} else if target != "" {
   650  				if err := os.Mkdir(target, 0o777); err != nil {
   651  					t.Fatal(err)
   652  				}
   653  			}
   654  
   655  			err := root.Remove(test.open)
   656  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   657  				return
   658  			}
   659  			_, err = os.Lstat(target)
   660  			if !errors.Is(err, os.ErrNotExist) {
   661  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   662  			}
   663  		})
   664  	}
   665  }
   666  
   667  func TestRootRemoveAll(t *testing.T) {
   668  	for _, test := range rootTestCases {
   669  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   670  			wantError := test.wantError
   671  			if test.ltarget != "" {
   672  				// Remove doesn't follow symlinks in the final path component,
   673  				// so it will successfully remove ltarget.
   674  				wantError = false
   675  				target = filepath.Join(root.Name(), test.ltarget)
   676  			} else if target != "" {
   677  				if err := os.Mkdir(target, 0o777); err != nil {
   678  					t.Fatal(err)
   679  				}
   680  				if err := os.WriteFile(filepath.Join(target, "file"), nil, 0o666); err != nil {
   681  					t.Fatal(err)
   682  				}
   683  			}
   684  			targetExists := true
   685  			if _, err := root.Lstat(test.open); errors.Is(err, os.ErrNotExist) {
   686  				// If the target doesn't exist, RemoveAll succeeds rather
   687  				// than returning ErrNotExist.
   688  				targetExists = false
   689  				wantError = false
   690  			}
   691  
   692  			err := root.RemoveAll(test.open)
   693  			if errEndsTest(t, err, wantError, "root.RemoveAll(%q)", test.open) {
   694  				return
   695  			}
   696  			if !targetExists {
   697  				return
   698  			}
   699  			_, err = os.Lstat(target)
   700  			if !errors.Is(err, os.ErrNotExist) {
   701  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   702  			}
   703  		})
   704  	}
   705  }
   706  
   707  func TestRootOpenFileAsRoot(t *testing.T) {
   708  	dir := t.TempDir()
   709  	target := filepath.Join(dir, "target")
   710  	if err := os.WriteFile(target, nil, 0o666); err != nil {
   711  		t.Fatal(err)
   712  	}
   713  	r, err := os.OpenRoot(target)
   714  	if err == nil {
   715  		r.Close()
   716  		t.Fatal("os.OpenRoot(file) succeeded; want failure")
   717  	}
   718  	r, err = os.OpenRoot(dir)
   719  	if err != nil {
   720  		t.Fatal(err)
   721  	}
   722  	defer r.Close()
   723  	rr, err := r.OpenRoot("target")
   724  	if err == nil {
   725  		rr.Close()
   726  		t.Fatal("Root.OpenRoot(file) succeeded; want failure")
   727  	}
   728  }
   729  
   730  func TestRootStat(t *testing.T) {
   731  	for _, test := range rootTestCases {
   732  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   733  			const content = "content"
   734  			if target != "" {
   735  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   736  					t.Fatal(err)
   737  				}
   738  			}
   739  
   740  			fi, err := root.Stat(test.open)
   741  			if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) {
   742  				return
   743  			}
   744  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   745  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   746  			}
   747  			if got, want := fi.Size(), int64(len(content)); got != want {
   748  				t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   749  			}
   750  		})
   751  	}
   752  }
   753  
   754  func TestRootLstat(t *testing.T) {
   755  	for _, test := range rootTestCases {
   756  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   757  			const content = "content"
   758  			wantError := test.wantError
   759  			if test.ltarget != "" {
   760  				// Lstat will stat the final link, rather than following it.
   761  				wantError = false
   762  			} else if target != "" {
   763  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   764  					t.Fatal(err)
   765  				}
   766  			}
   767  
   768  			fi, err := root.Lstat(test.open)
   769  			if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) {
   770  				return
   771  			}
   772  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   773  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   774  			}
   775  			if test.ltarget == "" {
   776  				if got := fi.Mode(); got&os.ModeSymlink != 0 {
   777  					t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got)
   778  				}
   779  				if got, want := fi.Size(), int64(len(content)); got != want {
   780  					t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   781  				}
   782  			} else {
   783  				if got := fi.Mode(); got&os.ModeSymlink == 0 {
   784  					t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got)
   785  				}
   786  			}
   787  		})
   788  	}
   789  }
   790  
   791  func TestRootReadlink(t *testing.T) {
   792  	for _, test := range rootTestCases {
   793  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   794  			const content = "content"
   795  			wantError := test.wantError
   796  			if test.ltarget != "" {
   797  				// Readlink will read the final link, rather than following it.
   798  				wantError = false
   799  			} else {
   800  				// Readlink fails on non-link targets.
   801  				wantError = true
   802  			}
   803  
   804  			got, err := root.Readlink(test.open)
   805  			if errEndsTest(t, err, wantError, "root.Readlink(%q)", test.open) {
   806  				return
   807  			}
   808  
   809  			want, err := os.Readlink(filepath.Join(root.Name(), test.ltarget))
   810  			if err != nil {
   811  				t.Fatalf("os.Readlink(%q) = %v, want success", test.ltarget, err)
   812  			}
   813  			if got != want {
   814  				t.Errorf("root.Readlink(%q) = %q, want %q", test.open, got, want)
   815  			}
   816  		})
   817  	}
   818  }
   819  
   820  // TestRootRenameFrom tests renaming the test case target to a known-good path.
   821  func TestRootRenameFrom(t *testing.T) {
   822  	testRootMoveFrom(t, true)
   823  }
   824  
   825  // TestRootRenameFrom tests linking the test case target to a known-good path.
   826  func TestRootLinkFrom(t *testing.T) {
   827  	testenv.MustHaveLink(t)
   828  	testRootMoveFrom(t, false)
   829  }
   830  
   831  func testRootMoveFrom(t *testing.T, rename bool) {
   832  	want := []byte("target")
   833  	for _, test := range rootTestCases {
   834  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   835  			if target != "" {
   836  				if err := os.WriteFile(target, want, 0o666); err != nil {
   837  					t.Fatal(err)
   838  				}
   839  			}
   840  			wantError := test.wantError
   841  			var linkTarget string
   842  			if test.ltarget != "" {
   843  				// Rename will rename the link, not the file linked to.
   844  				wantError = false
   845  				var err error
   846  				linkTarget, err = root.Readlink(test.ltarget)
   847  				if err != nil {
   848  					t.Fatalf("root.Readlink(%q) = %v, want success", test.ltarget, err)
   849  				}
   850  
   851  				// When GOOS=js, creating a hard link to a symlink fails.
   852  				if !rename && runtime.GOOS == "js" {
   853  					wantError = true
   854  				}
   855  
   856  				// Windows allows creating a hard link to a file symlink,
   857  				// but not to a directory symlink.
   858  				//
   859  				// This uses os.Stat to check the link target, because this
   860  				// is easier than figuring out whether the link itself is a
   861  				// directory link. The link was created with os.Symlink,
   862  				// which creates directory links when the target is a directory,
   863  				// so this is good enough for a test.
   864  				if !rename && runtime.GOOS == "windows" {
   865  					st, err := os.Stat(filepath.Join(root.Name(), test.ltarget))
   866  					if err == nil && st.IsDir() {
   867  						wantError = true
   868  					}
   869  				}
   870  			}
   871  
   872  			const dstPath = "destination"
   873  
   874  			// Plan 9 doesn't allow cross-directory renames.
   875  			if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
   876  				wantError = true
   877  			}
   878  
   879  			var op string
   880  			var err error
   881  			if rename {
   882  				op = "Rename"
   883  				err = root.Rename(test.open, dstPath)
   884  			} else {
   885  				op = "Link"
   886  				err = root.Link(test.open, dstPath)
   887  			}
   888  			if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, test.open, dstPath) {
   889  				return
   890  			}
   891  
   892  			origPath := target
   893  			if test.ltarget != "" {
   894  				origPath = filepath.Join(root.Name(), test.ltarget)
   895  			}
   896  			_, err = os.Lstat(origPath)
   897  			if rename {
   898  				if !errors.Is(err, os.ErrNotExist) {
   899  					t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", origPath, err)
   900  				}
   901  			} else {
   902  				if err != nil {
   903  					t.Errorf("after linking file, error accessing original: %v", err)
   904  				}
   905  			}
   906  
   907  			dstFullPath := filepath.Join(root.Name(), dstPath)
   908  			if test.ltarget != "" {
   909  				got, err := os.Readlink(dstFullPath)
   910  				if err != nil || got != linkTarget {
   911  					t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstFullPath, got, err, linkTarget)
   912  				}
   913  			} else {
   914  				got, err := os.ReadFile(dstFullPath)
   915  				if err != nil || !bytes.Equal(got, want) {
   916  					t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstFullPath, string(got), err, string(want))
   917  				}
   918  				st, err := os.Lstat(dstFullPath)
   919  				if err != nil || st.Mode()&fs.ModeSymlink != 0 {
   920  					t.Errorf(`os.Lstat(%q) = %v, %v; want non-symlink`, dstFullPath, st.Mode(), err)
   921  				}
   922  
   923  			}
   924  		})
   925  	}
   926  }
   927  
   928  // TestRootRenameTo tests renaming a known-good path to the test case target.
   929  func TestRootRenameTo(t *testing.T) {
   930  	testRootMoveTo(t, true)
   931  }
   932  
   933  // TestRootLinkTo tests renaming a known-good path to the test case target.
   934  func TestRootLinkTo(t *testing.T) {
   935  	testenv.MustHaveLink(t)
   936  	testRootMoveTo(t, true)
   937  }
   938  
   939  func testRootMoveTo(t *testing.T, rename bool) {
   940  	want := []byte("target")
   941  	for _, test := range rootTestCases {
   942  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   943  			const srcPath = "source"
   944  			if err := os.WriteFile(filepath.Join(root.Name(), srcPath), want, 0o666); err != nil {
   945  				t.Fatal(err)
   946  			}
   947  
   948  			target = test.target
   949  			wantError := test.wantError
   950  			if test.ltarget != "" {
   951  				// Rename will overwrite the final link rather than follow it.
   952  				target = test.ltarget
   953  				wantError = false
   954  			}
   955  
   956  			// Plan 9 doesn't allow cross-directory renames.
   957  			if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
   958  				wantError = true
   959  			}
   960  
   961  			var err error
   962  			var op string
   963  			if rename {
   964  				op = "Rename"
   965  				err = root.Rename(srcPath, test.open)
   966  			} else {
   967  				op = "Link"
   968  				err = root.Link(srcPath, test.open)
   969  			}
   970  			if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, srcPath, test.open) {
   971  				return
   972  			}
   973  
   974  			_, err = os.Lstat(filepath.Join(root.Name(), srcPath))
   975  			if rename {
   976  				if !errors.Is(err, os.ErrNotExist) {
   977  					t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", srcPath, err)
   978  				}
   979  			} else {
   980  				if err != nil {
   981  					t.Errorf("after linking file, error accessing original: %v", err)
   982  				}
   983  			}
   984  
   985  			got, err := os.ReadFile(filepath.Join(root.Name(), target))
   986  			if err != nil || !bytes.Equal(got, want) {
   987  				t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, target, string(got), err, string(want))
   988  			}
   989  		})
   990  	}
   991  }
   992  
   993  func TestRootSymlink(t *testing.T) {
   994  	testenv.MustHaveSymlink(t)
   995  	for _, test := range rootTestCases {
   996  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   997  			wantError := test.wantError
   998  			if test.ltarget != "" {
   999  				// We can't create a symlink over an existing symlink.
  1000  				wantError = true
  1001  			}
  1002  
  1003  			const wantTarget = "linktarget"
  1004  			err := root.Symlink(wantTarget, test.open)
  1005  			if errEndsTest(t, err, wantError, "root.Symlink(%q)", test.open) {
  1006  				return
  1007  			}
  1008  			got, err := os.Readlink(target)
  1009  			if err != nil || got != wantTarget {
  1010  				t.Fatalf("ReadLink(%q) = %q, %v; want %q, nil", target, got, err, wantTarget)
  1011  			}
  1012  		})
  1013  	}
  1014  }
  1015  
  1016  // A rootConsistencyTest is a test case comparing os.Root behavior with
  1017  // the corresponding non-Root function.
  1018  //
  1019  // These tests verify that, for example, Root.Open("file/./") and os.Open("file/./")
  1020  // have the same result, although the specific result may vary by platform.
  1021  type rootConsistencyTest struct {
  1022  	name string
  1023  
  1024  	// fs is the test filesystem layout. See makefs above.
  1025  	// fsFunc is called to modify the test filesystem, or replace it.
  1026  	fs     []string
  1027  	fsFunc func(t *testing.T, dir string) string
  1028  
  1029  	// open is the filename to access in the test.
  1030  	open string
  1031  
  1032  	// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
  1033  	// function return different errors for this test.
  1034  	detailedErrorMismatch func(t *testing.T) bool
  1035  
  1036  	// check is called before the test starts, and may t.Skip if necessary.
  1037  	check func(t *testing.T)
  1038  }
  1039  
  1040  var rootConsistencyTestCases = []rootConsistencyTest{{
  1041  	name: "file",
  1042  	fs: []string{
  1043  		"target",
  1044  	},
  1045  	open: "target",
  1046  }, {
  1047  	name: "dir slash dot",
  1048  	fs: []string{
  1049  		"target/file",
  1050  	},
  1051  	open: "target/.",
  1052  }, {
  1053  	name: "dot",
  1054  	fs: []string{
  1055  		"file",
  1056  	},
  1057  	open: ".",
  1058  }, {
  1059  	name: "file slash dot",
  1060  	fs: []string{
  1061  		"target",
  1062  	},
  1063  	open: "target/.",
  1064  	detailedErrorMismatch: func(t *testing.T) bool {
  1065  		// FreeBSD returns EPERM in the non-Root case.
  1066  		return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
  1067  	},
  1068  }, {
  1069  	name: "dir slash",
  1070  	fs: []string{
  1071  		"target/file",
  1072  	},
  1073  	open: "target/",
  1074  }, {
  1075  	name: "dot slash",
  1076  	fs: []string{
  1077  		"file",
  1078  	},
  1079  	open: "./",
  1080  }, {
  1081  	name: "file slash",
  1082  	fs: []string{
  1083  		"target",
  1084  	},
  1085  	open: "target/",
  1086  	detailedErrorMismatch: func(t *testing.T) bool {
  1087  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
  1088  		return runtime.GOOS == "js"
  1089  	},
  1090  }, {
  1091  	name: "file in path",
  1092  	fs: []string{
  1093  		"file",
  1094  	},
  1095  	open: "file/target",
  1096  }, {
  1097  	name: "directory in path missing",
  1098  	open: "dir/target",
  1099  }, {
  1100  	name: "target does not exist",
  1101  	open: "target",
  1102  }, {
  1103  	name: "symlink slash",
  1104  	fs: []string{
  1105  		"target/file",
  1106  		"link => target",
  1107  	},
  1108  	open: "link/",
  1109  }, {
  1110  	name: "symlink slash dot",
  1111  	fs: []string{
  1112  		"target/file",
  1113  		"link => target",
  1114  	},
  1115  	open: "link/.",
  1116  }, {
  1117  	name: "file symlink slash",
  1118  	fs: []string{
  1119  		"target",
  1120  		"link => target",
  1121  	},
  1122  	open: "link/",
  1123  	detailedErrorMismatch: func(t *testing.T) bool {
  1124  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
  1125  		return runtime.GOOS == "js"
  1126  	},
  1127  }, {
  1128  	name: "unresolved symlink",
  1129  	fs: []string{
  1130  		"link => target",
  1131  	},
  1132  	open: "link",
  1133  }, {
  1134  	name: "resolved symlink",
  1135  	fs: []string{
  1136  		"link => target",
  1137  		"target",
  1138  	},
  1139  	open: "link",
  1140  }, {
  1141  	name: "dotdot in path after symlink",
  1142  	fs: []string{
  1143  		"a => b/c",
  1144  		"b/c/",
  1145  		"b/target",
  1146  	},
  1147  	open: "a/../target",
  1148  }, {
  1149  	name: "symlink to dir ends in slash",
  1150  	fs: []string{
  1151  		"dir/",
  1152  		"link => dir/",
  1153  	},
  1154  	open: "link",
  1155  }, {
  1156  	name: "symlink to file ends in slash",
  1157  	fs: []string{
  1158  		"file",
  1159  		"link => file/",
  1160  	},
  1161  	open: "link",
  1162  }, {
  1163  	name: "long file name",
  1164  	open: strings.Repeat("a", 500),
  1165  }, {
  1166  	name: "unreadable directory",
  1167  	fs: []string{
  1168  		"dir/target",
  1169  	},
  1170  	fsFunc: func(t *testing.T, dir string) string {
  1171  		os.Chmod(filepath.Join(dir, "dir"), 0)
  1172  		t.Cleanup(func() {
  1173  			os.Chmod(filepath.Join(dir, "dir"), 0o700)
  1174  		})
  1175  		return dir
  1176  	},
  1177  	open: "dir/target",
  1178  }, {
  1179  	name: "unix domain socket target",
  1180  	fsFunc: func(t *testing.T, dir string) string {
  1181  		return tempDirWithUnixSocket(t, "a")
  1182  	},
  1183  	open: "a",
  1184  }, {
  1185  	name: "unix domain socket in path",
  1186  	fsFunc: func(t *testing.T, dir string) string {
  1187  		return tempDirWithUnixSocket(t, "a")
  1188  	},
  1189  	open: "a/b",
  1190  	detailedErrorMismatch: func(t *testing.T) bool {
  1191  		// On Windows, os.Root.Open returns "The directory name is invalid."
  1192  		// and os.Open returns "The file cannot be accessed by the system.".
  1193  		return runtime.GOOS == "windows"
  1194  	},
  1195  	check: func(t *testing.T) {
  1196  		if runtime.GOOS == "windows" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemoveAll/") {
  1197  			// Root.RemoveAll notices that a/ is not a directory,
  1198  			// and returns success.
  1199  			// os.RemoveAll tries to open a/ and fails because
  1200  			// it is not a regular file.
  1201  			// The inconsistency here isn't worth fixing, so just skip this test.
  1202  			t.Skip("known inconsistency on windows")
  1203  		}
  1204  	},
  1205  }, {
  1206  	name: "question mark",
  1207  	open: "?",
  1208  }, {
  1209  	name: "nul byte",
  1210  	open: "\x00",
  1211  }}
  1212  
  1213  func tempDirWithUnixSocket(t *testing.T, name string) string {
  1214  	dir, err := os.MkdirTemp("", "")
  1215  	if err != nil {
  1216  		t.Fatal(err)
  1217  	}
  1218  	t.Cleanup(func() {
  1219  		if err := os.RemoveAll(dir); err != nil {
  1220  			t.Error(err)
  1221  		}
  1222  	})
  1223  	addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name))
  1224  	if err != nil {
  1225  		t.Skipf("net.ResolveUnixAddr: %v", err)
  1226  	}
  1227  	conn, err := net.ListenUnix("unix", addr)
  1228  	if err != nil {
  1229  		t.Skipf("net.ListenUnix: %v", err)
  1230  	}
  1231  	t.Cleanup(func() {
  1232  		conn.Close()
  1233  	})
  1234  	return dir
  1235  }
  1236  
  1237  func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) {
  1238  	if runtime.GOOS == "wasip1" {
  1239  		// On wasip, non-Root functions clean paths before opening them,
  1240  		// resulting in inconsistent behavior.
  1241  		// https://go.dev/issue/69509
  1242  		t.Skip("#69509: inconsistent results on wasip1")
  1243  	}
  1244  
  1245  	t.Run(test.name, func(t *testing.T) {
  1246  		if test.check != nil {
  1247  			test.check(t)
  1248  		}
  1249  
  1250  		dir1 := makefs(t, test.fs)
  1251  		dir2 := makefs(t, test.fs)
  1252  		if test.fsFunc != nil {
  1253  			dir1 = test.fsFunc(t, dir1)
  1254  			dir2 = test.fsFunc(t, dir2)
  1255  		}
  1256  
  1257  		r, err := os.OpenRoot(dir1)
  1258  		if err != nil {
  1259  			t.Fatal(err)
  1260  		}
  1261  		defer r.Close()
  1262  
  1263  		res1, err1 := f(t, test.open, r)
  1264  		res2, err2 := f(t, dir2+"/"+test.open, nil)
  1265  
  1266  		if res1 != res2 || ((err1 == nil) != (err2 == nil)) {
  1267  			t.Errorf("with root:    res=%v", res1)
  1268  			t.Errorf("              err=%v", err1)
  1269  			t.Errorf("without root: res=%v", res2)
  1270  			t.Errorf("              err=%v", err2)
  1271  			t.Errorf("want consistent results, got mismatch")
  1272  		}
  1273  
  1274  		if err1 != nil || err2 != nil {
  1275  			underlyingError := func(how string, err error) error {
  1276  				switch e := err1.(type) {
  1277  				case *os.PathError:
  1278  					return e.Err
  1279  				case *os.LinkError:
  1280  					return e.Err
  1281  				default:
  1282  					t.Fatalf("%v, expected PathError or LinkError; got: %v", how, err)
  1283  				}
  1284  				return nil
  1285  			}
  1286  			e1 := underlyingError("with root", err1)
  1287  			e2 := underlyingError("without root", err1)
  1288  			detailedErrorMismatch := false
  1289  			if f := test.detailedErrorMismatch; f != nil {
  1290  				detailedErrorMismatch = f(t)
  1291  			}
  1292  			if runtime.GOOS == "plan9" {
  1293  				// Plan9 syscall errors aren't comparable.
  1294  				detailedErrorMismatch = true
  1295  			}
  1296  			if !detailedErrorMismatch && e1 != e2 {
  1297  				t.Errorf("with root:    err=%v", e1)
  1298  				t.Errorf("without root: err=%v", e2)
  1299  				t.Errorf("want consistent results, got mismatch")
  1300  			}
  1301  		}
  1302  	})
  1303  }
  1304  
  1305  func TestRootConsistencyOpen(t *testing.T) {
  1306  	for _, test := range rootConsistencyTestCases {
  1307  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1308  			var f *os.File
  1309  			var err error
  1310  			if r == nil {
  1311  				f, err = os.Open(path)
  1312  			} else {
  1313  				f, err = r.Open(path)
  1314  			}
  1315  			if err != nil {
  1316  				return "", err
  1317  			}
  1318  			defer f.Close()
  1319  			fi, err := f.Stat()
  1320  			if err == nil && !fi.IsDir() {
  1321  				b, err := io.ReadAll(f)
  1322  				return string(b), err
  1323  			} else {
  1324  				names, err := f.Readdirnames(-1)
  1325  				slices.Sort(names)
  1326  				return fmt.Sprintf("%q", names), err
  1327  			}
  1328  		})
  1329  	}
  1330  }
  1331  
  1332  func TestRootConsistencyCreate(t *testing.T) {
  1333  	for _, test := range rootConsistencyTestCases {
  1334  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1335  			var f *os.File
  1336  			var err error
  1337  			if r == nil {
  1338  				f, err = os.Create(path)
  1339  			} else {
  1340  				f, err = r.Create(path)
  1341  			}
  1342  			if err == nil {
  1343  				f.Write([]byte("file contents"))
  1344  				f.Close()
  1345  			}
  1346  			return "", err
  1347  		})
  1348  	}
  1349  }
  1350  
  1351  func TestRootConsistencyChmod(t *testing.T) {
  1352  	if runtime.GOOS == "wasip1" {
  1353  		t.Skip("Chmod not supported on " + runtime.GOOS)
  1354  	}
  1355  	for _, test := range rootConsistencyTestCases {
  1356  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1357  			chmod := os.Chmod
  1358  			lstat := os.Lstat
  1359  			if r != nil {
  1360  				chmod = r.Chmod
  1361  				lstat = r.Lstat
  1362  			}
  1363  
  1364  			var m1, m2 os.FileMode
  1365  			if err := chmod(path, 0o555); err != nil {
  1366  				return "chmod 0o555", err
  1367  			}
  1368  			fi, err := lstat(path)
  1369  			if err == nil {
  1370  				m1 = fi.Mode()
  1371  			}
  1372  			if err = chmod(path, 0o777); err != nil {
  1373  				return "chmod 0o777", err
  1374  			}
  1375  			fi, err = lstat(path)
  1376  			if err == nil {
  1377  				m2 = fi.Mode()
  1378  			}
  1379  			return fmt.Sprintf("%v %v", m1, m2), err
  1380  		})
  1381  	}
  1382  }
  1383  
  1384  func TestRootConsistencyMkdir(t *testing.T) {
  1385  	for _, test := range rootConsistencyTestCases {
  1386  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1387  			var err error
  1388  			if r == nil {
  1389  				err = os.Mkdir(path, 0o777)
  1390  			} else {
  1391  				err = r.Mkdir(path, 0o777)
  1392  			}
  1393  			return "", err
  1394  		})
  1395  	}
  1396  }
  1397  
  1398  func TestRootConsistencyMkdirAll(t *testing.T) {
  1399  	for _, test := range rootConsistencyTestCases {
  1400  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1401  			var err error
  1402  			if r == nil {
  1403  				err = os.MkdirAll(path, 0o777)
  1404  			} else {
  1405  				err = r.MkdirAll(path, 0o777)
  1406  			}
  1407  			return "", err
  1408  		})
  1409  	}
  1410  }
  1411  
  1412  func TestRootConsistencyRemove(t *testing.T) {
  1413  	for _, test := range rootConsistencyTestCases {
  1414  		if test.open == "." || test.open == "./" {
  1415  			continue // can't remove the root itself
  1416  		}
  1417  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1418  			var err error
  1419  			if r == nil {
  1420  				err = os.Remove(path)
  1421  			} else {
  1422  				err = r.Remove(path)
  1423  			}
  1424  			return "", err
  1425  		})
  1426  	}
  1427  }
  1428  
  1429  func TestRootConsistencyRemoveAll(t *testing.T) {
  1430  	for _, test := range rootConsistencyTestCases {
  1431  		if test.open == "." || test.open == "./" {
  1432  			continue // can't remove the root itself
  1433  		}
  1434  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1435  			var err error
  1436  			if r == nil {
  1437  				err = os.RemoveAll(path)
  1438  			} else {
  1439  				err = r.RemoveAll(path)
  1440  			}
  1441  			return "", err
  1442  		})
  1443  	}
  1444  }
  1445  
  1446  func TestRootConsistencyStat(t *testing.T) {
  1447  	for _, test := range rootConsistencyTestCases {
  1448  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1449  			var fi os.FileInfo
  1450  			var err error
  1451  			if r == nil {
  1452  				fi, err = os.Stat(path)
  1453  			} else {
  1454  				fi, err = r.Stat(path)
  1455  			}
  1456  			if err != nil {
  1457  				return "", err
  1458  			}
  1459  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1460  		})
  1461  	}
  1462  }
  1463  
  1464  func TestRootConsistencyLstat(t *testing.T) {
  1465  	for _, test := range rootConsistencyTestCases {
  1466  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1467  			var fi os.FileInfo
  1468  			var err error
  1469  			if r == nil {
  1470  				fi, err = os.Lstat(path)
  1471  			} else {
  1472  				fi, err = r.Lstat(path)
  1473  			}
  1474  			if err != nil {
  1475  				return "", err
  1476  			}
  1477  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1478  		})
  1479  	}
  1480  }
  1481  
  1482  func TestRootConsistencyReadlink(t *testing.T) {
  1483  	for _, test := range rootConsistencyTestCases {
  1484  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1485  			if r == nil {
  1486  				return os.Readlink(path)
  1487  			} else {
  1488  				return r.Readlink(path)
  1489  			}
  1490  		})
  1491  	}
  1492  }
  1493  
  1494  func TestRootConsistencyRename(t *testing.T) {
  1495  	testRootConsistencyMove(t, true)
  1496  }
  1497  
  1498  func TestRootConsistencyLink(t *testing.T) {
  1499  	testenv.MustHaveLink(t)
  1500  	testRootConsistencyMove(t, false)
  1501  }
  1502  
  1503  func testRootConsistencyMove(t *testing.T, rename bool) {
  1504  	if runtime.GOOS == "plan9" {
  1505  		// This test depends on moving files between directories.
  1506  		t.Skip("Plan 9 does not support cross-directory renames")
  1507  	}
  1508  	// Run this test in two directions:
  1509  	// Renaming the test path to a known-good path (from),
  1510  	// and renaming a known-good path to the test path (to).
  1511  	for _, name := range []string{"from", "to"} {
  1512  		t.Run(name, func(t *testing.T) {
  1513  			for _, test := range rootConsistencyTestCases {
  1514  				if runtime.GOOS == "windows" {
  1515  					// On Windows, Rename("/path/to/.", x) succeeds,
  1516  					// because Windows cleans the path to just "/path/to".
  1517  					// Root.Rename(".", x) fails as expected.
  1518  					// Don't run this consistency test on Windows.
  1519  					if test.open == "." || test.open == "./" {
  1520  						continue
  1521  					}
  1522  				}
  1523  
  1524  				test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1525  					var move func(oldname, newname string) error
  1526  					switch {
  1527  					case rename && r == nil:
  1528  						move = os.Rename
  1529  					case rename && r != nil:
  1530  						move = r.Rename
  1531  					case !rename && r == nil:
  1532  						move = os.Link
  1533  					case !rename && r != nil:
  1534  						move = r.Link
  1535  					}
  1536  					lstat := os.Lstat
  1537  					if r != nil {
  1538  						lstat = r.Lstat
  1539  					}
  1540  
  1541  					otherPath := "other"
  1542  					if r == nil {
  1543  						otherPath = filepath.Join(t.TempDir(), otherPath)
  1544  					}
  1545  
  1546  					var srcPath, dstPath string
  1547  					if name == "from" {
  1548  						srcPath = path
  1549  						dstPath = otherPath
  1550  					} else {
  1551  						srcPath = otherPath
  1552  						dstPath = path
  1553  					}
  1554  
  1555  					if !rename {
  1556  						// When the source is a symlink, Root.Link creates
  1557  						// a hard link to the symlink.
  1558  						// os.Link does whatever the link syscall does,
  1559  						// which varies between operating systems and
  1560  						// their versions.
  1561  						// Skip running the consistency test when
  1562  						// the source is a symlink.
  1563  						fi, err := lstat(srcPath)
  1564  						if err == nil && fi.Mode()&os.ModeSymlink != 0 {
  1565  							return "", nil
  1566  						}
  1567  					}
  1568  
  1569  					if err := move(srcPath, dstPath); err != nil {
  1570  						return "", err
  1571  					}
  1572  					fi, err := lstat(dstPath)
  1573  					if err != nil {
  1574  						t.Errorf("stat(%q) after successful copy: %v", dstPath, err)
  1575  						return "stat error", err
  1576  					}
  1577  					return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1578  				})
  1579  			}
  1580  		})
  1581  	}
  1582  }
  1583  
  1584  func TestRootConsistencySymlink(t *testing.T) {
  1585  	testenv.MustHaveSymlink(t)
  1586  	for _, test := range rootConsistencyTestCases {
  1587  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1588  			const target = "linktarget"
  1589  			var err error
  1590  			var got string
  1591  			if r == nil {
  1592  				err = os.Symlink(target, path)
  1593  				got, _ = os.Readlink(target)
  1594  			} else {
  1595  				err = r.Symlink(target, path)
  1596  				got, _ = r.Readlink(target)
  1597  			}
  1598  			return got, err
  1599  		})
  1600  	}
  1601  }
  1602  
  1603  func TestRootRenameAfterOpen(t *testing.T) {
  1604  	switch runtime.GOOS {
  1605  	case "windows":
  1606  		t.Skip("renaming open files not supported on " + runtime.GOOS)
  1607  	case "js", "plan9":
  1608  		t.Skip("openat not supported on " + runtime.GOOS)
  1609  	case "wasip1":
  1610  		if os.Getenv("GOWASIRUNTIME") == "wazero" {
  1611  			t.Skip("wazero does not track renamed directories")
  1612  		}
  1613  	}
  1614  
  1615  	dir := t.TempDir()
  1616  
  1617  	// Create directory "a" and open it.
  1618  	if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil {
  1619  		t.Fatal(err)
  1620  	}
  1621  	dirf, err := os.OpenRoot(filepath.Join(dir, "a"))
  1622  	if err != nil {
  1623  		t.Fatal(err)
  1624  	}
  1625  	defer dirf.Close()
  1626  
  1627  	// Rename "a" => "b", and create "b/f".
  1628  	if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil {
  1629  		t.Fatal(err)
  1630  	}
  1631  	if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil {
  1632  		t.Fatal(err)
  1633  	}
  1634  
  1635  	// Open "f", and confirm that we see it.
  1636  	f, err := dirf.OpenFile("f", os.O_RDONLY, 0)
  1637  	if err != nil {
  1638  		t.Fatalf("reading file after renaming parent: %v", err)
  1639  	}
  1640  	defer f.Close()
  1641  	b, err := io.ReadAll(f)
  1642  	if err != nil {
  1643  		t.Fatal(err)
  1644  	}
  1645  	if got, want := string(b), "hello"; got != want {
  1646  		t.Fatalf("file contents: %q, want %q", got, want)
  1647  	}
  1648  
  1649  	// f.Name reflects the original path we opened the directory under (".../a"), not "b".
  1650  	if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want {
  1651  		t.Errorf("f.Name() = %q, want %q", got, want)
  1652  	}
  1653  }
  1654  
  1655  func TestRootNonPermissionMode(t *testing.T) {
  1656  	r, err := os.OpenRoot(t.TempDir())
  1657  	if err != nil {
  1658  		t.Fatal(err)
  1659  	}
  1660  	defer r.Close()
  1661  	if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil {
  1662  		t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error")
  1663  	}
  1664  	if err := r.Mkdir("file", 0o1777); err == nil {
  1665  		t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error")
  1666  	}
  1667  }
  1668  
  1669  func TestRootUseAfterClose(t *testing.T) {
  1670  	r, err := os.OpenRoot(t.TempDir())
  1671  	if err != nil {
  1672  		t.Fatal(err)
  1673  	}
  1674  	r.Close()
  1675  	for _, test := range []struct {
  1676  		name string
  1677  		f    func(r *os.Root, filename string) error
  1678  	}{{
  1679  		name: "Open",
  1680  		f: func(r *os.Root, filename string) error {
  1681  			_, err := r.Open(filename)
  1682  			return err
  1683  		},
  1684  	}, {
  1685  		name: "Create",
  1686  		f: func(r *os.Root, filename string) error {
  1687  			_, err := r.Create(filename)
  1688  			return err
  1689  		},
  1690  	}, {
  1691  		name: "OpenFile",
  1692  		f: func(r *os.Root, filename string) error {
  1693  			_, err := r.OpenFile(filename, os.O_RDWR, 0o666)
  1694  			return err
  1695  		},
  1696  	}, {
  1697  		name: "OpenRoot",
  1698  		f: func(r *os.Root, filename string) error {
  1699  			_, err := r.OpenRoot(filename)
  1700  			return err
  1701  		},
  1702  	}, {
  1703  		name: "Mkdir",
  1704  		f: func(r *os.Root, filename string) error {
  1705  			return r.Mkdir(filename, 0o777)
  1706  		},
  1707  	}} {
  1708  		err := test.f(r, "target")
  1709  		pe, ok := err.(*os.PathError)
  1710  		if !ok || pe.Path != "target" || pe.Err != os.ErrClosed {
  1711  			t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err)
  1712  		}
  1713  	}
  1714  }
  1715  
  1716  func TestRootConcurrentClose(t *testing.T) {
  1717  	r, err := os.OpenRoot(t.TempDir())
  1718  	if err != nil {
  1719  		t.Fatal(err)
  1720  	}
  1721  	ch := make(chan error, 1)
  1722  	go func() {
  1723  		defer close(ch)
  1724  		first := true
  1725  		for {
  1726  			f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666)
  1727  			if err != nil {
  1728  				ch <- err
  1729  				return
  1730  			}
  1731  			if first {
  1732  				ch <- nil
  1733  				first = false
  1734  			}
  1735  			f.Close()
  1736  			if runtime.GOARCH == "wasm" {
  1737  				// TODO(go.dev/issue/71134) can lead to goroutine starvation.
  1738  				runtime.Gosched()
  1739  			}
  1740  		}
  1741  	}()
  1742  	if err := <-ch; err != nil {
  1743  		t.Errorf("OpenFile: %v, want success", err)
  1744  	}
  1745  	r.Close()
  1746  	if err := <-ch; !errors.Is(err, os.ErrClosed) {
  1747  		t.Errorf("OpenFile: %v, want ErrClosed", err)
  1748  	}
  1749  }
  1750  
  1751  // TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse.
  1752  //
  1753  // We create a deeply nested directory:
  1754  //
  1755  //	base/a/a/a/a/ [...] /a
  1756  //
  1757  // And a path that descends into the tree, then returns to the top using ..:
  1758  //
  1759  //	base/a/a/a/a/ [...] /a/../../../ [..] /../a/f
  1760  //
  1761  // While opening this file, we rename base/a/a to base/b.
  1762  // A naive lookup operation will resolve the path to base/f.
  1763  func TestRootRaceRenameDir(t *testing.T) {
  1764  	dir := t.TempDir()
  1765  	r, err := os.OpenRoot(dir)
  1766  	if err != nil {
  1767  		t.Fatal(err)
  1768  	}
  1769  	defer r.Close()
  1770  
  1771  	const depth = 4
  1772  
  1773  	os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777)
  1774  
  1775  	path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f"
  1776  	os.WriteFile(dir+"/f", []byte("secret"), 0o666)
  1777  	os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666)
  1778  
  1779  	// Compute how long it takes to open the path in the common case.
  1780  	const tries = 10
  1781  	var total time.Duration
  1782  	for range tries {
  1783  		start := time.Now()
  1784  		f, err := r.Open(path)
  1785  		if err != nil {
  1786  			t.Fatal(err)
  1787  		}
  1788  		b, err := io.ReadAll(f)
  1789  		if err != nil {
  1790  			t.Fatal(err)
  1791  		}
  1792  		if string(b) != "public" {
  1793  			t.Fatalf("read %q, want %q", b, "public")
  1794  		}
  1795  		f.Close()
  1796  		total += time.Since(start)
  1797  	}
  1798  	avg := total / tries
  1799  
  1800  	// We're trying to exploit a race, so try this a number of times.
  1801  	for range 100 {
  1802  		// Start a goroutine to open the file.
  1803  		gotc := make(chan []byte)
  1804  		go func() {
  1805  			f, err := r.Open(path)
  1806  			if err != nil {
  1807  				gotc <- nil
  1808  			}
  1809  			defer f.Close()
  1810  			b, _ := io.ReadAll(f)
  1811  			gotc <- b
  1812  		}()
  1813  
  1814  		// Wait for the open operation to partially complete,
  1815  		// and then rename a directory near the root.
  1816  		time.Sleep(avg / 4)
  1817  		if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil {
  1818  			// Windows and Plan9 won't let us rename a directory if we have
  1819  			// an open handle for it, so an error here is expected.
  1820  			switch runtime.GOOS {
  1821  			case "windows", "plan9":
  1822  			default:
  1823  				t.Fatal(err)
  1824  			}
  1825  		}
  1826  
  1827  		got := <-gotc
  1828  		os.Rename(dir+"/b", dir+"/base/a")
  1829  		if len(got) > 0 && string(got) != "public" {
  1830  			t.Errorf("read file: %q; want error or 'public'", got)
  1831  		}
  1832  	}
  1833  }
  1834  
  1835  func TestRootSymlinkToRoot(t *testing.T) {
  1836  	dir := makefs(t, []string{
  1837  		"d/d => ..",
  1838  	})
  1839  	root, err := os.OpenRoot(dir)
  1840  	if err != nil {
  1841  		t.Fatal(err)
  1842  	}
  1843  	defer root.Close()
  1844  	if err := root.Mkdir("d/d/new", 0777); err != nil {
  1845  		t.Fatal(err)
  1846  	}
  1847  	f, err := root.Open("d/d")
  1848  	if err != nil {
  1849  		t.Fatal(err)
  1850  	}
  1851  	defer f.Close()
  1852  	names, err := f.Readdirnames(-1)
  1853  	if err != nil {
  1854  		t.Fatal(err)
  1855  	}
  1856  	slices.Sort(names)
  1857  	if got, want := names, []string{"d", "new"}; !slices.Equal(got, want) {
  1858  		t.Errorf("root contains: %q, want %q", got, want)
  1859  	}
  1860  }
  1861  
  1862  func TestOpenInRoot(t *testing.T) {
  1863  	dir := makefs(t, []string{
  1864  		"file",
  1865  		"link => ../ROOT/file",
  1866  	})
  1867  	f, err := os.OpenInRoot(dir, "file")
  1868  	if err != nil {
  1869  		t.Fatalf("OpenInRoot(`file`) = %v, want success", err)
  1870  	}
  1871  	f.Close()
  1872  	for _, name := range []string{
  1873  		"link",
  1874  		"../ROOT/file",
  1875  		dir + "/file",
  1876  	} {
  1877  		f, err := os.OpenInRoot(dir, name)
  1878  		if err == nil {
  1879  			f.Close()
  1880  			t.Fatalf("OpenInRoot(%q) = nil, want error", name)
  1881  		}
  1882  	}
  1883  }
  1884  
  1885  func TestRootRemoveDot(t *testing.T) {
  1886  	dir := t.TempDir()
  1887  	root, err := os.OpenRoot(dir)
  1888  	if err != nil {
  1889  		t.Fatal(err)
  1890  	}
  1891  	defer root.Close()
  1892  	if err := root.Remove("."); err == nil {
  1893  		t.Errorf(`root.Remove(".") = %v, want error`, err)
  1894  	}
  1895  	if err := root.RemoveAll("."); err == nil {
  1896  		t.Errorf(`root.RemoveAll(".") = %v, want error`, err)
  1897  	}
  1898  	if _, err := os.Stat(dir); err != nil {
  1899  		t.Error(`root.Remove(All)?(".") removed the root`)
  1900  	}
  1901  }
  1902  
  1903  func TestRootWriteReadFile(t *testing.T) {
  1904  	dir := t.TempDir()
  1905  	root, err := os.OpenRoot(dir)
  1906  	if err != nil {
  1907  		t.Fatal(err)
  1908  	}
  1909  	defer root.Close()
  1910  
  1911  	name := "filename"
  1912  	want := []byte("file contents")
  1913  	if err := root.WriteFile(name, want, 0o666); err != nil {
  1914  		t.Fatalf("root.WriteFile(%q, %q, 0o666) = %v; want nil", name, want, err)
  1915  	}
  1916  
  1917  	got, err := root.ReadFile(name)
  1918  	if err != nil {
  1919  		t.Fatalf("root.ReadFile(%q) = %q, %v; want %q, nil", name, got, err, want)
  1920  	}
  1921  }
  1922  

View as plain text