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

View as plain text