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  	"io"
    12  	"io/fs"
    13  	"net"
    14  	"os"
    15  	"path"
    16  	"path/filepath"
    17  	"runtime"
    18  	"slices"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  )
    23  
    24  // testMaybeRooted calls f in two subtests,
    25  // one with a Root and one with a nil r.
    26  func testMaybeRooted(t *testing.T, f func(t *testing.T, r *os.Root)) {
    27  	t.Run("NoRoot", func(t *testing.T) {
    28  		t.Chdir(t.TempDir())
    29  		f(t, nil)
    30  	})
    31  	t.Run("InRoot", func(t *testing.T) {
    32  		t.Chdir(t.TempDir())
    33  		r, err := os.OpenRoot(".")
    34  		if err != nil {
    35  			t.Fatal(err)
    36  		}
    37  		defer r.Close()
    38  		f(t, r)
    39  	})
    40  }
    41  
    42  // makefs creates a test filesystem layout and returns the path to its root.
    43  //
    44  // Each entry in the slice is a file, directory, or symbolic link to create:
    45  //
    46  //   - "d/": directory d
    47  //   - "f": file f with contents f
    48  //   - "a => b": symlink a with target b
    49  //
    50  // The directory containing the filesystem is always named ROOT.
    51  // $ABS is replaced with the absolute path of the directory containing the filesystem.
    52  //
    53  // Parent directories are automatically created as needed.
    54  //
    55  // makefs calls t.Skip if the layout contains features not supported by the current GOOS.
    56  func makefs(t *testing.T, fs []string) string {
    57  	root := path.Join(t.TempDir(), "ROOT")
    58  	if err := os.Mkdir(root, 0o777); err != nil {
    59  		t.Fatal(err)
    60  	}
    61  	for _, ent := range fs {
    62  		ent = strings.ReplaceAll(ent, "$ABS", root)
    63  		base, link, isLink := strings.Cut(ent, " => ")
    64  		if isLink {
    65  			if runtime.GOOS == "wasip1" && path.IsAbs(link) {
    66  				t.Skip("absolute link targets not supported on " + runtime.GOOS)
    67  			}
    68  			if runtime.GOOS == "plan9" {
    69  				t.Skip("symlinks not supported on " + runtime.GOOS)
    70  			}
    71  			ent = base
    72  		}
    73  		if err := os.MkdirAll(path.Join(root, path.Dir(base)), 0o777); err != nil {
    74  			t.Fatal(err)
    75  		}
    76  		if isLink {
    77  			if err := os.Symlink(link, path.Join(root, base)); err != nil {
    78  				t.Fatal(err)
    79  			}
    80  		} else if strings.HasSuffix(ent, "/") {
    81  			if err := os.MkdirAll(path.Join(root, ent), 0o777); err != nil {
    82  				t.Fatal(err)
    83  			}
    84  		} else {
    85  			if err := os.WriteFile(path.Join(root, ent), []byte(ent), 0o666); err != nil {
    86  				t.Fatal(err)
    87  			}
    88  		}
    89  	}
    90  	return root
    91  }
    92  
    93  // A rootTest is a test case for os.Root.
    94  type rootTest struct {
    95  	name string
    96  
    97  	// fs is the test filesystem layout. See makefs above.
    98  	fs []string
    99  
   100  	// open is the filename to access in the test.
   101  	open string
   102  
   103  	// target is the filename that we expect to be accessed, after resolving all symlinks.
   104  	// For test cases where the operation fails due to an escaping path such as ../ROOT/x,
   105  	// the target is the filename that should not have been opened.
   106  	target string
   107  
   108  	// ltarget is the filename that we expect to accessed, after resolving all symlinks
   109  	// except the last one. This is the file we expect to be removed by Remove or statted
   110  	// by Lstat.
   111  	//
   112  	// If the last path component in open is not a symlink, ltarget should be "".
   113  	ltarget string
   114  
   115  	// wantError is true if accessing the file should fail.
   116  	wantError bool
   117  
   118  	// alwaysFails is true if the open operation is expected to fail
   119  	// even when using non-openat operations.
   120  	//
   121  	// This lets us check that tests that are expected to fail because (for example)
   122  	// a path escapes the directory root will succeed when the escaping checks are not
   123  	// performed.
   124  	alwaysFails bool
   125  }
   126  
   127  // run sets up the test filesystem layout, os.OpenDirs the root, and calls f.
   128  func (test *rootTest) run(t *testing.T, f func(t *testing.T, target string, d *os.Root)) {
   129  	t.Run(test.name, func(t *testing.T) {
   130  		root := makefs(t, test.fs)
   131  		d, err := os.OpenRoot(root)
   132  		if err != nil {
   133  			t.Fatal(err)
   134  		}
   135  		defer d.Close()
   136  		// The target is a file that will be accessed,
   137  		// or a file that should not be accessed
   138  		// (because doing so escapes the root).
   139  		target := test.target
   140  		if test.target != "" {
   141  			target = filepath.Join(root, test.target)
   142  		}
   143  		f(t, target, d)
   144  	})
   145  }
   146  
   147  // errEndsTest checks the error result of a test,
   148  // verifying that it succeeded or failed as expected.
   149  //
   150  // It returns true if the test is done due to encountering an expected error.
   151  // false if the test should continue.
   152  func errEndsTest(t *testing.T, err error, wantError bool, format string, args ...any) bool {
   153  	t.Helper()
   154  	if wantError {
   155  		if err == nil {
   156  			op := fmt.Sprintf(format, args...)
   157  			t.Fatalf("%v = nil; want error", op)
   158  		}
   159  		return true
   160  	} else {
   161  		if err != nil {
   162  			op := fmt.Sprintf(format, args...)
   163  			t.Fatalf("%v = %v; want success", op, err)
   164  		}
   165  		return false
   166  	}
   167  }
   168  
   169  var rootTestCases = []rootTest{{
   170  	name:   "plain path",
   171  	fs:     []string{},
   172  	open:   "target",
   173  	target: "target",
   174  }, {
   175  	name: "path in directory",
   176  	fs: []string{
   177  		"a/b/c/",
   178  	},
   179  	open:   "a/b/c/target",
   180  	target: "a/b/c/target",
   181  }, {
   182  	name: "symlink",
   183  	fs: []string{
   184  		"link => target",
   185  	},
   186  	open:    "link",
   187  	target:  "target",
   188  	ltarget: "link",
   189  }, {
   190  	name: "symlink dotdot slash",
   191  	fs: []string{
   192  		"link => ../",
   193  	},
   194  	open:      "link",
   195  	ltarget:   "link",
   196  	wantError: true,
   197  }, {
   198  	name: "symlink ending in slash",
   199  	fs: []string{
   200  		"dir/",
   201  		"link => dir/",
   202  	},
   203  	open:   "link/target",
   204  	target: "dir/target",
   205  }, {
   206  	name: "symlink dotdot dotdot slash",
   207  	fs: []string{
   208  		"dir/link => ../../",
   209  	},
   210  	open:      "dir/link",
   211  	ltarget:   "dir/link",
   212  	wantError: true,
   213  }, {
   214  	name: "symlink chain",
   215  	fs: []string{
   216  		"link => a/b/c/target",
   217  		"a/b => e",
   218  		"a/e => ../f",
   219  		"f => g/h/i",
   220  		"g/h/i => ..",
   221  		"g/c/",
   222  	},
   223  	open:    "link",
   224  	target:  "g/c/target",
   225  	ltarget: "link",
   226  }, {
   227  	name: "path with dot",
   228  	fs: []string{
   229  		"a/b/",
   230  	},
   231  	open:   "./a/./b/./target",
   232  	target: "a/b/target",
   233  }, {
   234  	name: "path with dotdot",
   235  	fs: []string{
   236  		"a/b/",
   237  	},
   238  	open:   "a/../a/b/../../a/b/../b/target",
   239  	target: "a/b/target",
   240  }, {
   241  	name:      "path with dotdot slash",
   242  	fs:        []string{},
   243  	open:      "../",
   244  	wantError: true,
   245  }, {
   246  	name:      "path with dotdot dotdot slash",
   247  	fs:        []string{},
   248  	open:      "a/../../",
   249  	wantError: true,
   250  }, {
   251  	name: "dotdot no symlink",
   252  	fs: []string{
   253  		"a/",
   254  	},
   255  	open:   "a/../target",
   256  	target: "target",
   257  }, {
   258  	name: "dotdot after symlink",
   259  	fs: []string{
   260  		"a => b/c",
   261  		"b/c/",
   262  	},
   263  	open: "a/../target",
   264  	target: func() string {
   265  		if runtime.GOOS == "windows" {
   266  			// On Windows, the path is cleaned before symlink resolution.
   267  			return "target"
   268  		}
   269  		return "b/target"
   270  	}(),
   271  }, {
   272  	name: "dotdot before symlink",
   273  	fs: []string{
   274  		"a => b/c",
   275  		"b/c/",
   276  	},
   277  	open:   "b/../a/target",
   278  	target: "b/c/target",
   279  }, {
   280  	name: "symlink ends in dot",
   281  	fs: []string{
   282  		"a => b/.",
   283  		"b/",
   284  	},
   285  	open:   "a/target",
   286  	target: "b/target",
   287  }, {
   288  	name:        "directory does not exist",
   289  	fs:          []string{},
   290  	open:        "a/file",
   291  	wantError:   true,
   292  	alwaysFails: true,
   293  }, {
   294  	name:        "empty path",
   295  	fs:          []string{},
   296  	open:        "",
   297  	wantError:   true,
   298  	alwaysFails: true,
   299  }, {
   300  	name: "symlink cycle",
   301  	fs: []string{
   302  		"a => a",
   303  	},
   304  	open:        "a",
   305  	ltarget:     "a",
   306  	wantError:   true,
   307  	alwaysFails: true,
   308  }, {
   309  	name:      "path escapes",
   310  	fs:        []string{},
   311  	open:      "../ROOT/target",
   312  	target:    "target",
   313  	wantError: true,
   314  }, {
   315  	name: "long path escapes",
   316  	fs: []string{
   317  		"a/",
   318  	},
   319  	open:      "a/../../ROOT/target",
   320  	target:    "target",
   321  	wantError: true,
   322  }, {
   323  	name: "absolute symlink",
   324  	fs: []string{
   325  		"link => $ABS/target",
   326  	},
   327  	open:      "link",
   328  	ltarget:   "link",
   329  	target:    "target",
   330  	wantError: true,
   331  }, {
   332  	name: "relative symlink",
   333  	fs: []string{
   334  		"link => ../ROOT/target",
   335  	},
   336  	open:      "link",
   337  	target:    "target",
   338  	ltarget:   "link",
   339  	wantError: true,
   340  }, {
   341  	name: "symlink chain escapes",
   342  	fs: []string{
   343  		"link => a/b/c/target",
   344  		"a/b => e",
   345  		"a/e => ../../ROOT",
   346  		"c/",
   347  	},
   348  	open:      "link",
   349  	target:    "c/target",
   350  	ltarget:   "link",
   351  	wantError: true,
   352  }}
   353  
   354  func TestRootOpen_File(t *testing.T) {
   355  	want := []byte("target")
   356  	for _, test := range rootTestCases {
   357  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   358  			if target != "" {
   359  				if err := os.WriteFile(target, want, 0o666); err != nil {
   360  					t.Fatal(err)
   361  				}
   362  			}
   363  			f, err := root.Open(test.open)
   364  			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
   365  				return
   366  			}
   367  			defer f.Close()
   368  			got, err := io.ReadAll(f)
   369  			if err != nil || !bytes.Equal(got, want) {
   370  				t.Errorf(`Dir.Open(%q): read content %q, %v; want %q`, test.open, string(got), err, string(want))
   371  			}
   372  		})
   373  	}
   374  }
   375  
   376  func TestRootOpen_Directory(t *testing.T) {
   377  	for _, test := range rootTestCases {
   378  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   379  			if target != "" {
   380  				if err := os.Mkdir(target, 0o777); err != nil {
   381  					t.Fatal(err)
   382  				}
   383  				if err := os.WriteFile(target+"/found", nil, 0o666); err != nil {
   384  					t.Fatal(err)
   385  				}
   386  			}
   387  			f, err := root.Open(test.open)
   388  			if errEndsTest(t, err, test.wantError, "root.Open(%q)", test.open) {
   389  				return
   390  			}
   391  			defer f.Close()
   392  			got, err := f.Readdirnames(-1)
   393  			if err != nil {
   394  				t.Errorf(`Dir.Open(%q).Readdirnames: %v`, test.open, err)
   395  			}
   396  			if want := []string{"found"}; !slices.Equal(got, want) {
   397  				t.Errorf(`Dir.Open(%q).Readdirnames: %q, want %q`, test.open, got, want)
   398  			}
   399  		})
   400  	}
   401  }
   402  
   403  func TestRootCreate(t *testing.T) {
   404  	want := []byte("target")
   405  	for _, test := range rootTestCases {
   406  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   407  			f, err := root.Create(test.open)
   408  			if errEndsTest(t, err, test.wantError, "root.Create(%q)", test.open) {
   409  				return
   410  			}
   411  			if _, err := f.Write(want); err != nil {
   412  				t.Fatal(err)
   413  			}
   414  			f.Close()
   415  			got, err := os.ReadFile(target)
   416  			if err != nil {
   417  				t.Fatalf(`reading file created with root.Create(%q): %v`, test.open, err)
   418  			}
   419  			if !bytes.Equal(got, want) {
   420  				t.Fatalf(`reading file created with root.Create(%q): got %q; want %q`, test.open, got, want)
   421  			}
   422  		})
   423  	}
   424  }
   425  
   426  func TestRootMkdir(t *testing.T) {
   427  	for _, test := range rootTestCases {
   428  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   429  			wantError := test.wantError
   430  			if !wantError {
   431  				fi, err := os.Lstat(filepath.Join(root.Name(), test.open))
   432  				if err == nil && fi.Mode().Type() == fs.ModeSymlink {
   433  					// This case is trying to mkdir("some symlink"),
   434  					// which is an error.
   435  					wantError = true
   436  				}
   437  			}
   438  
   439  			err := root.Mkdir(test.open, 0o777)
   440  			if errEndsTest(t, err, wantError, "root.Create(%q)", test.open) {
   441  				return
   442  			}
   443  			fi, err := os.Lstat(target)
   444  			if err != nil {
   445  				t.Fatalf(`stat file created with Root.Mkdir(%q): %v`, test.open, err)
   446  			}
   447  			if !fi.IsDir() {
   448  				t.Fatalf(`stat file created with Root.Mkdir(%q): not a directory`, test.open)
   449  			}
   450  			if mode := fi.Mode(); mode&0o777 == 0 {
   451  				// Issue #73559: We're not going to worry about the exact
   452  				// mode bits (which will have been modified by umask),
   453  				// but there should be mode bits.
   454  				t.Fatalf(`stat file created with Root.Mkdir(%q): mode=%v, want non-zero`, test.open, mode)
   455  			}
   456  		})
   457  	}
   458  }
   459  
   460  func TestRootOpenRoot(t *testing.T) {
   461  	for _, test := range rootTestCases {
   462  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   463  			if target != "" {
   464  				if err := os.Mkdir(target, 0o777); err != nil {
   465  					t.Fatal(err)
   466  				}
   467  				if err := os.WriteFile(target+"/f", nil, 0o666); err != nil {
   468  					t.Fatal(err)
   469  				}
   470  			}
   471  			rr, err := root.OpenRoot(test.open)
   472  			if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) {
   473  				return
   474  			}
   475  			defer rr.Close()
   476  			f, err := rr.Open("f")
   477  			if err != nil {
   478  				t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err)
   479  			}
   480  			f.Close()
   481  		})
   482  	}
   483  }
   484  
   485  func TestRootRemoveFile(t *testing.T) {
   486  	for _, test := range rootTestCases {
   487  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   488  			wantError := test.wantError
   489  			if test.ltarget != "" {
   490  				// Remove doesn't follow symlinks in the final path component,
   491  				// so it will successfully remove ltarget.
   492  				wantError = false
   493  				target = filepath.Join(root.Name(), test.ltarget)
   494  			} else if target != "" {
   495  				if err := os.WriteFile(target, nil, 0o666); err != nil {
   496  					t.Fatal(err)
   497  				}
   498  			}
   499  
   500  			err := root.Remove(test.open)
   501  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   502  				return
   503  			}
   504  			_, err = os.Lstat(target)
   505  			if !errors.Is(err, os.ErrNotExist) {
   506  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   507  			}
   508  		})
   509  	}
   510  }
   511  
   512  func TestRootRemoveDirectory(t *testing.T) {
   513  	for _, test := range rootTestCases {
   514  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   515  			wantError := test.wantError
   516  			if test.ltarget != "" {
   517  				// Remove doesn't follow symlinks in the final path component,
   518  				// so it will successfully remove ltarget.
   519  				wantError = false
   520  				target = filepath.Join(root.Name(), test.ltarget)
   521  			} else if target != "" {
   522  				if err := os.Mkdir(target, 0o777); err != nil {
   523  					t.Fatal(err)
   524  				}
   525  			}
   526  
   527  			err := root.Remove(test.open)
   528  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   529  				return
   530  			}
   531  			_, err = os.Lstat(target)
   532  			if !errors.Is(err, os.ErrNotExist) {
   533  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   534  			}
   535  		})
   536  	}
   537  }
   538  
   539  func TestRootOpenFileAsRoot(t *testing.T) {
   540  	dir := t.TempDir()
   541  	target := filepath.Join(dir, "target")
   542  	if err := os.WriteFile(target, nil, 0o666); err != nil {
   543  		t.Fatal(err)
   544  	}
   545  	_, err := os.OpenRoot(target)
   546  	if err == nil {
   547  		t.Fatal("os.OpenRoot(file) succeeded; want failure")
   548  	}
   549  	r, err := os.OpenRoot(dir)
   550  	if err != nil {
   551  		t.Fatal(err)
   552  	}
   553  	defer r.Close()
   554  	_, err = r.OpenRoot("target")
   555  	if err == nil {
   556  		t.Fatal("Root.OpenRoot(file) succeeded; want failure")
   557  	}
   558  }
   559  
   560  func TestRootStat(t *testing.T) {
   561  	for _, test := range rootTestCases {
   562  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   563  			const content = "content"
   564  			if target != "" {
   565  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   566  					t.Fatal(err)
   567  				}
   568  			}
   569  
   570  			fi, err := root.Stat(test.open)
   571  			if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) {
   572  				return
   573  			}
   574  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   575  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   576  			}
   577  			if got, want := fi.Size(), int64(len(content)); got != want {
   578  				t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   579  			}
   580  		})
   581  	}
   582  }
   583  
   584  func TestRootLstat(t *testing.T) {
   585  	for _, test := range rootTestCases {
   586  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   587  			const content = "content"
   588  			wantError := test.wantError
   589  			if test.ltarget != "" {
   590  				// Lstat will stat the final link, rather than following it.
   591  				wantError = false
   592  			} else if target != "" {
   593  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   594  					t.Fatal(err)
   595  				}
   596  			}
   597  
   598  			fi, err := root.Lstat(test.open)
   599  			if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) {
   600  				return
   601  			}
   602  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   603  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   604  			}
   605  			if test.ltarget == "" {
   606  				if got := fi.Mode(); got&os.ModeSymlink != 0 {
   607  					t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got)
   608  				}
   609  				if got, want := fi.Size(), int64(len(content)); got != want {
   610  					t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   611  				}
   612  			} else {
   613  				if got := fi.Mode(); got&os.ModeSymlink == 0 {
   614  					t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got)
   615  				}
   616  			}
   617  		})
   618  	}
   619  }
   620  
   621  // A rootConsistencyTest is a test case comparing os.Root behavior with
   622  // the corresponding non-Root function.
   623  //
   624  // These tests verify that, for example, Root.Open("file/./") and os.Open("file/./")
   625  // have the same result, although the specific result may vary by platform.
   626  type rootConsistencyTest struct {
   627  	name string
   628  
   629  	// fs is the test filesystem layout. See makefs above.
   630  	// fsFunc is called to modify the test filesystem, or replace it.
   631  	fs     []string
   632  	fsFunc func(t *testing.T, dir string) string
   633  
   634  	// open is the filename to access in the test.
   635  	open string
   636  
   637  	// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
   638  	// function return different errors for this test.
   639  	detailedErrorMismatch func(t *testing.T) bool
   640  }
   641  
   642  var rootConsistencyTestCases = []rootConsistencyTest{{
   643  	name: "file",
   644  	fs: []string{
   645  		"target",
   646  	},
   647  	open: "target",
   648  }, {
   649  	name: "dir slash dot",
   650  	fs: []string{
   651  		"target/file",
   652  	},
   653  	open: "target/.",
   654  }, {
   655  	name: "dot",
   656  	fs: []string{
   657  		"file",
   658  	},
   659  	open: ".",
   660  }, {
   661  	name: "file slash dot",
   662  	fs: []string{
   663  		"target",
   664  	},
   665  	open: "target/.",
   666  	detailedErrorMismatch: func(t *testing.T) bool {
   667  		// FreeBSD returns EPERM in the non-Root case.
   668  		return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
   669  	},
   670  }, {
   671  	name: "dir slash",
   672  	fs: []string{
   673  		"target/file",
   674  	},
   675  	open: "target/",
   676  }, {
   677  	name: "dot slash",
   678  	fs: []string{
   679  		"file",
   680  	},
   681  	open: "./",
   682  }, {
   683  	name: "file slash",
   684  	fs: []string{
   685  		"target",
   686  	},
   687  	open: "target/",
   688  	detailedErrorMismatch: func(t *testing.T) bool {
   689  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
   690  		return runtime.GOOS == "js"
   691  	},
   692  }, {
   693  	name: "file in path",
   694  	fs: []string{
   695  		"file",
   696  	},
   697  	open: "file/target",
   698  }, {
   699  	name: "directory in path missing",
   700  	open: "dir/target",
   701  }, {
   702  	name: "target does not exist",
   703  	open: "target",
   704  }, {
   705  	name: "symlink slash",
   706  	fs: []string{
   707  		"target/file",
   708  		"link => target",
   709  	},
   710  	open: "link/",
   711  }, {
   712  	name: "symlink slash dot",
   713  	fs: []string{
   714  		"target/file",
   715  		"link => target",
   716  	},
   717  	open: "link/.",
   718  }, {
   719  	name: "file symlink slash",
   720  	fs: []string{
   721  		"target",
   722  		"link => target",
   723  	},
   724  	open: "link/",
   725  	detailedErrorMismatch: func(t *testing.T) bool {
   726  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
   727  		return runtime.GOOS == "js"
   728  	},
   729  }, {
   730  	name: "unresolved symlink",
   731  	fs: []string{
   732  		"link => target",
   733  	},
   734  	open: "link",
   735  }, {
   736  	name: "resolved symlink",
   737  	fs: []string{
   738  		"link => target",
   739  		"target",
   740  	},
   741  	open: "link",
   742  }, {
   743  	name: "dotdot in path after symlink",
   744  	fs: []string{
   745  		"a => b/c",
   746  		"b/c/",
   747  		"b/target",
   748  	},
   749  	open: "a/../target",
   750  }, {
   751  	name: "long file name",
   752  	open: strings.Repeat("a", 500),
   753  }, {
   754  	name: "unreadable directory",
   755  	fs: []string{
   756  		"dir/target",
   757  	},
   758  	fsFunc: func(t *testing.T, dir string) string {
   759  		os.Chmod(filepath.Join(dir, "dir"), 0)
   760  		t.Cleanup(func() {
   761  			os.Chmod(filepath.Join(dir, "dir"), 0o700)
   762  		})
   763  		return dir
   764  	},
   765  	open: "dir/target",
   766  }, {
   767  	name: "unix domain socket target",
   768  	fsFunc: func(t *testing.T, dir string) string {
   769  		return tempDirWithUnixSocket(t, "a")
   770  	},
   771  	open: "a",
   772  }, {
   773  	name: "unix domain socket in path",
   774  	fsFunc: func(t *testing.T, dir string) string {
   775  		return tempDirWithUnixSocket(t, "a")
   776  	},
   777  	open: "a/b",
   778  	detailedErrorMismatch: func(t *testing.T) bool {
   779  		// On Windows, os.Root.Open returns "The directory name is invalid."
   780  		// and os.Open returns "The file cannot be accessed by the system.".
   781  		return runtime.GOOS == "windows"
   782  	},
   783  }, {
   784  	name: "question mark",
   785  	open: "?",
   786  }, {
   787  	name: "nul byte",
   788  	open: "\x00",
   789  }}
   790  
   791  func tempDirWithUnixSocket(t *testing.T, name string) string {
   792  	dir, err := os.MkdirTemp("", "")
   793  	if err != nil {
   794  		t.Fatal(err)
   795  	}
   796  	t.Cleanup(func() {
   797  		if err := os.RemoveAll(dir); err != nil {
   798  			t.Error(err)
   799  		}
   800  	})
   801  	addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name))
   802  	if err != nil {
   803  		t.Skipf("net.ResolveUnixAddr: %v", err)
   804  	}
   805  	conn, err := net.ListenUnix("unix", addr)
   806  	if err != nil {
   807  		t.Skipf("net.ListenUnix: %v", err)
   808  	}
   809  	t.Cleanup(func() {
   810  		conn.Close()
   811  	})
   812  	return dir
   813  }
   814  
   815  func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) {
   816  	if runtime.GOOS == "wasip1" {
   817  		// On wasip, non-Root functions clean paths before opening them,
   818  		// resulting in inconsistent behavior.
   819  		// https://go.dev/issue/69509
   820  		t.Skip("#69509: inconsistent results on wasip1")
   821  	}
   822  
   823  	t.Run(test.name, func(t *testing.T) {
   824  		dir1 := makefs(t, test.fs)
   825  		dir2 := makefs(t, test.fs)
   826  		if test.fsFunc != nil {
   827  			dir1 = test.fsFunc(t, dir1)
   828  			dir2 = test.fsFunc(t, dir2)
   829  		}
   830  
   831  		r, err := os.OpenRoot(dir1)
   832  		if err != nil {
   833  			t.Fatal(err)
   834  		}
   835  		defer r.Close()
   836  
   837  		res1, err1 := f(t, test.open, r)
   838  		res2, err2 := f(t, dir2+"/"+test.open, nil)
   839  
   840  		if res1 != res2 || ((err1 == nil) != (err2 == nil)) {
   841  			t.Errorf("with root:    res=%v", res1)
   842  			t.Errorf("              err=%v", err1)
   843  			t.Errorf("without root: res=%v", res2)
   844  			t.Errorf("              err=%v", err2)
   845  			t.Errorf("want consistent results, got mismatch")
   846  		}
   847  
   848  		if err1 != nil || err2 != nil {
   849  			e1, ok := err1.(*os.PathError)
   850  			if !ok {
   851  				t.Fatalf("with root, expected PathError; got: %v", err1)
   852  			}
   853  			e2, ok := err2.(*os.PathError)
   854  			if !ok {
   855  				t.Fatalf("without root, expected PathError; got: %v", err1)
   856  			}
   857  			detailedErrorMismatch := false
   858  			if f := test.detailedErrorMismatch; f != nil {
   859  				detailedErrorMismatch = f(t)
   860  			}
   861  			if runtime.GOOS == "plan9" {
   862  				// Plan9 syscall errors aren't comparable.
   863  				detailedErrorMismatch = true
   864  			}
   865  			if !detailedErrorMismatch && e1.Err != e2.Err {
   866  				t.Errorf("with root:    err=%v", e1.Err)
   867  				t.Errorf("without root: err=%v", e2.Err)
   868  				t.Errorf("want consistent results, got mismatch")
   869  			}
   870  		}
   871  	})
   872  }
   873  
   874  func TestRootConsistencyOpen(t *testing.T) {
   875  	for _, test := range rootConsistencyTestCases {
   876  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   877  			var f *os.File
   878  			var err error
   879  			if r == nil {
   880  				f, err = os.Open(path)
   881  			} else {
   882  				f, err = r.Open(path)
   883  			}
   884  			if err != nil {
   885  				return "", err
   886  			}
   887  			defer f.Close()
   888  			fi, err := f.Stat()
   889  			if err == nil && !fi.IsDir() {
   890  				b, err := io.ReadAll(f)
   891  				return string(b), err
   892  			} else {
   893  				names, err := f.Readdirnames(-1)
   894  				slices.Sort(names)
   895  				return fmt.Sprintf("%q", names), err
   896  			}
   897  		})
   898  	}
   899  }
   900  
   901  func TestRootConsistencyCreate(t *testing.T) {
   902  	for _, test := range rootConsistencyTestCases {
   903  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   904  			var f *os.File
   905  			var err error
   906  			if r == nil {
   907  				f, err = os.Create(path)
   908  			} else {
   909  				f, err = r.Create(path)
   910  			}
   911  			if err == nil {
   912  				f.Write([]byte("file contents"))
   913  				f.Close()
   914  			}
   915  			return "", err
   916  		})
   917  	}
   918  }
   919  
   920  func TestRootConsistencyMkdir(t *testing.T) {
   921  	for _, test := range rootConsistencyTestCases {
   922  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   923  			var err error
   924  			if r == nil {
   925  				err = os.Mkdir(path, 0o777)
   926  			} else {
   927  				err = r.Mkdir(path, 0o777)
   928  			}
   929  			return "", err
   930  		})
   931  	}
   932  }
   933  
   934  func TestRootConsistencyRemove(t *testing.T) {
   935  	for _, test := range rootConsistencyTestCases {
   936  		if test.open == "." || test.open == "./" {
   937  			continue // can't remove the root itself
   938  		}
   939  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   940  			var err error
   941  			if r == nil {
   942  				err = os.Remove(path)
   943  			} else {
   944  				err = r.Remove(path)
   945  			}
   946  			return "", err
   947  		})
   948  	}
   949  }
   950  
   951  func TestRootConsistencyStat(t *testing.T) {
   952  	for _, test := range rootConsistencyTestCases {
   953  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   954  			var fi os.FileInfo
   955  			var err error
   956  			if r == nil {
   957  				fi, err = os.Stat(path)
   958  			} else {
   959  				fi, err = r.Stat(path)
   960  			}
   961  			if err != nil {
   962  				return "", err
   963  			}
   964  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
   965  		})
   966  	}
   967  }
   968  
   969  func TestRootConsistencyLstat(t *testing.T) {
   970  	for _, test := range rootConsistencyTestCases {
   971  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   972  			var fi os.FileInfo
   973  			var err error
   974  			if r == nil {
   975  				fi, err = os.Lstat(path)
   976  			} else {
   977  				fi, err = r.Lstat(path)
   978  			}
   979  			if err != nil {
   980  				return "", err
   981  			}
   982  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
   983  		})
   984  	}
   985  }
   986  
   987  func TestRootRenameAfterOpen(t *testing.T) {
   988  	switch runtime.GOOS {
   989  	case "windows":
   990  		t.Skip("renaming open files not supported on " + runtime.GOOS)
   991  	case "js", "plan9":
   992  		t.Skip("openat not supported on " + runtime.GOOS)
   993  	case "wasip1":
   994  		if os.Getenv("GOWASIRUNTIME") == "wazero" {
   995  			t.Skip("wazero does not track renamed directories")
   996  		}
   997  	}
   998  
   999  	dir := t.TempDir()
  1000  
  1001  	// Create directory "a" and open it.
  1002  	if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil {
  1003  		t.Fatal(err)
  1004  	}
  1005  	dirf, err := os.OpenRoot(filepath.Join(dir, "a"))
  1006  	if err != nil {
  1007  		t.Fatal(err)
  1008  	}
  1009  	defer dirf.Close()
  1010  
  1011  	// Rename "a" => "b", and create "b/f".
  1012  	if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil {
  1013  		t.Fatal(err)
  1014  	}
  1015  	if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil {
  1016  		t.Fatal(err)
  1017  	}
  1018  
  1019  	// Open "f", and confirm that we see it.
  1020  	f, err := dirf.OpenFile("f", os.O_RDONLY, 0)
  1021  	if err != nil {
  1022  		t.Fatalf("reading file after renaming parent: %v", err)
  1023  	}
  1024  	defer f.Close()
  1025  	b, err := io.ReadAll(f)
  1026  	if err != nil {
  1027  		t.Fatal(err)
  1028  	}
  1029  	if got, want := string(b), "hello"; got != want {
  1030  		t.Fatalf("file contents: %q, want %q", got, want)
  1031  	}
  1032  
  1033  	// f.Name reflects the original path we opened the directory under (".../a"), not "b".
  1034  	if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want {
  1035  		t.Errorf("f.Name() = %q, want %q", got, want)
  1036  	}
  1037  }
  1038  
  1039  func TestRootNonPermissionMode(t *testing.T) {
  1040  	r, err := os.OpenRoot(t.TempDir())
  1041  	if err != nil {
  1042  		t.Fatal(err)
  1043  	}
  1044  	defer r.Close()
  1045  	if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil {
  1046  		t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error")
  1047  	}
  1048  	if err := r.Mkdir("file", 0o1777); err == nil {
  1049  		t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error")
  1050  	}
  1051  }
  1052  
  1053  func TestRootUseAfterClose(t *testing.T) {
  1054  	r, err := os.OpenRoot(t.TempDir())
  1055  	if err != nil {
  1056  		t.Fatal(err)
  1057  	}
  1058  	r.Close()
  1059  	for _, test := range []struct {
  1060  		name string
  1061  		f    func(r *os.Root, filename string) error
  1062  	}{{
  1063  		name: "Open",
  1064  		f: func(r *os.Root, filename string) error {
  1065  			_, err := r.Open(filename)
  1066  			return err
  1067  		},
  1068  	}, {
  1069  		name: "Create",
  1070  		f: func(r *os.Root, filename string) error {
  1071  			_, err := r.Create(filename)
  1072  			return err
  1073  		},
  1074  	}, {
  1075  		name: "OpenFile",
  1076  		f: func(r *os.Root, filename string) error {
  1077  			_, err := r.OpenFile(filename, os.O_RDWR, 0o666)
  1078  			return err
  1079  		},
  1080  	}, {
  1081  		name: "OpenRoot",
  1082  		f: func(r *os.Root, filename string) error {
  1083  			_, err := r.OpenRoot(filename)
  1084  			return err
  1085  		},
  1086  	}, {
  1087  		name: "Mkdir",
  1088  		f: func(r *os.Root, filename string) error {
  1089  			return r.Mkdir(filename, 0o777)
  1090  		},
  1091  	}} {
  1092  		err := test.f(r, "target")
  1093  		pe, ok := err.(*os.PathError)
  1094  		if !ok || pe.Path != "target" || pe.Err != os.ErrClosed {
  1095  			t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err)
  1096  		}
  1097  	}
  1098  }
  1099  
  1100  func TestRootConcurrentClose(t *testing.T) {
  1101  	r, err := os.OpenRoot(t.TempDir())
  1102  	if err != nil {
  1103  		t.Fatal(err)
  1104  	}
  1105  	ch := make(chan error, 1)
  1106  	go func() {
  1107  		defer close(ch)
  1108  		first := true
  1109  		for {
  1110  			f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666)
  1111  			if err != nil {
  1112  				ch <- err
  1113  				return
  1114  			}
  1115  			if first {
  1116  				ch <- nil
  1117  				first = false
  1118  			}
  1119  			f.Close()
  1120  			if runtime.GOARCH == "wasm" {
  1121  				// TODO(go.dev/issue/71134) can lead to goroutine starvation.
  1122  				runtime.Gosched()
  1123  			}
  1124  		}
  1125  	}()
  1126  	if err := <-ch; err != nil {
  1127  		t.Errorf("OpenFile: %v, want success", err)
  1128  	}
  1129  	r.Close()
  1130  	if err := <-ch; !errors.Is(err, os.ErrClosed) {
  1131  		t.Errorf("OpenFile: %v, want ErrClosed", err)
  1132  	}
  1133  }
  1134  
  1135  // TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse.
  1136  //
  1137  // We create a deeply nested directory:
  1138  //
  1139  //	base/a/a/a/a/ [...] /a
  1140  //
  1141  // And a path that descends into the tree, then returns to the top using ..:
  1142  //
  1143  //	base/a/a/a/a/ [...] /a/../../../ [..] /../a/f
  1144  //
  1145  // While opening this file, we rename base/a/a to base/b.
  1146  // A naive lookup operation will resolve the path to base/f.
  1147  func TestRootRaceRenameDir(t *testing.T) {
  1148  	dir := t.TempDir()
  1149  	r, err := os.OpenRoot(dir)
  1150  	if err != nil {
  1151  		t.Fatal(err)
  1152  	}
  1153  	defer r.Close()
  1154  
  1155  	const depth = 4
  1156  
  1157  	os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777)
  1158  
  1159  	path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f"
  1160  	os.WriteFile(dir+"/f", []byte("secret"), 0o666)
  1161  	os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666)
  1162  
  1163  	// Compute how long it takes to open the path in the common case.
  1164  	const tries = 10
  1165  	var total time.Duration
  1166  	for range tries {
  1167  		start := time.Now()
  1168  		f, err := r.Open(path)
  1169  		if err != nil {
  1170  			t.Fatal(err)
  1171  		}
  1172  		b, err := io.ReadAll(f)
  1173  		if err != nil {
  1174  			t.Fatal(err)
  1175  		}
  1176  		if string(b) != "public" {
  1177  			t.Fatalf("read %q, want %q", b, "public")
  1178  		}
  1179  		f.Close()
  1180  		total += time.Since(start)
  1181  	}
  1182  	avg := total / tries
  1183  
  1184  	// We're trying to exploit a race, so try this a number of times.
  1185  	for range 100 {
  1186  		// Start a goroutine to open the file.
  1187  		gotc := make(chan []byte)
  1188  		go func() {
  1189  			f, err := r.Open(path)
  1190  			if err != nil {
  1191  				gotc <- nil
  1192  			}
  1193  			defer f.Close()
  1194  			b, _ := io.ReadAll(f)
  1195  			gotc <- b
  1196  		}()
  1197  
  1198  		// Wait for the open operation to partially complete,
  1199  		// and then rename a directory near the root.
  1200  		time.Sleep(avg / 4)
  1201  		if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil {
  1202  			// Windows and Plan9 won't let us rename a directory if we have
  1203  			// an open handle for it, so an error here is expected.
  1204  			switch runtime.GOOS {
  1205  			case "windows", "plan9":
  1206  			default:
  1207  				t.Fatal(err)
  1208  			}
  1209  		}
  1210  
  1211  		got := <-gotc
  1212  		os.Rename(dir+"/b", dir+"/base/a")
  1213  		if len(got) > 0 && string(got) != "public" {
  1214  			t.Errorf("read file: %q; want error or 'public'", got)
  1215  		}
  1216  	}
  1217  }
  1218  
  1219  func TestRootSymlinkToRoot(t *testing.T) {
  1220  	dir := makefs(t, []string{
  1221  		"d/d => ..",
  1222  	})
  1223  	root, err := os.OpenRoot(dir)
  1224  	if err != nil {
  1225  		t.Fatal(err)
  1226  	}
  1227  	defer root.Close()
  1228  	if err := root.Mkdir("d/d/new", 0777); err != nil {
  1229  		t.Fatal(err)
  1230  	}
  1231  	f, err := root.Open("d/d")
  1232  	if err != nil {
  1233  		t.Fatal(err)
  1234  	}
  1235  	defer f.Close()
  1236  	names, err := f.Readdirnames(-1)
  1237  	if err != nil {
  1238  		t.Fatal(err)
  1239  	}
  1240  	slices.Sort(names)
  1241  	if got, want := names, []string{"d", "new"}; !slices.Equal(got, want) {
  1242  		t.Errorf("root contains: %q, want %q", got, want)
  1243  	}
  1244  }
  1245  
  1246  func TestOpenInRoot(t *testing.T) {
  1247  	dir := makefs(t, []string{
  1248  		"file",
  1249  		"link => ../ROOT/file",
  1250  	})
  1251  	f, err := os.OpenInRoot(dir, "file")
  1252  	if err != nil {
  1253  		t.Fatalf("OpenInRoot(`file`) = %v, want success", err)
  1254  	}
  1255  	f.Close()
  1256  	for _, name := range []string{
  1257  		"link",
  1258  		"../ROOT/file",
  1259  		dir + "/file",
  1260  	} {
  1261  		f, err := os.OpenInRoot(dir, name)
  1262  		if err == nil {
  1263  			f.Close()
  1264  			t.Fatalf("OpenInRoot(%q) = nil, want error", name)
  1265  		}
  1266  	}
  1267  }
  1268  

View as plain text