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

View as plain text