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 TestRootOpenRoot(t *testing.T) {
   555  	for _, test := range rootTestCases {
   556  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   557  			if target != "" {
   558  				if err := os.Mkdir(target, 0o777); err != nil {
   559  					t.Fatal(err)
   560  				}
   561  				if err := os.WriteFile(target+"/f", nil, 0o666); err != nil {
   562  					t.Fatal(err)
   563  				}
   564  			}
   565  			rr, err := root.OpenRoot(test.open)
   566  			if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) {
   567  				return
   568  			}
   569  			defer rr.Close()
   570  			f, err := rr.Open("f")
   571  			if err != nil {
   572  				t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err)
   573  			}
   574  			f.Close()
   575  		})
   576  	}
   577  }
   578  
   579  func TestRootRemoveFile(t *testing.T) {
   580  	for _, test := range rootTestCases {
   581  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   582  			wantError := test.wantError
   583  			if test.ltarget != "" {
   584  				// Remove doesn't follow symlinks in the final path component,
   585  				// so it will successfully remove ltarget.
   586  				wantError = false
   587  				target = filepath.Join(root.Name(), test.ltarget)
   588  			} else if target != "" {
   589  				if err := os.WriteFile(target, nil, 0o666); err != nil {
   590  					t.Fatal(err)
   591  				}
   592  			}
   593  
   594  			err := root.Remove(test.open)
   595  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   596  				return
   597  			}
   598  			_, err = os.Lstat(target)
   599  			if !errors.Is(err, os.ErrNotExist) {
   600  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   601  			}
   602  		})
   603  	}
   604  }
   605  
   606  func TestRootRemoveDirectory(t *testing.T) {
   607  	for _, test := range rootTestCases {
   608  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   609  			wantError := test.wantError
   610  			if test.ltarget != "" {
   611  				// Remove doesn't follow symlinks in the final path component,
   612  				// so it will successfully remove ltarget.
   613  				wantError = false
   614  				target = filepath.Join(root.Name(), test.ltarget)
   615  			} else if target != "" {
   616  				if err := os.Mkdir(target, 0o777); err != nil {
   617  					t.Fatal(err)
   618  				}
   619  			}
   620  
   621  			err := root.Remove(test.open)
   622  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   623  				return
   624  			}
   625  			_, err = os.Lstat(target)
   626  			if !errors.Is(err, os.ErrNotExist) {
   627  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   628  			}
   629  		})
   630  	}
   631  }
   632  
   633  func TestRootOpenFileAsRoot(t *testing.T) {
   634  	dir := t.TempDir()
   635  	target := filepath.Join(dir, "target")
   636  	if err := os.WriteFile(target, nil, 0o666); err != nil {
   637  		t.Fatal(err)
   638  	}
   639  	r, err := os.OpenRoot(target)
   640  	if err == nil {
   641  		r.Close()
   642  		t.Fatal("os.OpenRoot(file) succeeded; want failure")
   643  	}
   644  	r, err = os.OpenRoot(dir)
   645  	if err != nil {
   646  		t.Fatal(err)
   647  	}
   648  	defer r.Close()
   649  	rr, err := r.OpenRoot("target")
   650  	if err == nil {
   651  		rr.Close()
   652  		t.Fatal("Root.OpenRoot(file) succeeded; want failure")
   653  	}
   654  }
   655  
   656  func TestRootStat(t *testing.T) {
   657  	for _, test := range rootTestCases {
   658  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   659  			const content = "content"
   660  			if target != "" {
   661  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   662  					t.Fatal(err)
   663  				}
   664  			}
   665  
   666  			fi, err := root.Stat(test.open)
   667  			if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) {
   668  				return
   669  			}
   670  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   671  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   672  			}
   673  			if got, want := fi.Size(), int64(len(content)); got != want {
   674  				t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   675  			}
   676  		})
   677  	}
   678  }
   679  
   680  func TestRootLstat(t *testing.T) {
   681  	for _, test := range rootTestCases {
   682  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   683  			const content = "content"
   684  			wantError := test.wantError
   685  			if test.ltarget != "" {
   686  				// Lstat will stat the final link, rather than following it.
   687  				wantError = false
   688  			} else if target != "" {
   689  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   690  					t.Fatal(err)
   691  				}
   692  			}
   693  
   694  			fi, err := root.Lstat(test.open)
   695  			if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) {
   696  				return
   697  			}
   698  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   699  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   700  			}
   701  			if test.ltarget == "" {
   702  				if got := fi.Mode(); got&os.ModeSymlink != 0 {
   703  					t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got)
   704  				}
   705  				if got, want := fi.Size(), int64(len(content)); got != want {
   706  					t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   707  				}
   708  			} else {
   709  				if got := fi.Mode(); got&os.ModeSymlink == 0 {
   710  					t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got)
   711  				}
   712  			}
   713  		})
   714  	}
   715  }
   716  
   717  func TestRootReadlink(t *testing.T) {
   718  	for _, test := range rootTestCases {
   719  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   720  			const content = "content"
   721  			wantError := test.wantError
   722  			if test.ltarget != "" {
   723  				// Readlink will read the final link, rather than following it.
   724  				wantError = false
   725  			} else {
   726  				// Readlink fails on non-link targets.
   727  				wantError = true
   728  			}
   729  
   730  			got, err := root.Readlink(test.open)
   731  			if errEndsTest(t, err, wantError, "root.Readlink(%q)", test.open) {
   732  				return
   733  			}
   734  
   735  			want, err := os.Readlink(filepath.Join(root.Name(), test.ltarget))
   736  			if err != nil {
   737  				t.Fatalf("os.Readlink(%q) = %v, want success", test.ltarget, err)
   738  			}
   739  			if got != want {
   740  				t.Errorf("root.Readlink(%q) = %q, want %q", test.open, got, want)
   741  			}
   742  		})
   743  	}
   744  }
   745  
   746  // TestRootRenameFrom tests renaming the test case target to a known-good path.
   747  func TestRootRenameFrom(t *testing.T) {
   748  	testRootMoveFrom(t, true)
   749  }
   750  
   751  // TestRootRenameFrom tests linking the test case target to a known-good path.
   752  func TestRootLinkFrom(t *testing.T) {
   753  	testenv.MustHaveLink(t)
   754  	testRootMoveFrom(t, false)
   755  }
   756  
   757  func testRootMoveFrom(t *testing.T, rename bool) {
   758  	want := []byte("target")
   759  	for _, test := range rootTestCases {
   760  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   761  			if target != "" {
   762  				if err := os.WriteFile(target, want, 0o666); err != nil {
   763  					t.Fatal(err)
   764  				}
   765  			}
   766  			wantError := test.wantError
   767  			var linkTarget string
   768  			if test.ltarget != "" {
   769  				// Rename will rename the link, not the file linked to.
   770  				wantError = false
   771  				var err error
   772  				linkTarget, err = root.Readlink(test.ltarget)
   773  				if err != nil {
   774  					t.Fatalf("root.Readlink(%q) = %v, want success", test.ltarget, err)
   775  				}
   776  
   777  				// When GOOS=js, creating a hard link to a symlink fails.
   778  				if !rename && runtime.GOOS == "js" {
   779  					wantError = true
   780  				}
   781  
   782  				// Windows allows creating a hard link to a file symlink,
   783  				// but not to a directory symlink.
   784  				//
   785  				// This uses os.Stat to check the link target, because this
   786  				// is easier than figuring out whether the link itself is a
   787  				// directory link. The link was created with os.Symlink,
   788  				// which creates directory links when the target is a directory,
   789  				// so this is good enough for a test.
   790  				if !rename && runtime.GOOS == "windows" {
   791  					st, err := os.Stat(filepath.Join(root.Name(), test.ltarget))
   792  					if err == nil && st.IsDir() {
   793  						wantError = true
   794  					}
   795  				}
   796  			}
   797  
   798  			const dstPath = "destination"
   799  
   800  			// Plan 9 doesn't allow cross-directory renames.
   801  			if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
   802  				wantError = true
   803  			}
   804  
   805  			var op string
   806  			var err error
   807  			if rename {
   808  				op = "Rename"
   809  				err = root.Rename(test.open, dstPath)
   810  			} else {
   811  				op = "Link"
   812  				err = root.Link(test.open, dstPath)
   813  			}
   814  			if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, test.open, dstPath) {
   815  				return
   816  			}
   817  
   818  			origPath := target
   819  			if test.ltarget != "" {
   820  				origPath = filepath.Join(root.Name(), test.ltarget)
   821  			}
   822  			_, err = os.Lstat(origPath)
   823  			if rename {
   824  				if !errors.Is(err, os.ErrNotExist) {
   825  					t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", origPath, err)
   826  				}
   827  			} else {
   828  				if err != nil {
   829  					t.Errorf("after linking file, error accessing original: %v", err)
   830  				}
   831  			}
   832  
   833  			dstFullPath := filepath.Join(root.Name(), dstPath)
   834  			if test.ltarget != "" {
   835  				got, err := os.Readlink(dstFullPath)
   836  				if err != nil || got != linkTarget {
   837  					t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstFullPath, got, err, linkTarget)
   838  				}
   839  			} else {
   840  				got, err := os.ReadFile(dstFullPath)
   841  				if err != nil || !bytes.Equal(got, want) {
   842  					t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstFullPath, string(got), err, string(want))
   843  				}
   844  				st, err := os.Lstat(dstFullPath)
   845  				if err != nil || st.Mode()&fs.ModeSymlink != 0 {
   846  					t.Errorf(`os.Lstat(%q) = %v, %v; want non-symlink`, dstFullPath, st.Mode(), err)
   847  				}
   848  
   849  			}
   850  		})
   851  	}
   852  }
   853  
   854  // TestRootRenameTo tests renaming a known-good path to the test case target.
   855  func TestRootRenameTo(t *testing.T) {
   856  	testRootMoveTo(t, true)
   857  }
   858  
   859  // TestRootLinkTo tests renaming a known-good path to the test case target.
   860  func TestRootLinkTo(t *testing.T) {
   861  	testenv.MustHaveLink(t)
   862  	testRootMoveTo(t, true)
   863  }
   864  
   865  func testRootMoveTo(t *testing.T, rename bool) {
   866  	want := []byte("target")
   867  	for _, test := range rootTestCases {
   868  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   869  			const srcPath = "source"
   870  			if err := os.WriteFile(filepath.Join(root.Name(), srcPath), want, 0o666); err != nil {
   871  				t.Fatal(err)
   872  			}
   873  
   874  			target = test.target
   875  			wantError := test.wantError
   876  			if test.ltarget != "" {
   877  				// Rename will overwrite the final link rather than follow it.
   878  				target = test.ltarget
   879  				wantError = false
   880  			}
   881  
   882  			// Plan 9 doesn't allow cross-directory renames.
   883  			if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
   884  				wantError = true
   885  			}
   886  
   887  			var err error
   888  			var op string
   889  			if rename {
   890  				op = "Rename"
   891  				err = root.Rename(srcPath, test.open)
   892  			} else {
   893  				op = "Link"
   894  				err = root.Link(srcPath, test.open)
   895  			}
   896  			if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, srcPath, test.open) {
   897  				return
   898  			}
   899  
   900  			_, err = os.Lstat(filepath.Join(root.Name(), srcPath))
   901  			if rename {
   902  				if !errors.Is(err, os.ErrNotExist) {
   903  					t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", srcPath, err)
   904  				}
   905  			} else {
   906  				if err != nil {
   907  					t.Errorf("after linking file, error accessing original: %v", err)
   908  				}
   909  			}
   910  
   911  			got, err := os.ReadFile(filepath.Join(root.Name(), target))
   912  			if err != nil || !bytes.Equal(got, want) {
   913  				t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, target, string(got), err, string(want))
   914  			}
   915  		})
   916  	}
   917  }
   918  
   919  func TestRootSymlink(t *testing.T) {
   920  	testenv.MustHaveSymlink(t)
   921  	for _, test := range rootTestCases {
   922  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   923  			wantError := test.wantError
   924  			if test.ltarget != "" {
   925  				// We can't create a symlink over an existing symlink.
   926  				wantError = true
   927  			}
   928  
   929  			const wantTarget = "linktarget"
   930  			err := root.Symlink(wantTarget, test.open)
   931  			if errEndsTest(t, err, wantError, "root.Symlink(%q)", test.open) {
   932  				return
   933  			}
   934  			got, err := os.Readlink(target)
   935  			if err != nil || got != wantTarget {
   936  				t.Fatalf("ReadLink(%q) = %q, %v; want %q, nil", target, got, err, wantTarget)
   937  			}
   938  		})
   939  	}
   940  }
   941  
   942  // A rootConsistencyTest is a test case comparing os.Root behavior with
   943  // the corresponding non-Root function.
   944  //
   945  // These tests verify that, for example, Root.Open("file/./") and os.Open("file/./")
   946  // have the same result, although the specific result may vary by platform.
   947  type rootConsistencyTest struct {
   948  	name string
   949  
   950  	// fs is the test filesystem layout. See makefs above.
   951  	// fsFunc is called to modify the test filesystem, or replace it.
   952  	fs     []string
   953  	fsFunc func(t *testing.T, dir string) string
   954  
   955  	// open is the filename to access in the test.
   956  	open string
   957  
   958  	// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
   959  	// function return different errors for this test.
   960  	detailedErrorMismatch func(t *testing.T) bool
   961  }
   962  
   963  var rootConsistencyTestCases = []rootConsistencyTest{{
   964  	name: "file",
   965  	fs: []string{
   966  		"target",
   967  	},
   968  	open: "target",
   969  }, {
   970  	name: "dir slash dot",
   971  	fs: []string{
   972  		"target/file",
   973  	},
   974  	open: "target/.",
   975  }, {
   976  	name: "dot",
   977  	fs: []string{
   978  		"file",
   979  	},
   980  	open: ".",
   981  }, {
   982  	name: "file slash dot",
   983  	fs: []string{
   984  		"target",
   985  	},
   986  	open: "target/.",
   987  	detailedErrorMismatch: func(t *testing.T) bool {
   988  		// FreeBSD returns EPERM in the non-Root case.
   989  		return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
   990  	},
   991  }, {
   992  	name: "dir slash",
   993  	fs: []string{
   994  		"target/file",
   995  	},
   996  	open: "target/",
   997  }, {
   998  	name: "dot slash",
   999  	fs: []string{
  1000  		"file",
  1001  	},
  1002  	open: "./",
  1003  }, {
  1004  	name: "file slash",
  1005  	fs: []string{
  1006  		"target",
  1007  	},
  1008  	open: "target/",
  1009  	detailedErrorMismatch: func(t *testing.T) bool {
  1010  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
  1011  		return runtime.GOOS == "js"
  1012  	},
  1013  }, {
  1014  	name: "file in path",
  1015  	fs: []string{
  1016  		"file",
  1017  	},
  1018  	open: "file/target",
  1019  }, {
  1020  	name: "directory in path missing",
  1021  	open: "dir/target",
  1022  }, {
  1023  	name: "target does not exist",
  1024  	open: "target",
  1025  }, {
  1026  	name: "symlink slash",
  1027  	fs: []string{
  1028  		"target/file",
  1029  		"link => target",
  1030  	},
  1031  	open: "link/",
  1032  }, {
  1033  	name: "symlink slash dot",
  1034  	fs: []string{
  1035  		"target/file",
  1036  		"link => target",
  1037  	},
  1038  	open: "link/.",
  1039  }, {
  1040  	name: "file symlink slash",
  1041  	fs: []string{
  1042  		"target",
  1043  		"link => target",
  1044  	},
  1045  	open: "link/",
  1046  	detailedErrorMismatch: func(t *testing.T) bool {
  1047  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
  1048  		return runtime.GOOS == "js"
  1049  	},
  1050  }, {
  1051  	name: "unresolved symlink",
  1052  	fs: []string{
  1053  		"link => target",
  1054  	},
  1055  	open: "link",
  1056  }, {
  1057  	name: "resolved symlink",
  1058  	fs: []string{
  1059  		"link => target",
  1060  		"target",
  1061  	},
  1062  	open: "link",
  1063  }, {
  1064  	name: "dotdot in path after symlink",
  1065  	fs: []string{
  1066  		"a => b/c",
  1067  		"b/c/",
  1068  		"b/target",
  1069  	},
  1070  	open: "a/../target",
  1071  }, {
  1072  	name: "symlink to dir ends in slash",
  1073  	fs: []string{
  1074  		"dir/",
  1075  		"link => dir",
  1076  	},
  1077  	open: "link",
  1078  }, {
  1079  	name: "symlink to file ends in slash",
  1080  	fs: []string{
  1081  		"file",
  1082  		"link => file/",
  1083  	},
  1084  	open: "link",
  1085  }, {
  1086  	name: "long file name",
  1087  	open: strings.Repeat("a", 500),
  1088  }, {
  1089  	name: "unreadable directory",
  1090  	fs: []string{
  1091  		"dir/target",
  1092  	},
  1093  	fsFunc: func(t *testing.T, dir string) string {
  1094  		os.Chmod(filepath.Join(dir, "dir"), 0)
  1095  		t.Cleanup(func() {
  1096  			os.Chmod(filepath.Join(dir, "dir"), 0o700)
  1097  		})
  1098  		return dir
  1099  	},
  1100  	open: "dir/target",
  1101  }, {
  1102  	name: "unix domain socket target",
  1103  	fsFunc: func(t *testing.T, dir string) string {
  1104  		return tempDirWithUnixSocket(t, "a")
  1105  	},
  1106  	open: "a",
  1107  }, {
  1108  	name: "unix domain socket in path",
  1109  	fsFunc: func(t *testing.T, dir string) string {
  1110  		return tempDirWithUnixSocket(t, "a")
  1111  	},
  1112  	open: "a/b",
  1113  	detailedErrorMismatch: func(t *testing.T) bool {
  1114  		// On Windows, os.Root.Open returns "The directory name is invalid."
  1115  		// and os.Open returns "The file cannot be accessed by the system.".
  1116  		return runtime.GOOS == "windows"
  1117  	},
  1118  }, {
  1119  	name: "question mark",
  1120  	open: "?",
  1121  }, {
  1122  	name: "nul byte",
  1123  	open: "\x00",
  1124  }}
  1125  
  1126  func tempDirWithUnixSocket(t *testing.T, name string) string {
  1127  	dir, err := os.MkdirTemp("", "")
  1128  	if err != nil {
  1129  		t.Fatal(err)
  1130  	}
  1131  	t.Cleanup(func() {
  1132  		if err := os.RemoveAll(dir); err != nil {
  1133  			t.Error(err)
  1134  		}
  1135  	})
  1136  	addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name))
  1137  	if err != nil {
  1138  		t.Skipf("net.ResolveUnixAddr: %v", err)
  1139  	}
  1140  	conn, err := net.ListenUnix("unix", addr)
  1141  	if err != nil {
  1142  		t.Skipf("net.ListenUnix: %v", err)
  1143  	}
  1144  	t.Cleanup(func() {
  1145  		conn.Close()
  1146  	})
  1147  	return dir
  1148  }
  1149  
  1150  func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) {
  1151  	if runtime.GOOS == "wasip1" {
  1152  		// On wasip, non-Root functions clean paths before opening them,
  1153  		// resulting in inconsistent behavior.
  1154  		// https://go.dev/issue/69509
  1155  		t.Skip("#69509: inconsistent results on wasip1")
  1156  	}
  1157  
  1158  	t.Run(test.name, func(t *testing.T) {
  1159  		dir1 := makefs(t, test.fs)
  1160  		dir2 := makefs(t, test.fs)
  1161  		if test.fsFunc != nil {
  1162  			dir1 = test.fsFunc(t, dir1)
  1163  			dir2 = test.fsFunc(t, dir2)
  1164  		}
  1165  
  1166  		r, err := os.OpenRoot(dir1)
  1167  		if err != nil {
  1168  			t.Fatal(err)
  1169  		}
  1170  		defer r.Close()
  1171  
  1172  		res1, err1 := f(t, test.open, r)
  1173  		res2, err2 := f(t, dir2+"/"+test.open, nil)
  1174  
  1175  		if res1 != res2 || ((err1 == nil) != (err2 == nil)) {
  1176  			t.Errorf("with root:    res=%v", res1)
  1177  			t.Errorf("              err=%v", err1)
  1178  			t.Errorf("without root: res=%v", res2)
  1179  			t.Errorf("              err=%v", err2)
  1180  			t.Errorf("want consistent results, got mismatch")
  1181  		}
  1182  
  1183  		if err1 != nil || err2 != nil {
  1184  			underlyingError := func(how string, err error) error {
  1185  				switch e := err1.(type) {
  1186  				case *os.PathError:
  1187  					return e.Err
  1188  				case *os.LinkError:
  1189  					return e.Err
  1190  				default:
  1191  					t.Fatalf("%v, expected PathError or LinkError; got: %v", how, err)
  1192  				}
  1193  				return nil
  1194  			}
  1195  			e1 := underlyingError("with root", err1)
  1196  			e2 := underlyingError("without root", err1)
  1197  			detailedErrorMismatch := false
  1198  			if f := test.detailedErrorMismatch; f != nil {
  1199  				detailedErrorMismatch = f(t)
  1200  			}
  1201  			if runtime.GOOS == "plan9" {
  1202  				// Plan9 syscall errors aren't comparable.
  1203  				detailedErrorMismatch = true
  1204  			}
  1205  			if !detailedErrorMismatch && e1 != e2 {
  1206  				t.Errorf("with root:    err=%v", e1)
  1207  				t.Errorf("without root: err=%v", e2)
  1208  				t.Errorf("want consistent results, got mismatch")
  1209  			}
  1210  		}
  1211  	})
  1212  }
  1213  
  1214  func TestRootConsistencyOpen(t *testing.T) {
  1215  	for _, test := range rootConsistencyTestCases {
  1216  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1217  			var f *os.File
  1218  			var err error
  1219  			if r == nil {
  1220  				f, err = os.Open(path)
  1221  			} else {
  1222  				f, err = r.Open(path)
  1223  			}
  1224  			if err != nil {
  1225  				return "", err
  1226  			}
  1227  			defer f.Close()
  1228  			fi, err := f.Stat()
  1229  			if err == nil && !fi.IsDir() {
  1230  				b, err := io.ReadAll(f)
  1231  				return string(b), err
  1232  			} else {
  1233  				names, err := f.Readdirnames(-1)
  1234  				slices.Sort(names)
  1235  				return fmt.Sprintf("%q", names), err
  1236  			}
  1237  		})
  1238  	}
  1239  }
  1240  
  1241  func TestRootConsistencyCreate(t *testing.T) {
  1242  	for _, test := range rootConsistencyTestCases {
  1243  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1244  			var f *os.File
  1245  			var err error
  1246  			if r == nil {
  1247  				f, err = os.Create(path)
  1248  			} else {
  1249  				f, err = r.Create(path)
  1250  			}
  1251  			if err == nil {
  1252  				f.Write([]byte("file contents"))
  1253  				f.Close()
  1254  			}
  1255  			return "", err
  1256  		})
  1257  	}
  1258  }
  1259  
  1260  func TestRootConsistencyChmod(t *testing.T) {
  1261  	if runtime.GOOS == "wasip1" {
  1262  		t.Skip("Chmod not supported on " + runtime.GOOS)
  1263  	}
  1264  	for _, test := range rootConsistencyTestCases {
  1265  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1266  			chmod := os.Chmod
  1267  			lstat := os.Lstat
  1268  			if r != nil {
  1269  				chmod = r.Chmod
  1270  				lstat = r.Lstat
  1271  			}
  1272  
  1273  			var m1, m2 os.FileMode
  1274  			if err := chmod(path, 0o555); err != nil {
  1275  				return "chmod 0o555", err
  1276  			}
  1277  			fi, err := lstat(path)
  1278  			if err == nil {
  1279  				m1 = fi.Mode()
  1280  			}
  1281  			if err = chmod(path, 0o777); err != nil {
  1282  				return "chmod 0o777", err
  1283  			}
  1284  			fi, err = lstat(path)
  1285  			if err == nil {
  1286  				m2 = fi.Mode()
  1287  			}
  1288  			return fmt.Sprintf("%v %v", m1, m2), err
  1289  		})
  1290  	}
  1291  }
  1292  
  1293  func TestRootConsistencyMkdir(t *testing.T) {
  1294  	for _, test := range rootConsistencyTestCases {
  1295  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1296  			var err error
  1297  			if r == nil {
  1298  				err = os.Mkdir(path, 0o777)
  1299  			} else {
  1300  				err = r.Mkdir(path, 0o777)
  1301  			}
  1302  			return "", err
  1303  		})
  1304  	}
  1305  }
  1306  
  1307  func TestRootConsistencyRemove(t *testing.T) {
  1308  	for _, test := range rootConsistencyTestCases {
  1309  		if test.open == "." || test.open == "./" {
  1310  			continue // can't remove the root itself
  1311  		}
  1312  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1313  			var err error
  1314  			if r == nil {
  1315  				err = os.Remove(path)
  1316  			} else {
  1317  				err = r.Remove(path)
  1318  			}
  1319  			return "", err
  1320  		})
  1321  	}
  1322  }
  1323  
  1324  func TestRootConsistencyStat(t *testing.T) {
  1325  	for _, test := range rootConsistencyTestCases {
  1326  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1327  			var fi os.FileInfo
  1328  			var err error
  1329  			if r == nil {
  1330  				fi, err = os.Stat(path)
  1331  			} else {
  1332  				fi, err = r.Stat(path)
  1333  			}
  1334  			if err != nil {
  1335  				return "", err
  1336  			}
  1337  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1338  		})
  1339  	}
  1340  }
  1341  
  1342  func TestRootConsistencyLstat(t *testing.T) {
  1343  	for _, test := range rootConsistencyTestCases {
  1344  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1345  			var fi os.FileInfo
  1346  			var err error
  1347  			if r == nil {
  1348  				fi, err = os.Lstat(path)
  1349  			} else {
  1350  				fi, err = r.Lstat(path)
  1351  			}
  1352  			if err != nil {
  1353  				return "", err
  1354  			}
  1355  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1356  		})
  1357  	}
  1358  }
  1359  
  1360  func TestRootConsistencyReadlink(t *testing.T) {
  1361  	for _, test := range rootConsistencyTestCases {
  1362  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1363  			if r == nil {
  1364  				return os.Readlink(path)
  1365  			} else {
  1366  				return r.Readlink(path)
  1367  			}
  1368  		})
  1369  	}
  1370  }
  1371  
  1372  func TestRootConsistencyRename(t *testing.T) {
  1373  	testRootConsistencyMove(t, true)
  1374  }
  1375  
  1376  func TestRootConsistencyLink(t *testing.T) {
  1377  	testenv.MustHaveLink(t)
  1378  	testRootConsistencyMove(t, false)
  1379  }
  1380  
  1381  func testRootConsistencyMove(t *testing.T, rename bool) {
  1382  	if runtime.GOOS == "plan9" {
  1383  		// This test depends on moving files between directories.
  1384  		t.Skip("Plan 9 does not support cross-directory renames")
  1385  	}
  1386  	// Run this test in two directions:
  1387  	// Renaming the test path to a known-good path (from),
  1388  	// and renaming a known-good path to the test path (to).
  1389  	for _, name := range []string{"from", "to"} {
  1390  		t.Run(name, func(t *testing.T) {
  1391  			for _, test := range rootConsistencyTestCases {
  1392  				if runtime.GOOS == "windows" {
  1393  					// On Windows, Rename("/path/to/.", x) succeeds,
  1394  					// because Windows cleans the path to just "/path/to".
  1395  					// Root.Rename(".", x) fails as expected.
  1396  					// Don't run this consistency test on Windows.
  1397  					if test.open == "." || test.open == "./" {
  1398  						continue
  1399  					}
  1400  				}
  1401  
  1402  				test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1403  					var move func(oldname, newname string) error
  1404  					switch {
  1405  					case rename && r == nil:
  1406  						move = os.Rename
  1407  					case rename && r != nil:
  1408  						move = r.Rename
  1409  					case !rename && r == nil:
  1410  						move = os.Link
  1411  					case !rename && r != nil:
  1412  						move = r.Link
  1413  					}
  1414  					lstat := os.Lstat
  1415  					if r != nil {
  1416  						lstat = r.Lstat
  1417  					}
  1418  
  1419  					otherPath := "other"
  1420  					if r == nil {
  1421  						otherPath = filepath.Join(t.TempDir(), otherPath)
  1422  					}
  1423  
  1424  					var srcPath, dstPath string
  1425  					if name == "from" {
  1426  						srcPath = path
  1427  						dstPath = otherPath
  1428  					} else {
  1429  						srcPath = otherPath
  1430  						dstPath = path
  1431  					}
  1432  
  1433  					if !rename {
  1434  						// When the source is a symlink, Root.Link creates
  1435  						// a hard link to the symlink.
  1436  						// os.Link does whatever the link syscall does,
  1437  						// which varies between operating systems and
  1438  						// their versions.
  1439  						// Skip running the consistency test when
  1440  						// the source is a symlink.
  1441  						fi, err := lstat(srcPath)
  1442  						if err == nil && fi.Mode()&os.ModeSymlink != 0 {
  1443  							return "", nil
  1444  						}
  1445  					}
  1446  
  1447  					if err := move(srcPath, dstPath); err != nil {
  1448  						return "", err
  1449  					}
  1450  					fi, err := lstat(dstPath)
  1451  					if err != nil {
  1452  						t.Errorf("stat(%q) after successful copy: %v", dstPath, err)
  1453  						return "stat error", err
  1454  					}
  1455  					return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1456  				})
  1457  			}
  1458  		})
  1459  	}
  1460  }
  1461  
  1462  func TestRootConsistencySymlink(t *testing.T) {
  1463  	testenv.MustHaveSymlink(t)
  1464  	for _, test := range rootConsistencyTestCases {
  1465  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1466  			const target = "linktarget"
  1467  			var err error
  1468  			var got string
  1469  			if r == nil {
  1470  				err = os.Symlink(target, path)
  1471  				got, _ = os.Readlink(target)
  1472  			} else {
  1473  				err = r.Symlink(target, path)
  1474  				got, _ = r.Readlink(target)
  1475  			}
  1476  			return got, err
  1477  		})
  1478  	}
  1479  }
  1480  
  1481  func TestRootRenameAfterOpen(t *testing.T) {
  1482  	switch runtime.GOOS {
  1483  	case "windows":
  1484  		t.Skip("renaming open files not supported on " + runtime.GOOS)
  1485  	case "js", "plan9":
  1486  		t.Skip("openat not supported on " + runtime.GOOS)
  1487  	case "wasip1":
  1488  		if os.Getenv("GOWASIRUNTIME") == "wazero" {
  1489  			t.Skip("wazero does not track renamed directories")
  1490  		}
  1491  	}
  1492  
  1493  	dir := t.TempDir()
  1494  
  1495  	// Create directory "a" and open it.
  1496  	if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil {
  1497  		t.Fatal(err)
  1498  	}
  1499  	dirf, err := os.OpenRoot(filepath.Join(dir, "a"))
  1500  	if err != nil {
  1501  		t.Fatal(err)
  1502  	}
  1503  	defer dirf.Close()
  1504  
  1505  	// Rename "a" => "b", and create "b/f".
  1506  	if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil {
  1507  		t.Fatal(err)
  1508  	}
  1509  	if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil {
  1510  		t.Fatal(err)
  1511  	}
  1512  
  1513  	// Open "f", and confirm that we see it.
  1514  	f, err := dirf.OpenFile("f", os.O_RDONLY, 0)
  1515  	if err != nil {
  1516  		t.Fatalf("reading file after renaming parent: %v", err)
  1517  	}
  1518  	defer f.Close()
  1519  	b, err := io.ReadAll(f)
  1520  	if err != nil {
  1521  		t.Fatal(err)
  1522  	}
  1523  	if got, want := string(b), "hello"; got != want {
  1524  		t.Fatalf("file contents: %q, want %q", got, want)
  1525  	}
  1526  
  1527  	// f.Name reflects the original path we opened the directory under (".../a"), not "b".
  1528  	if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want {
  1529  		t.Errorf("f.Name() = %q, want %q", got, want)
  1530  	}
  1531  }
  1532  
  1533  func TestRootNonPermissionMode(t *testing.T) {
  1534  	r, err := os.OpenRoot(t.TempDir())
  1535  	if err != nil {
  1536  		t.Fatal(err)
  1537  	}
  1538  	defer r.Close()
  1539  	if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil {
  1540  		t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error")
  1541  	}
  1542  	if err := r.Mkdir("file", 0o1777); err == nil {
  1543  		t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error")
  1544  	}
  1545  }
  1546  
  1547  func TestRootUseAfterClose(t *testing.T) {
  1548  	r, err := os.OpenRoot(t.TempDir())
  1549  	if err != nil {
  1550  		t.Fatal(err)
  1551  	}
  1552  	r.Close()
  1553  	for _, test := range []struct {
  1554  		name string
  1555  		f    func(r *os.Root, filename string) error
  1556  	}{{
  1557  		name: "Open",
  1558  		f: func(r *os.Root, filename string) error {
  1559  			_, err := r.Open(filename)
  1560  			return err
  1561  		},
  1562  	}, {
  1563  		name: "Create",
  1564  		f: func(r *os.Root, filename string) error {
  1565  			_, err := r.Create(filename)
  1566  			return err
  1567  		},
  1568  	}, {
  1569  		name: "OpenFile",
  1570  		f: func(r *os.Root, filename string) error {
  1571  			_, err := r.OpenFile(filename, os.O_RDWR, 0o666)
  1572  			return err
  1573  		},
  1574  	}, {
  1575  		name: "OpenRoot",
  1576  		f: func(r *os.Root, filename string) error {
  1577  			_, err := r.OpenRoot(filename)
  1578  			return err
  1579  		},
  1580  	}, {
  1581  		name: "Mkdir",
  1582  		f: func(r *os.Root, filename string) error {
  1583  			return r.Mkdir(filename, 0o777)
  1584  		},
  1585  	}} {
  1586  		err := test.f(r, "target")
  1587  		pe, ok := err.(*os.PathError)
  1588  		if !ok || pe.Path != "target" || pe.Err != os.ErrClosed {
  1589  			t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err)
  1590  		}
  1591  	}
  1592  }
  1593  
  1594  func TestRootConcurrentClose(t *testing.T) {
  1595  	r, err := os.OpenRoot(t.TempDir())
  1596  	if err != nil {
  1597  		t.Fatal(err)
  1598  	}
  1599  	ch := make(chan error, 1)
  1600  	go func() {
  1601  		defer close(ch)
  1602  		first := true
  1603  		for {
  1604  			f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666)
  1605  			if err != nil {
  1606  				ch <- err
  1607  				return
  1608  			}
  1609  			if first {
  1610  				ch <- nil
  1611  				first = false
  1612  			}
  1613  			f.Close()
  1614  			if runtime.GOARCH == "wasm" {
  1615  				// TODO(go.dev/issue/71134) can lead to goroutine starvation.
  1616  				runtime.Gosched()
  1617  			}
  1618  		}
  1619  	}()
  1620  	if err := <-ch; err != nil {
  1621  		t.Errorf("OpenFile: %v, want success", err)
  1622  	}
  1623  	r.Close()
  1624  	if err := <-ch; !errors.Is(err, os.ErrClosed) {
  1625  		t.Errorf("OpenFile: %v, want ErrClosed", err)
  1626  	}
  1627  }
  1628  
  1629  // TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse.
  1630  //
  1631  // We create a deeply nested directory:
  1632  //
  1633  //	base/a/a/a/a/ [...] /a
  1634  //
  1635  // And a path that descends into the tree, then returns to the top using ..:
  1636  //
  1637  //	base/a/a/a/a/ [...] /a/../../../ [..] /../a/f
  1638  //
  1639  // While opening this file, we rename base/a/a to base/b.
  1640  // A naive lookup operation will resolve the path to base/f.
  1641  func TestRootRaceRenameDir(t *testing.T) {
  1642  	dir := t.TempDir()
  1643  	r, err := os.OpenRoot(dir)
  1644  	if err != nil {
  1645  		t.Fatal(err)
  1646  	}
  1647  	defer r.Close()
  1648  
  1649  	const depth = 4
  1650  
  1651  	os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777)
  1652  
  1653  	path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f"
  1654  	os.WriteFile(dir+"/f", []byte("secret"), 0o666)
  1655  	os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666)
  1656  
  1657  	// Compute how long it takes to open the path in the common case.
  1658  	const tries = 10
  1659  	var total time.Duration
  1660  	for range tries {
  1661  		start := time.Now()
  1662  		f, err := r.Open(path)
  1663  		if err != nil {
  1664  			t.Fatal(err)
  1665  		}
  1666  		b, err := io.ReadAll(f)
  1667  		if err != nil {
  1668  			t.Fatal(err)
  1669  		}
  1670  		if string(b) != "public" {
  1671  			t.Fatalf("read %q, want %q", b, "public")
  1672  		}
  1673  		f.Close()
  1674  		total += time.Since(start)
  1675  	}
  1676  	avg := total / tries
  1677  
  1678  	// We're trying to exploit a race, so try this a number of times.
  1679  	for range 100 {
  1680  		// Start a goroutine to open the file.
  1681  		gotc := make(chan []byte)
  1682  		go func() {
  1683  			f, err := r.Open(path)
  1684  			if err != nil {
  1685  				gotc <- nil
  1686  			}
  1687  			defer f.Close()
  1688  			b, _ := io.ReadAll(f)
  1689  			gotc <- b
  1690  		}()
  1691  
  1692  		// Wait for the open operation to partially complete,
  1693  		// and then rename a directory near the root.
  1694  		time.Sleep(avg / 4)
  1695  		if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil {
  1696  			// Windows and Plan9 won't let us rename a directory if we have
  1697  			// an open handle for it, so an error here is expected.
  1698  			switch runtime.GOOS {
  1699  			case "windows", "plan9":
  1700  			default:
  1701  				t.Fatal(err)
  1702  			}
  1703  		}
  1704  
  1705  		got := <-gotc
  1706  		os.Rename(dir+"/b", dir+"/base/a")
  1707  		if len(got) > 0 && string(got) != "public" {
  1708  			t.Errorf("read file: %q; want error or 'public'", got)
  1709  		}
  1710  	}
  1711  }
  1712  
  1713  func TestRootSymlinkToRoot(t *testing.T) {
  1714  	dir := makefs(t, []string{
  1715  		"d/d => ..",
  1716  	})
  1717  	root, err := os.OpenRoot(dir)
  1718  	if err != nil {
  1719  		t.Fatal(err)
  1720  	}
  1721  	defer root.Close()
  1722  	if err := root.Mkdir("d/d/new", 0777); err != nil {
  1723  		t.Fatal(err)
  1724  	}
  1725  	f, err := root.Open("d/d")
  1726  	if err != nil {
  1727  		t.Fatal(err)
  1728  	}
  1729  	defer f.Close()
  1730  	names, err := f.Readdirnames(-1)
  1731  	if err != nil {
  1732  		t.Fatal(err)
  1733  	}
  1734  	slices.Sort(names)
  1735  	if got, want := names, []string{"d", "new"}; !slices.Equal(got, want) {
  1736  		t.Errorf("root contains: %q, want %q", got, want)
  1737  	}
  1738  }
  1739  
  1740  func TestOpenInRoot(t *testing.T) {
  1741  	dir := makefs(t, []string{
  1742  		"file",
  1743  		"link => ../ROOT/file",
  1744  	})
  1745  	f, err := os.OpenInRoot(dir, "file")
  1746  	if err != nil {
  1747  		t.Fatalf("OpenInRoot(`file`) = %v, want success", err)
  1748  	}
  1749  	f.Close()
  1750  	for _, name := range []string{
  1751  		"link",
  1752  		"../ROOT/file",
  1753  		dir + "/file",
  1754  	} {
  1755  		f, err := os.OpenInRoot(dir, name)
  1756  		if err == nil {
  1757  			f.Close()
  1758  			t.Fatalf("OpenInRoot(%q) = nil, want error", name)
  1759  		}
  1760  	}
  1761  }
  1762  

View as plain text