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 chain",
   192  	fs: []string{
   193  		"link => a/b/c/target",
   194  		"a/b => e",
   195  		"a/e => ../f",
   196  		"f => g/h/i",
   197  		"g/h/i => ..",
   198  		"g/c/",
   199  	},
   200  	open:    "link",
   201  	target:  "g/c/target",
   202  	ltarget: "link",
   203  }, {
   204  	name: "path with dot",
   205  	fs: []string{
   206  		"a/b/",
   207  	},
   208  	open:   "./a/./b/./target",
   209  	target: "a/b/target",
   210  }, {
   211  	name: "path with dotdot",
   212  	fs: []string{
   213  		"a/b/",
   214  	},
   215  	open:   "a/../a/b/../../a/b/../b/target",
   216  	target: "a/b/target",
   217  }, {
   218  	name: "dotdot no symlink",
   219  	fs: []string{
   220  		"a/",
   221  	},
   222  	open:   "a/../target",
   223  	target: "target",
   224  }, {
   225  	name: "dotdot after symlink",
   226  	fs: []string{
   227  		"a => b/c",
   228  		"b/c/",
   229  	},
   230  	open: "a/../target",
   231  	target: func() string {
   232  		if runtime.GOOS == "windows" {
   233  			// On Windows, the path is cleaned before symlink resolution.
   234  			return "target"
   235  		}
   236  		return "b/target"
   237  	}(),
   238  }, {
   239  	name: "dotdot before symlink",
   240  	fs: []string{
   241  		"a => b/c",
   242  		"b/c/",
   243  	},
   244  	open:   "b/../a/target",
   245  	target: "b/c/target",
   246  }, {
   247  	name: "symlink ends in dot",
   248  	fs: []string{
   249  		"a => b/.",
   250  		"b/",
   251  	},
   252  	open:   "a/target",
   253  	target: "b/target",
   254  }, {
   255  	name:        "directory does not exist",
   256  	fs:          []string{},
   257  	open:        "a/file",
   258  	wantError:   true,
   259  	alwaysFails: true,
   260  }, {
   261  	name:        "empty path",
   262  	fs:          []string{},
   263  	open:        "",
   264  	wantError:   true,
   265  	alwaysFails: true,
   266  }, {
   267  	name: "symlink cycle",
   268  	fs: []string{
   269  		"a => a",
   270  	},
   271  	open:        "a",
   272  	ltarget:     "a",
   273  	wantError:   true,
   274  	alwaysFails: true,
   275  }, {
   276  	name:      "path escapes",
   277  	fs:        []string{},
   278  	open:      "../ROOT/target",
   279  	target:    "target",
   280  	wantError: true,
   281  }, {
   282  	name: "long path escapes",
   283  	fs: []string{
   284  		"a/",
   285  	},
   286  	open:      "a/../../ROOT/target",
   287  	target:    "target",
   288  	wantError: true,
   289  }, {
   290  	name: "absolute symlink",
   291  	fs: []string{
   292  		"link => $ABS/target",
   293  	},
   294  	open:      "link",
   295  	ltarget:   "link",
   296  	target:    "target",
   297  	wantError: true,
   298  }, {
   299  	name: "relative symlink",
   300  	fs: []string{
   301  		"link => ../ROOT/target",
   302  	},
   303  	open:      "link",
   304  	target:    "target",
   305  	ltarget:   "link",
   306  	wantError: true,
   307  }, {
   308  	name: "symlink chain escapes",
   309  	fs: []string{
   310  		"link => a/b/c/target",
   311  		"a/b => e",
   312  		"a/e => ../../ROOT",
   313  		"c/",
   314  	},
   315  	open:      "link",
   316  	target:    "c/target",
   317  	ltarget:   "link",
   318  	wantError: true,
   319  }}
   320  
   321  func TestRootOpen_File(t *testing.T) {
   322  	want := []byte("target")
   323  	for _, test := range rootTestCases {
   324  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   325  			if target != "" {
   326  				if err := os.WriteFile(target, want, 0o666); err != nil {
   327  					t.Fatal(err)
   328  				}
   329  			}
   330  			f, err := root.Open(test.open)
   331  			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
   332  				return
   333  			}
   334  			defer f.Close()
   335  			got, err := io.ReadAll(f)
   336  			if err != nil || !bytes.Equal(got, want) {
   337  				t.Errorf(`Dir.Open(%q): read content %q, %v; want %q`, test.open, string(got), err, string(want))
   338  			}
   339  		})
   340  	}
   341  }
   342  
   343  func TestRootOpen_Directory(t *testing.T) {
   344  	for _, test := range rootTestCases {
   345  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   346  			if target != "" {
   347  				if err := os.Mkdir(target, 0o777); err != nil {
   348  					t.Fatal(err)
   349  				}
   350  				if err := os.WriteFile(target+"/found", nil, 0o666); err != nil {
   351  					t.Fatal(err)
   352  				}
   353  			}
   354  			f, err := root.Open(test.open)
   355  			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
   356  				return
   357  			}
   358  			defer f.Close()
   359  			got, err := f.Readdirnames(-1)
   360  			if err != nil {
   361  				t.Errorf(`Dir.Open(%q).Readdirnames: %v`, test.open, err)
   362  			}
   363  			if want := []string{"found"}; !slices.Equal(got, want) {
   364  				t.Errorf(`Dir.Open(%q).Readdirnames: %q, want %q`, test.open, got, want)
   365  			}
   366  		})
   367  	}
   368  }
   369  
   370  func TestRootCreate(t *testing.T) {
   371  	want := []byte("target")
   372  	for _, test := range rootTestCases {
   373  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   374  			f, err := root.Create(test.open)
   375  			if errEndsTest(t, err, test.wantError, "root.Create(%q)", test.open) {
   376  				return
   377  			}
   378  			if _, err := f.Write(want); err != nil {
   379  				t.Fatal(err)
   380  			}
   381  			f.Close()
   382  			got, err := os.ReadFile(target)
   383  			if err != nil {
   384  				t.Fatalf(`reading file created with root.Create(%q): %v`, test.open, err)
   385  			}
   386  			if !bytes.Equal(got, want) {
   387  				t.Fatalf(`reading file created with root.Create(%q): got %q; want %q`, test.open, got, want)
   388  			}
   389  		})
   390  	}
   391  }
   392  
   393  func TestRootChmod(t *testing.T) {
   394  	if runtime.GOOS == "wasip1" {
   395  		t.Skip("Chmod not supported on " + runtime.GOOS)
   396  	}
   397  	for _, test := range rootTestCases {
   398  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   399  			if target != "" {
   400  				// Create a file with no read/write permissions,
   401  				// to ensure we can use Chmod on an inaccessible file.
   402  				if err := os.WriteFile(target, nil, 0o000); err != nil {
   403  					t.Fatal(err)
   404  				}
   405  			}
   406  			if runtime.GOOS == "windows" {
   407  				// On Windows, Chmod("symlink") affects the link, not its target.
   408  				// See issue 71492.
   409  				fi, err := root.Lstat(test.open)
   410  				if err == nil && !fi.Mode().IsRegular() {
   411  					t.Skip("https://go.dev/issue/71492")
   412  				}
   413  			}
   414  			want := os.FileMode(0o666)
   415  			err := root.Chmod(test.open, want)
   416  			if errEndsTest(t, err, test.wantError, "root.Chmod(%q)", test.open) {
   417  				return
   418  			}
   419  			st, err := os.Stat(target)
   420  			if err != nil {
   421  				t.Fatalf("os.Stat(%q) = %v", target, err)
   422  			}
   423  			if got := st.Mode(); got != want {
   424  				t.Errorf("after root.Chmod(%q, %v): file mode = %v, want %v", test.open, want, got, want)
   425  			}
   426  		})
   427  	}
   428  }
   429  
   430  func TestRootChtimes(t *testing.T) {
   431  	// Don't check atimes if the fs is mounted noatime,
   432  	// or on Plan 9 which does not permit changing atimes to arbitrary values.
   433  	checkAtimes := !hasNoatime() && runtime.GOOS != "plan9"
   434  	for _, test := range rootTestCases {
   435  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   436  			if target != "" {
   437  				if err := os.WriteFile(target, nil, 0o666); err != nil {
   438  					t.Fatal(err)
   439  				}
   440  			}
   441  			for _, times := range []struct {
   442  				atime, mtime time.Time
   443  			}{{
   444  				atime: time.Now().Add(-1 * time.Minute),
   445  				mtime: time.Now().Add(-1 * time.Minute),
   446  			}, {
   447  				atime: time.Now().Add(1 * time.Minute),
   448  				mtime: time.Now().Add(1 * time.Minute),
   449  			}, {
   450  				atime: time.Time{},
   451  				mtime: time.Now(),
   452  			}, {
   453  				atime: time.Now(),
   454  				mtime: time.Time{},
   455  			}} {
   456  				switch runtime.GOOS {
   457  				case "js", "plan9":
   458  					times.atime = times.atime.Truncate(1 * time.Second)
   459  					times.mtime = times.mtime.Truncate(1 * time.Second)
   460  				}
   461  
   462  				err := root.Chtimes(test.open, times.atime, times.mtime)
   463  				if errEndsTest(t, err, test.wantError, "root.Chtimes(%q)", test.open) {
   464  					return
   465  				}
   466  				st, err := os.Stat(target)
   467  				if err != nil {
   468  					t.Fatalf("os.Stat(%q) = %v", target, err)
   469  				}
   470  				if got := st.ModTime(); !times.mtime.IsZero() && !got.Equal(times.mtime) {
   471  					t.Errorf("after root.Chtimes(%q, %v, %v): got mtime=%v, want %v", test.open, times.atime, times.mtime, got, times.mtime)
   472  				}
   473  				if checkAtimes {
   474  					if got := os.Atime(st); !times.atime.IsZero() && !got.Equal(times.atime) {
   475  						t.Errorf("after root.Chtimes(%q, %v, %v): got atime=%v, want %v", test.open, times.atime, times.mtime, got, times.atime)
   476  					}
   477  				}
   478  			}
   479  		})
   480  	}
   481  }
   482  
   483  func TestRootMkdir(t *testing.T) {
   484  	for _, test := range rootTestCases {
   485  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   486  			wantError := test.wantError
   487  			if !wantError {
   488  				fi, err := os.Lstat(filepath.Join(root.Name(), test.open))
   489  				if err == nil && fi.Mode().Type() == fs.ModeSymlink {
   490  					// This case is trying to mkdir("some symlink"),
   491  					// which is an error.
   492  					wantError = true
   493  				}
   494  			}
   495  
   496  			err := root.Mkdir(test.open, 0o777)
   497  			if errEndsTest(t, err, wantError, "root.Create(%q)", test.open) {
   498  				return
   499  			}
   500  			fi, err := os.Lstat(target)
   501  			if err != nil {
   502  				t.Fatalf(`stat file created with Root.Mkdir(%q): %v`, test.open, err)
   503  			}
   504  			if !fi.IsDir() {
   505  				t.Fatalf(`stat file created with Root.Mkdir(%q): not a directory`, test.open)
   506  			}
   507  		})
   508  	}
   509  }
   510  
   511  func TestRootOpenRoot(t *testing.T) {
   512  	for _, test := range rootTestCases {
   513  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   514  			if target != "" {
   515  				if err := os.Mkdir(target, 0o777); err != nil {
   516  					t.Fatal(err)
   517  				}
   518  				if err := os.WriteFile(target+"/f", nil, 0o666); err != nil {
   519  					t.Fatal(err)
   520  				}
   521  			}
   522  			rr, err := root.OpenRoot(test.open)
   523  			if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) {
   524  				return
   525  			}
   526  			defer rr.Close()
   527  			f, err := rr.Open("f")
   528  			if err != nil {
   529  				t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err)
   530  			}
   531  			f.Close()
   532  		})
   533  	}
   534  }
   535  
   536  func TestRootRemoveFile(t *testing.T) {
   537  	for _, test := range rootTestCases {
   538  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   539  			wantError := test.wantError
   540  			if test.ltarget != "" {
   541  				// Remove doesn't follow symlinks in the final path component,
   542  				// so it will successfully remove ltarget.
   543  				wantError = false
   544  				target = filepath.Join(root.Name(), test.ltarget)
   545  			} else if target != "" {
   546  				if err := os.WriteFile(target, nil, 0o666); err != nil {
   547  					t.Fatal(err)
   548  				}
   549  			}
   550  
   551  			err := root.Remove(test.open)
   552  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   553  				return
   554  			}
   555  			_, err = os.Lstat(target)
   556  			if !errors.Is(err, os.ErrNotExist) {
   557  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   558  			}
   559  		})
   560  	}
   561  }
   562  
   563  func TestRootRemoveDirectory(t *testing.T) {
   564  	for _, test := range rootTestCases {
   565  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   566  			wantError := test.wantError
   567  			if test.ltarget != "" {
   568  				// Remove doesn't follow symlinks in the final path component,
   569  				// so it will successfully remove ltarget.
   570  				wantError = false
   571  				target = filepath.Join(root.Name(), test.ltarget)
   572  			} else if target != "" {
   573  				if err := os.Mkdir(target, 0o777); err != nil {
   574  					t.Fatal(err)
   575  				}
   576  			}
   577  
   578  			err := root.Remove(test.open)
   579  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   580  				return
   581  			}
   582  			_, err = os.Lstat(target)
   583  			if !errors.Is(err, os.ErrNotExist) {
   584  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   585  			}
   586  		})
   587  	}
   588  }
   589  
   590  func TestRootOpenFileAsRoot(t *testing.T) {
   591  	dir := t.TempDir()
   592  	target := filepath.Join(dir, "target")
   593  	if err := os.WriteFile(target, nil, 0o666); err != nil {
   594  		t.Fatal(err)
   595  	}
   596  	r, err := os.OpenRoot(target)
   597  	if err == nil {
   598  		r.Close()
   599  		t.Fatal("os.OpenRoot(file) succeeded; want failure")
   600  	}
   601  	r, err = os.OpenRoot(dir)
   602  	if err != nil {
   603  		t.Fatal(err)
   604  	}
   605  	defer r.Close()
   606  	rr, err := r.OpenRoot("target")
   607  	if err == nil {
   608  		rr.Close()
   609  		t.Fatal("Root.OpenRoot(file) succeeded; want failure")
   610  	}
   611  }
   612  
   613  func TestRootStat(t *testing.T) {
   614  	for _, test := range rootTestCases {
   615  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   616  			const content = "content"
   617  			if target != "" {
   618  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   619  					t.Fatal(err)
   620  				}
   621  			}
   622  
   623  			fi, err := root.Stat(test.open)
   624  			if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) {
   625  				return
   626  			}
   627  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   628  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   629  			}
   630  			if got, want := fi.Size(), int64(len(content)); got != want {
   631  				t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   632  			}
   633  		})
   634  	}
   635  }
   636  
   637  func TestRootLstat(t *testing.T) {
   638  	for _, test := range rootTestCases {
   639  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   640  			const content = "content"
   641  			wantError := test.wantError
   642  			if test.ltarget != "" {
   643  				// Lstat will stat the final link, rather than following it.
   644  				wantError = false
   645  			} else if target != "" {
   646  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   647  					t.Fatal(err)
   648  				}
   649  			}
   650  
   651  			fi, err := root.Lstat(test.open)
   652  			if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) {
   653  				return
   654  			}
   655  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   656  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   657  			}
   658  			if test.ltarget == "" {
   659  				if got := fi.Mode(); got&os.ModeSymlink != 0 {
   660  					t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got)
   661  				}
   662  				if got, want := fi.Size(), int64(len(content)); got != want {
   663  					t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   664  				}
   665  			} else {
   666  				if got := fi.Mode(); got&os.ModeSymlink == 0 {
   667  					t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got)
   668  				}
   669  			}
   670  		})
   671  	}
   672  }
   673  
   674  func TestRootReadlink(t *testing.T) {
   675  	for _, test := range rootTestCases {
   676  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   677  			const content = "content"
   678  			wantError := test.wantError
   679  			if test.ltarget != "" {
   680  				// Readlink will read the final link, rather than following it.
   681  				wantError = false
   682  			} else {
   683  				// Readlink fails on non-link targets.
   684  				wantError = true
   685  			}
   686  
   687  			got, err := root.Readlink(test.open)
   688  			if errEndsTest(t, err, wantError, "root.Readlink(%q)", test.open) {
   689  				return
   690  			}
   691  
   692  			want, err := os.Readlink(filepath.Join(root.Name(), test.ltarget))
   693  			if err != nil {
   694  				t.Fatalf("os.Readlink(%q) = %v, want success", test.ltarget, err)
   695  			}
   696  			if got != want {
   697  				t.Errorf("root.Readlink(%q) = %q, want %q", test.open, got, want)
   698  			}
   699  		})
   700  	}
   701  }
   702  
   703  // TestRootRenameFrom tests renaming the test case target to a known-good path.
   704  func TestRootRenameFrom(t *testing.T) {
   705  	testRootMoveFrom(t, true)
   706  }
   707  
   708  // TestRootRenameFrom tests linking the test case target to a known-good path.
   709  func TestRootLinkFrom(t *testing.T) {
   710  	testenv.MustHaveLink(t)
   711  	testRootMoveFrom(t, false)
   712  }
   713  
   714  func testRootMoveFrom(t *testing.T, rename bool) {
   715  	want := []byte("target")
   716  	for _, test := range rootTestCases {
   717  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   718  			if target != "" {
   719  				if err := os.WriteFile(target, want, 0o666); err != nil {
   720  					t.Fatal(err)
   721  				}
   722  			}
   723  			wantError := test.wantError
   724  			var linkTarget string
   725  			if test.ltarget != "" {
   726  				// Rename will rename the link, not the file linked to.
   727  				wantError = false
   728  				var err error
   729  				linkTarget, err = root.Readlink(test.ltarget)
   730  				if err != nil {
   731  					t.Fatalf("root.Readlink(%q) = %v, want success", test.ltarget, err)
   732  				}
   733  
   734  				// When GOOS=js, creating a hard link to a symlink fails.
   735  				if !rename && runtime.GOOS == "js" {
   736  					wantError = true
   737  				}
   738  			}
   739  
   740  			const dstPath = "destination"
   741  
   742  			// Plan 9 doesn't allow cross-directory renames.
   743  			if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
   744  				wantError = true
   745  			}
   746  
   747  			var op string
   748  			var err error
   749  			if rename {
   750  				op = "Rename"
   751  				err = root.Rename(test.open, dstPath)
   752  			} else {
   753  				op = "Link"
   754  				err = root.Link(test.open, dstPath)
   755  			}
   756  			if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, test.open, dstPath) {
   757  				return
   758  			}
   759  
   760  			origPath := target
   761  			if test.ltarget != "" {
   762  				origPath = filepath.Join(root.Name(), test.ltarget)
   763  			}
   764  			_, err = os.Lstat(origPath)
   765  			if rename {
   766  				if !errors.Is(err, os.ErrNotExist) {
   767  					t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", origPath, err)
   768  				}
   769  			} else {
   770  				if err != nil {
   771  					t.Errorf("after linking file, error accessing original: %v", err)
   772  				}
   773  			}
   774  
   775  			dstFullPath := filepath.Join(root.Name(), dstPath)
   776  			if test.ltarget != "" {
   777  				got, err := os.Readlink(dstFullPath)
   778  				if err != nil || got != linkTarget {
   779  					t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstFullPath, got, err, linkTarget)
   780  				}
   781  			} else {
   782  				got, err := os.ReadFile(dstFullPath)
   783  				if err != nil || !bytes.Equal(got, want) {
   784  					t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstFullPath, string(got), err, string(want))
   785  				}
   786  				st, err := os.Lstat(dstFullPath)
   787  				if err != nil || st.Mode()&fs.ModeSymlink != 0 {
   788  					t.Errorf(`os.Lstat(%q) = %v, %v; want non-symlink`, dstFullPath, st.Mode(), err)
   789  				}
   790  
   791  			}
   792  		})
   793  	}
   794  }
   795  
   796  // TestRootRenameTo tests renaming a known-good path to the test case target.
   797  func TestRootRenameTo(t *testing.T) {
   798  	testRootMoveTo(t, true)
   799  }
   800  
   801  // TestRootLinkTo tests renaming a known-good path to the test case target.
   802  func TestRootLinkTo(t *testing.T) {
   803  	testenv.MustHaveLink(t)
   804  	testRootMoveTo(t, true)
   805  }
   806  
   807  func testRootMoveTo(t *testing.T, rename bool) {
   808  	want := []byte("target")
   809  	for _, test := range rootTestCases {
   810  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   811  			const srcPath = "source"
   812  			if err := os.WriteFile(filepath.Join(root.Name(), srcPath), want, 0o666); err != nil {
   813  				t.Fatal(err)
   814  			}
   815  
   816  			target = test.target
   817  			wantError := test.wantError
   818  			if test.ltarget != "" {
   819  				// Rename will overwrite the final link rather than follow it.
   820  				target = test.ltarget
   821  				wantError = false
   822  			}
   823  
   824  			// Plan 9 doesn't allow cross-directory renames.
   825  			if runtime.GOOS == "plan9" && strings.Contains(test.open, "/") {
   826  				wantError = true
   827  			}
   828  
   829  			var err error
   830  			var op string
   831  			if rename {
   832  				op = "Rename"
   833  				err = root.Rename(srcPath, test.open)
   834  			} else {
   835  				op = "Link"
   836  				err = root.Link(srcPath, test.open)
   837  			}
   838  			if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, srcPath, test.open) {
   839  				return
   840  			}
   841  
   842  			_, err = os.Lstat(filepath.Join(root.Name(), srcPath))
   843  			if rename {
   844  				if !errors.Is(err, os.ErrNotExist) {
   845  					t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", srcPath, err)
   846  				}
   847  			} else {
   848  				if err != nil {
   849  					t.Errorf("after linking file, error accessing original: %v", err)
   850  				}
   851  			}
   852  
   853  			got, err := os.ReadFile(filepath.Join(root.Name(), target))
   854  			if err != nil || !bytes.Equal(got, want) {
   855  				t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, target, string(got), err, string(want))
   856  			}
   857  		})
   858  	}
   859  }
   860  
   861  func TestRootSymlink(t *testing.T) {
   862  	testenv.MustHaveSymlink(t)
   863  	for _, test := range rootTestCases {
   864  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   865  			wantError := test.wantError
   866  			if test.ltarget != "" {
   867  				// We can't create a symlink over an existing symlink.
   868  				wantError = true
   869  			}
   870  
   871  			const wantTarget = "linktarget"
   872  			err := root.Symlink(wantTarget, test.open)
   873  			if errEndsTest(t, err, wantError, "root.Symlink(%q)", test.open) {
   874  				return
   875  			}
   876  			got, err := os.Readlink(target)
   877  			if err != nil || got != wantTarget {
   878  				t.Fatalf("ReadLink(%q) = %q, %v; want %q, nil", target, got, err, wantTarget)
   879  			}
   880  		})
   881  	}
   882  }
   883  
   884  // A rootConsistencyTest is a test case comparing os.Root behavior with
   885  // the corresponding non-Root function.
   886  //
   887  // These tests verify that, for example, Root.Open("file/./") and os.Open("file/./")
   888  // have the same result, although the specific result may vary by platform.
   889  type rootConsistencyTest struct {
   890  	name string
   891  
   892  	// fs is the test filesystem layout. See makefs above.
   893  	// fsFunc is called to modify the test filesystem, or replace it.
   894  	fs     []string
   895  	fsFunc func(t *testing.T, dir string) string
   896  
   897  	// open is the filename to access in the test.
   898  	open string
   899  
   900  	// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
   901  	// function return different errors for this test.
   902  	detailedErrorMismatch func(t *testing.T) bool
   903  }
   904  
   905  var rootConsistencyTestCases = []rootConsistencyTest{{
   906  	name: "file",
   907  	fs: []string{
   908  		"target",
   909  	},
   910  	open: "target",
   911  }, {
   912  	name: "dir slash dot",
   913  	fs: []string{
   914  		"target/file",
   915  	},
   916  	open: "target/.",
   917  }, {
   918  	name: "dot",
   919  	fs: []string{
   920  		"file",
   921  	},
   922  	open: ".",
   923  }, {
   924  	name: "file slash dot",
   925  	fs: []string{
   926  		"target",
   927  	},
   928  	open: "target/.",
   929  	detailedErrorMismatch: func(t *testing.T) bool {
   930  		// FreeBSD returns EPERM in the non-Root case.
   931  		return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
   932  	},
   933  }, {
   934  	name: "dir slash",
   935  	fs: []string{
   936  		"target/file",
   937  	},
   938  	open: "target/",
   939  }, {
   940  	name: "dot slash",
   941  	fs: []string{
   942  		"file",
   943  	},
   944  	open: "./",
   945  }, {
   946  	name: "file slash",
   947  	fs: []string{
   948  		"target",
   949  	},
   950  	open: "target/",
   951  	detailedErrorMismatch: func(t *testing.T) bool {
   952  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
   953  		return runtime.GOOS == "js"
   954  	},
   955  }, {
   956  	name: "file in path",
   957  	fs: []string{
   958  		"file",
   959  	},
   960  	open: "file/target",
   961  }, {
   962  	name: "directory in path missing",
   963  	open: "dir/target",
   964  }, {
   965  	name: "target does not exist",
   966  	open: "target",
   967  }, {
   968  	name: "symlink slash",
   969  	fs: []string{
   970  		"target/file",
   971  		"link => target",
   972  	},
   973  	open: "link/",
   974  }, {
   975  	name: "symlink slash dot",
   976  	fs: []string{
   977  		"target/file",
   978  		"link => target",
   979  	},
   980  	open: "link/.",
   981  }, {
   982  	name: "file symlink slash",
   983  	fs: []string{
   984  		"target",
   985  		"link => target",
   986  	},
   987  	open: "link/",
   988  	detailedErrorMismatch: func(t *testing.T) bool {
   989  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
   990  		return runtime.GOOS == "js"
   991  	},
   992  }, {
   993  	name: "unresolved symlink",
   994  	fs: []string{
   995  		"link => target",
   996  	},
   997  	open: "link",
   998  }, {
   999  	name: "resolved symlink",
  1000  	fs: []string{
  1001  		"link => target",
  1002  		"target",
  1003  	},
  1004  	open: "link",
  1005  }, {
  1006  	name: "dotdot in path after symlink",
  1007  	fs: []string{
  1008  		"a => b/c",
  1009  		"b/c/",
  1010  		"b/target",
  1011  	},
  1012  	open: "a/../target",
  1013  }, {
  1014  	name: "long file name",
  1015  	open: strings.Repeat("a", 500),
  1016  }, {
  1017  	name: "unreadable directory",
  1018  	fs: []string{
  1019  		"dir/target",
  1020  	},
  1021  	fsFunc: func(t *testing.T, dir string) string {
  1022  		os.Chmod(filepath.Join(dir, "dir"), 0)
  1023  		t.Cleanup(func() {
  1024  			os.Chmod(filepath.Join(dir, "dir"), 0o700)
  1025  		})
  1026  		return dir
  1027  	},
  1028  	open: "dir/target",
  1029  }, {
  1030  	name: "unix domain socket target",
  1031  	fsFunc: func(t *testing.T, dir string) string {
  1032  		return tempDirWithUnixSocket(t, "a")
  1033  	},
  1034  	open: "a",
  1035  }, {
  1036  	name: "unix domain socket in path",
  1037  	fsFunc: func(t *testing.T, dir string) string {
  1038  		return tempDirWithUnixSocket(t, "a")
  1039  	},
  1040  	open: "a/b",
  1041  	detailedErrorMismatch: func(t *testing.T) bool {
  1042  		// On Windows, os.Root.Open returns "The directory name is invalid."
  1043  		// and os.Open returns "The file cannot be accessed by the system.".
  1044  		return runtime.GOOS == "windows"
  1045  	},
  1046  }, {
  1047  	name: "question mark",
  1048  	open: "?",
  1049  }, {
  1050  	name: "nul byte",
  1051  	open: "\x00",
  1052  }}
  1053  
  1054  func tempDirWithUnixSocket(t *testing.T, name string) string {
  1055  	dir, err := os.MkdirTemp("", "")
  1056  	if err != nil {
  1057  		t.Fatal(err)
  1058  	}
  1059  	t.Cleanup(func() {
  1060  		if err := os.RemoveAll(dir); err != nil {
  1061  			t.Error(err)
  1062  		}
  1063  	})
  1064  	addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name))
  1065  	if err != nil {
  1066  		t.Skipf("net.ResolveUnixAddr: %v", err)
  1067  	}
  1068  	conn, err := net.ListenUnix("unix", addr)
  1069  	if err != nil {
  1070  		t.Skipf("net.ListenUnix: %v", err)
  1071  	}
  1072  	t.Cleanup(func() {
  1073  		conn.Close()
  1074  	})
  1075  	return dir
  1076  }
  1077  
  1078  func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) {
  1079  	if runtime.GOOS == "wasip1" {
  1080  		// On wasip, non-Root functions clean paths before opening them,
  1081  		// resulting in inconsistent behavior.
  1082  		// https://go.dev/issue/69509
  1083  		t.Skip("#69509: inconsistent results on wasip1")
  1084  	}
  1085  
  1086  	t.Run(test.name, func(t *testing.T) {
  1087  		dir1 := makefs(t, test.fs)
  1088  		dir2 := makefs(t, test.fs)
  1089  		if test.fsFunc != nil {
  1090  			dir1 = test.fsFunc(t, dir1)
  1091  			dir2 = test.fsFunc(t, dir2)
  1092  		}
  1093  
  1094  		r, err := os.OpenRoot(dir1)
  1095  		if err != nil {
  1096  			t.Fatal(err)
  1097  		}
  1098  		defer r.Close()
  1099  
  1100  		res1, err1 := f(t, test.open, r)
  1101  		res2, err2 := f(t, dir2+"/"+test.open, nil)
  1102  
  1103  		if res1 != res2 || ((err1 == nil) != (err2 == nil)) {
  1104  			t.Errorf("with root:    res=%v", res1)
  1105  			t.Errorf("              err=%v", err1)
  1106  			t.Errorf("without root: res=%v", res2)
  1107  			t.Errorf("              err=%v", err2)
  1108  			t.Errorf("want consistent results, got mismatch")
  1109  		}
  1110  
  1111  		if err1 != nil || err2 != nil {
  1112  			underlyingError := func(how string, err error) error {
  1113  				switch e := err1.(type) {
  1114  				case *os.PathError:
  1115  					return e.Err
  1116  				case *os.LinkError:
  1117  					return e.Err
  1118  				default:
  1119  					t.Fatalf("%v, expected PathError or LinkError; got: %v", how, err)
  1120  				}
  1121  				return nil
  1122  			}
  1123  			e1 := underlyingError("with root", err1)
  1124  			e2 := underlyingError("without root", err1)
  1125  			detailedErrorMismatch := false
  1126  			if f := test.detailedErrorMismatch; f != nil {
  1127  				detailedErrorMismatch = f(t)
  1128  			}
  1129  			if runtime.GOOS == "plan9" {
  1130  				// Plan9 syscall errors aren't comparable.
  1131  				detailedErrorMismatch = true
  1132  			}
  1133  			if !detailedErrorMismatch && e1 != e2 {
  1134  				t.Errorf("with root:    err=%v", e1)
  1135  				t.Errorf("without root: err=%v", e2)
  1136  				t.Errorf("want consistent results, got mismatch")
  1137  			}
  1138  		}
  1139  	})
  1140  }
  1141  
  1142  func TestRootConsistencyOpen(t *testing.T) {
  1143  	for _, test := range rootConsistencyTestCases {
  1144  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1145  			var f *os.File
  1146  			var err error
  1147  			if r == nil {
  1148  				f, err = os.Open(path)
  1149  			} else {
  1150  				f, err = r.Open(path)
  1151  			}
  1152  			if err != nil {
  1153  				return "", err
  1154  			}
  1155  			defer f.Close()
  1156  			fi, err := f.Stat()
  1157  			if err == nil && !fi.IsDir() {
  1158  				b, err := io.ReadAll(f)
  1159  				return string(b), err
  1160  			} else {
  1161  				names, err := f.Readdirnames(-1)
  1162  				slices.Sort(names)
  1163  				return fmt.Sprintf("%q", names), err
  1164  			}
  1165  		})
  1166  	}
  1167  }
  1168  
  1169  func TestRootConsistencyCreate(t *testing.T) {
  1170  	for _, test := range rootConsistencyTestCases {
  1171  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1172  			var f *os.File
  1173  			var err error
  1174  			if r == nil {
  1175  				f, err = os.Create(path)
  1176  			} else {
  1177  				f, err = r.Create(path)
  1178  			}
  1179  			if err == nil {
  1180  				f.Write([]byte("file contents"))
  1181  				f.Close()
  1182  			}
  1183  			return "", err
  1184  		})
  1185  	}
  1186  }
  1187  
  1188  func TestRootConsistencyChmod(t *testing.T) {
  1189  	if runtime.GOOS == "wasip1" {
  1190  		t.Skip("Chmod not supported on " + runtime.GOOS)
  1191  	}
  1192  	for _, test := range rootConsistencyTestCases {
  1193  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1194  			chmod := os.Chmod
  1195  			lstat := os.Lstat
  1196  			if r != nil {
  1197  				chmod = r.Chmod
  1198  				lstat = r.Lstat
  1199  			}
  1200  
  1201  			var m1, m2 os.FileMode
  1202  			if err := chmod(path, 0o555); err != nil {
  1203  				return "chmod 0o555", err
  1204  			}
  1205  			fi, err := lstat(path)
  1206  			if err == nil {
  1207  				m1 = fi.Mode()
  1208  			}
  1209  			if err = chmod(path, 0o777); err != nil {
  1210  				return "chmod 0o777", err
  1211  			}
  1212  			fi, err = lstat(path)
  1213  			if err == nil {
  1214  				m2 = fi.Mode()
  1215  			}
  1216  			return fmt.Sprintf("%v %v", m1, m2), err
  1217  		})
  1218  	}
  1219  }
  1220  
  1221  func TestRootConsistencyMkdir(t *testing.T) {
  1222  	for _, test := range rootConsistencyTestCases {
  1223  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1224  			var err error
  1225  			if r == nil {
  1226  				err = os.Mkdir(path, 0o777)
  1227  			} else {
  1228  				err = r.Mkdir(path, 0o777)
  1229  			}
  1230  			return "", err
  1231  		})
  1232  	}
  1233  }
  1234  
  1235  func TestRootConsistencyRemove(t *testing.T) {
  1236  	for _, test := range rootConsistencyTestCases {
  1237  		if test.open == "." || test.open == "./" {
  1238  			continue // can't remove the root itself
  1239  		}
  1240  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1241  			var err error
  1242  			if r == nil {
  1243  				err = os.Remove(path)
  1244  			} else {
  1245  				err = r.Remove(path)
  1246  			}
  1247  			return "", err
  1248  		})
  1249  	}
  1250  }
  1251  
  1252  func TestRootConsistencyStat(t *testing.T) {
  1253  	for _, test := range rootConsistencyTestCases {
  1254  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1255  			var fi os.FileInfo
  1256  			var err error
  1257  			if r == nil {
  1258  				fi, err = os.Stat(path)
  1259  			} else {
  1260  				fi, err = r.Stat(path)
  1261  			}
  1262  			if err != nil {
  1263  				return "", err
  1264  			}
  1265  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1266  		})
  1267  	}
  1268  }
  1269  
  1270  func TestRootConsistencyLstat(t *testing.T) {
  1271  	for _, test := range rootConsistencyTestCases {
  1272  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1273  			var fi os.FileInfo
  1274  			var err error
  1275  			if r == nil {
  1276  				fi, err = os.Lstat(path)
  1277  			} else {
  1278  				fi, err = r.Lstat(path)
  1279  			}
  1280  			if err != nil {
  1281  				return "", err
  1282  			}
  1283  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1284  		})
  1285  	}
  1286  }
  1287  
  1288  func TestRootConsistencyReadlink(t *testing.T) {
  1289  	for _, test := range rootConsistencyTestCases {
  1290  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1291  			if r == nil {
  1292  				return os.Readlink(path)
  1293  			} else {
  1294  				return r.Readlink(path)
  1295  			}
  1296  		})
  1297  	}
  1298  }
  1299  
  1300  func TestRootConsistencyRename(t *testing.T) {
  1301  	testRootConsistencyMove(t, true)
  1302  }
  1303  
  1304  func TestRootConsistencyLink(t *testing.T) {
  1305  	testenv.MustHaveLink(t)
  1306  	testRootConsistencyMove(t, false)
  1307  }
  1308  
  1309  func testRootConsistencyMove(t *testing.T, rename bool) {
  1310  	if runtime.GOOS == "plan9" {
  1311  		// This test depends on moving files between directories.
  1312  		t.Skip("Plan 9 does not support cross-directory renames")
  1313  	}
  1314  	// Run this test in two directions:
  1315  	// Renaming the test path to a known-good path (from),
  1316  	// and renaming a known-good path to the test path (to).
  1317  	for _, name := range []string{"from", "to"} {
  1318  		t.Run(name, func(t *testing.T) {
  1319  			for _, test := range rootConsistencyTestCases {
  1320  				if runtime.GOOS == "windows" {
  1321  					// On Windows, Rename("/path/to/.", x) succeeds,
  1322  					// because Windows cleans the path to just "/path/to".
  1323  					// Root.Rename(".", x) fails as expected.
  1324  					// Don't run this consistency test on Windows.
  1325  					if test.open == "." || test.open == "./" {
  1326  						continue
  1327  					}
  1328  				}
  1329  
  1330  				test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1331  					var move func(oldname, newname string) error
  1332  					switch {
  1333  					case rename && r == nil:
  1334  						move = os.Rename
  1335  					case rename && r != nil:
  1336  						move = r.Rename
  1337  					case !rename && r == nil:
  1338  						move = os.Link
  1339  					case !rename && r != nil:
  1340  						move = r.Link
  1341  					}
  1342  					lstat := os.Lstat
  1343  					if r != nil {
  1344  						lstat = r.Lstat
  1345  					}
  1346  
  1347  					otherPath := "other"
  1348  					if r == nil {
  1349  						otherPath = filepath.Join(t.TempDir(), otherPath)
  1350  					}
  1351  
  1352  					var srcPath, dstPath string
  1353  					if name == "from" {
  1354  						srcPath = path
  1355  						dstPath = otherPath
  1356  					} else {
  1357  						srcPath = otherPath
  1358  						dstPath = path
  1359  					}
  1360  
  1361  					if !rename {
  1362  						// When the source is a symlink, Root.Link creates
  1363  						// a hard link to the symlink.
  1364  						// os.Link does whatever the link syscall does,
  1365  						// which varies between operating systems and
  1366  						// their versions.
  1367  						// Skip running the consistency test when
  1368  						// the source is a symlink.
  1369  						fi, err := lstat(srcPath)
  1370  						if err == nil && fi.Mode()&os.ModeSymlink != 0 {
  1371  							return "", nil
  1372  						}
  1373  					}
  1374  
  1375  					if err := move(srcPath, dstPath); err != nil {
  1376  						return "", err
  1377  					}
  1378  					fi, err := lstat(dstPath)
  1379  					if err != nil {
  1380  						t.Errorf("stat(%q) after successful copy: %v", dstPath, err)
  1381  						return "stat error", err
  1382  					}
  1383  					return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
  1384  				})
  1385  			}
  1386  		})
  1387  	}
  1388  }
  1389  
  1390  func TestRootConsistencySymlink(t *testing.T) {
  1391  	testenv.MustHaveSymlink(t)
  1392  	for _, test := range rootConsistencyTestCases {
  1393  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
  1394  			const target = "linktarget"
  1395  			var err error
  1396  			var got string
  1397  			if r == nil {
  1398  				err = os.Symlink(target, path)
  1399  				got, _ = os.Readlink(target)
  1400  			} else {
  1401  				err = r.Symlink(target, path)
  1402  				got, _ = r.Readlink(target)
  1403  			}
  1404  			return got, err
  1405  		})
  1406  	}
  1407  }
  1408  
  1409  func TestRootRenameAfterOpen(t *testing.T) {
  1410  	switch runtime.GOOS {
  1411  	case "windows":
  1412  		t.Skip("renaming open files not supported on " + runtime.GOOS)
  1413  	case "js", "plan9":
  1414  		t.Skip("openat not supported on " + runtime.GOOS)
  1415  	case "wasip1":
  1416  		if os.Getenv("GOWASIRUNTIME") == "wazero" {
  1417  			t.Skip("wazero does not track renamed directories")
  1418  		}
  1419  	}
  1420  
  1421  	dir := t.TempDir()
  1422  
  1423  	// Create directory "a" and open it.
  1424  	if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil {
  1425  		t.Fatal(err)
  1426  	}
  1427  	dirf, err := os.OpenRoot(filepath.Join(dir, "a"))
  1428  	if err != nil {
  1429  		t.Fatal(err)
  1430  	}
  1431  	defer dirf.Close()
  1432  
  1433  	// Rename "a" => "b", and create "b/f".
  1434  	if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil {
  1435  		t.Fatal(err)
  1436  	}
  1437  	if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil {
  1438  		t.Fatal(err)
  1439  	}
  1440  
  1441  	// Open "f", and confirm that we see it.
  1442  	f, err := dirf.OpenFile("f", os.O_RDONLY, 0)
  1443  	if err != nil {
  1444  		t.Fatalf("reading file after renaming parent: %v", err)
  1445  	}
  1446  	defer f.Close()
  1447  	b, err := io.ReadAll(f)
  1448  	if err != nil {
  1449  		t.Fatal(err)
  1450  	}
  1451  	if got, want := string(b), "hello"; got != want {
  1452  		t.Fatalf("file contents: %q, want %q", got, want)
  1453  	}
  1454  
  1455  	// f.Name reflects the original path we opened the directory under (".../a"), not "b".
  1456  	if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want {
  1457  		t.Errorf("f.Name() = %q, want %q", got, want)
  1458  	}
  1459  }
  1460  
  1461  func TestRootNonPermissionMode(t *testing.T) {
  1462  	r, err := os.OpenRoot(t.TempDir())
  1463  	if err != nil {
  1464  		t.Fatal(err)
  1465  	}
  1466  	defer r.Close()
  1467  	if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil {
  1468  		t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error")
  1469  	}
  1470  	if err := r.Mkdir("file", 0o1777); err == nil {
  1471  		t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error")
  1472  	}
  1473  }
  1474  
  1475  func TestRootUseAfterClose(t *testing.T) {
  1476  	r, err := os.OpenRoot(t.TempDir())
  1477  	if err != nil {
  1478  		t.Fatal(err)
  1479  	}
  1480  	r.Close()
  1481  	for _, test := range []struct {
  1482  		name string
  1483  		f    func(r *os.Root, filename string) error
  1484  	}{{
  1485  		name: "Open",
  1486  		f: func(r *os.Root, filename string) error {
  1487  			_, err := r.Open(filename)
  1488  			return err
  1489  		},
  1490  	}, {
  1491  		name: "Create",
  1492  		f: func(r *os.Root, filename string) error {
  1493  			_, err := r.Create(filename)
  1494  			return err
  1495  		},
  1496  	}, {
  1497  		name: "OpenFile",
  1498  		f: func(r *os.Root, filename string) error {
  1499  			_, err := r.OpenFile(filename, os.O_RDWR, 0o666)
  1500  			return err
  1501  		},
  1502  	}, {
  1503  		name: "OpenRoot",
  1504  		f: func(r *os.Root, filename string) error {
  1505  			_, err := r.OpenRoot(filename)
  1506  			return err
  1507  		},
  1508  	}, {
  1509  		name: "Mkdir",
  1510  		f: func(r *os.Root, filename string) error {
  1511  			return r.Mkdir(filename, 0o777)
  1512  		},
  1513  	}} {
  1514  		err := test.f(r, "target")
  1515  		pe, ok := err.(*os.PathError)
  1516  		if !ok || pe.Path != "target" || pe.Err != os.ErrClosed {
  1517  			t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err)
  1518  		}
  1519  	}
  1520  }
  1521  
  1522  func TestRootConcurrentClose(t *testing.T) {
  1523  	r, err := os.OpenRoot(t.TempDir())
  1524  	if err != nil {
  1525  		t.Fatal(err)
  1526  	}
  1527  	ch := make(chan error, 1)
  1528  	go func() {
  1529  		defer close(ch)
  1530  		first := true
  1531  		for {
  1532  			f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666)
  1533  			if err != nil {
  1534  				ch <- err
  1535  				return
  1536  			}
  1537  			if first {
  1538  				ch <- nil
  1539  				first = false
  1540  			}
  1541  			f.Close()
  1542  			if runtime.GOARCH == "wasm" {
  1543  				// TODO(go.dev/issue/71134) can lead to goroutine starvation.
  1544  				runtime.Gosched()
  1545  			}
  1546  		}
  1547  	}()
  1548  	if err := <-ch; err != nil {
  1549  		t.Errorf("OpenFile: %v, want success", err)
  1550  	}
  1551  	r.Close()
  1552  	if err := <-ch; !errors.Is(err, os.ErrClosed) {
  1553  		t.Errorf("OpenFile: %v, want ErrClosed", err)
  1554  	}
  1555  }
  1556  
  1557  // TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse.
  1558  //
  1559  // We create a deeply nested directory:
  1560  //
  1561  //	base/a/a/a/a/ [...] /a
  1562  //
  1563  // And a path that descends into the tree, then returns to the top using ..:
  1564  //
  1565  //	base/a/a/a/a/ [...] /a/../../../ [..] /../a/f
  1566  //
  1567  // While opening this file, we rename base/a/a to base/b.
  1568  // A naive lookup operation will resolve the path to base/f.
  1569  func TestRootRaceRenameDir(t *testing.T) {
  1570  	dir := t.TempDir()
  1571  	r, err := os.OpenRoot(dir)
  1572  	if err != nil {
  1573  		t.Fatal(err)
  1574  	}
  1575  	defer r.Close()
  1576  
  1577  	const depth = 4
  1578  
  1579  	os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777)
  1580  
  1581  	path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f"
  1582  	os.WriteFile(dir+"/f", []byte("secret"), 0o666)
  1583  	os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666)
  1584  
  1585  	// Compute how long it takes to open the path in the common case.
  1586  	const tries = 10
  1587  	var total time.Duration
  1588  	for range tries {
  1589  		start := time.Now()
  1590  		f, err := r.Open(path)
  1591  		if err != nil {
  1592  			t.Fatal(err)
  1593  		}
  1594  		b, err := io.ReadAll(f)
  1595  		if err != nil {
  1596  			t.Fatal(err)
  1597  		}
  1598  		if string(b) != "public" {
  1599  			t.Fatalf("read %q, want %q", b, "public")
  1600  		}
  1601  		f.Close()
  1602  		total += time.Since(start)
  1603  	}
  1604  	avg := total / tries
  1605  
  1606  	// We're trying to exploit a race, so try this a number of times.
  1607  	for range 100 {
  1608  		// Start a goroutine to open the file.
  1609  		gotc := make(chan []byte)
  1610  		go func() {
  1611  			f, err := r.Open(path)
  1612  			if err != nil {
  1613  				gotc <- nil
  1614  			}
  1615  			defer f.Close()
  1616  			b, _ := io.ReadAll(f)
  1617  			gotc <- b
  1618  		}()
  1619  
  1620  		// Wait for the open operation to partially complete,
  1621  		// and then rename a directory near the root.
  1622  		time.Sleep(avg / 4)
  1623  		if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil {
  1624  			// Windows and Plan9 won't let us rename a directory if we have
  1625  			// an open handle for it, so an error here is expected.
  1626  			switch runtime.GOOS {
  1627  			case "windows", "plan9":
  1628  			default:
  1629  				t.Fatal(err)
  1630  			}
  1631  		}
  1632  
  1633  		got := <-gotc
  1634  		os.Rename(dir+"/b", dir+"/base/a")
  1635  		if len(got) > 0 && string(got) != "public" {
  1636  			t.Errorf("read file: %q; want error or 'public'", got)
  1637  		}
  1638  	}
  1639  }
  1640  
  1641  func TestRootSymlinkToRoot(t *testing.T) {
  1642  	dir := makefs(t, []string{
  1643  		"d/d => ..",
  1644  	})
  1645  	root, err := os.OpenRoot(dir)
  1646  	if err != nil {
  1647  		t.Fatal(err)
  1648  	}
  1649  	defer root.Close()
  1650  	if err := root.Mkdir("d/d/new", 0777); err != nil {
  1651  		t.Fatal(err)
  1652  	}
  1653  	f, err := root.Open("d/d")
  1654  	if err != nil {
  1655  		t.Fatal(err)
  1656  	}
  1657  	defer f.Close()
  1658  	names, err := f.Readdirnames(-1)
  1659  	if err != nil {
  1660  		t.Fatal(err)
  1661  	}
  1662  	slices.Sort(names)
  1663  	if got, want := names, []string{"d", "new"}; !slices.Equal(got, want) {
  1664  		t.Errorf("root contains: %q, want %q", got, want)
  1665  	}
  1666  }
  1667  
  1668  func TestOpenInRoot(t *testing.T) {
  1669  	dir := makefs(t, []string{
  1670  		"file",
  1671  		"link => ../ROOT/file",
  1672  	})
  1673  	f, err := os.OpenInRoot(dir, "file")
  1674  	if err != nil {
  1675  		t.Fatalf("OpenInRoot(`file`) = %v, want success", err)
  1676  	}
  1677  	f.Close()
  1678  	for _, name := range []string{
  1679  		"link",
  1680  		"../ROOT/file",
  1681  		dir + "/file",
  1682  	} {
  1683  		f, err := os.OpenInRoot(dir, name)
  1684  		if err == nil {
  1685  			f.Close()
  1686  			t.Fatalf("OpenInRoot(%q) = nil, want error", name)
  1687  		}
  1688  	}
  1689  }
  1690  

View as plain text