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

View as plain text