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  		})
   451  	}
   452  }
   453  
   454  func TestRootOpenRoot(t *testing.T) {
   455  	for _, test := range rootTestCases {
   456  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   457  			if target != "" {
   458  				if err := os.Mkdir(target, 0o777); err != nil {
   459  					t.Fatal(err)
   460  				}
   461  				if err := os.WriteFile(target+"/f", nil, 0o666); err != nil {
   462  					t.Fatal(err)
   463  				}
   464  			}
   465  			rr, err := root.OpenRoot(test.open)
   466  			if errEndsTest(t, err, test.wantError, "root.OpenRoot(%q)", test.open) {
   467  				return
   468  			}
   469  			defer rr.Close()
   470  			f, err := rr.Open("f")
   471  			if err != nil {
   472  				t.Fatalf(`root.OpenRoot(%q).Open("f") = %v`, test.open, err)
   473  			}
   474  			f.Close()
   475  		})
   476  	}
   477  }
   478  
   479  func TestRootRemoveFile(t *testing.T) {
   480  	for _, test := range rootTestCases {
   481  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   482  			wantError := test.wantError
   483  			if test.ltarget != "" {
   484  				// Remove doesn't follow symlinks in the final path component,
   485  				// so it will successfully remove ltarget.
   486  				wantError = false
   487  				target = filepath.Join(root.Name(), test.ltarget)
   488  			} else if target != "" {
   489  				if err := os.WriteFile(target, nil, 0o666); err != nil {
   490  					t.Fatal(err)
   491  				}
   492  			}
   493  
   494  			err := root.Remove(test.open)
   495  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   496  				return
   497  			}
   498  			_, err = os.Lstat(target)
   499  			if !errors.Is(err, os.ErrNotExist) {
   500  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   501  			}
   502  		})
   503  	}
   504  }
   505  
   506  func TestRootRemoveDirectory(t *testing.T) {
   507  	for _, test := range rootTestCases {
   508  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   509  			wantError := test.wantError
   510  			if test.ltarget != "" {
   511  				// Remove doesn't follow symlinks in the final path component,
   512  				// so it will successfully remove ltarget.
   513  				wantError = false
   514  				target = filepath.Join(root.Name(), test.ltarget)
   515  			} else if target != "" {
   516  				if err := os.Mkdir(target, 0o777); err != nil {
   517  					t.Fatal(err)
   518  				}
   519  			}
   520  
   521  			err := root.Remove(test.open)
   522  			if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
   523  				return
   524  			}
   525  			_, err = os.Lstat(target)
   526  			if !errors.Is(err, os.ErrNotExist) {
   527  				t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
   528  			}
   529  		})
   530  	}
   531  }
   532  
   533  func TestRootOpenFileAsRoot(t *testing.T) {
   534  	dir := t.TempDir()
   535  	target := filepath.Join(dir, "target")
   536  	if err := os.WriteFile(target, nil, 0o666); err != nil {
   537  		t.Fatal(err)
   538  	}
   539  	_, err := os.OpenRoot(target)
   540  	if err == nil {
   541  		t.Fatal("os.OpenRoot(file) succeeded; want failure")
   542  	}
   543  	r, err := os.OpenRoot(dir)
   544  	if err != nil {
   545  		t.Fatal(err)
   546  	}
   547  	defer r.Close()
   548  	_, err = r.OpenRoot("target")
   549  	if err == nil {
   550  		t.Fatal("Root.OpenRoot(file) succeeded; want failure")
   551  	}
   552  }
   553  
   554  func TestRootStat(t *testing.T) {
   555  	for _, test := range rootTestCases {
   556  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   557  			const content = "content"
   558  			if target != "" {
   559  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   560  					t.Fatal(err)
   561  				}
   562  			}
   563  
   564  			fi, err := root.Stat(test.open)
   565  			if errEndsTest(t, err, test.wantError, "root.Stat(%q)", test.open) {
   566  				return
   567  			}
   568  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   569  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   570  			}
   571  			if got, want := fi.Size(), int64(len(content)); got != want {
   572  				t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   573  			}
   574  		})
   575  	}
   576  }
   577  
   578  func TestRootLstat(t *testing.T) {
   579  	for _, test := range rootTestCases {
   580  		test.run(t, func(t *testing.T, target string, root *os.Root) {
   581  			const content = "content"
   582  			wantError := test.wantError
   583  			if test.ltarget != "" {
   584  				// Lstat will stat the final link, rather than following it.
   585  				wantError = false
   586  			} else if target != "" {
   587  				if err := os.WriteFile(target, []byte(content), 0o666); err != nil {
   588  					t.Fatal(err)
   589  				}
   590  			}
   591  
   592  			fi, err := root.Lstat(test.open)
   593  			if errEndsTest(t, err, wantError, "root.Stat(%q)", test.open) {
   594  				return
   595  			}
   596  			if got, want := fi.Name(), filepath.Base(test.open); got != want {
   597  				t.Errorf("root.Stat(%q).Name() = %q, want %q", test.open, got, want)
   598  			}
   599  			if test.ltarget == "" {
   600  				if got := fi.Mode(); got&os.ModeSymlink != 0 {
   601  					t.Errorf("root.Stat(%q).Mode() = %v, want non-symlink", test.open, got)
   602  				}
   603  				if got, want := fi.Size(), int64(len(content)); got != want {
   604  					t.Errorf("root.Stat(%q).Size() = %v, want %v", test.open, got, want)
   605  				}
   606  			} else {
   607  				if got := fi.Mode(); got&os.ModeSymlink == 0 {
   608  					t.Errorf("root.Stat(%q).Mode() = %v, want symlink", test.open, got)
   609  				}
   610  			}
   611  		})
   612  	}
   613  }
   614  
   615  // A rootConsistencyTest is a test case comparing os.Root behavior with
   616  // the corresponding non-Root function.
   617  //
   618  // These tests verify that, for example, Root.Open("file/./") and os.Open("file/./")
   619  // have the same result, although the specific result may vary by platform.
   620  type rootConsistencyTest struct {
   621  	name string
   622  
   623  	// fs is the test filesystem layout. See makefs above.
   624  	// fsFunc is called to modify the test filesystem, or replace it.
   625  	fs     []string
   626  	fsFunc func(t *testing.T, dir string) string
   627  
   628  	// open is the filename to access in the test.
   629  	open string
   630  
   631  	// detailedErrorMismatch indicates that os.Root and the corresponding non-Root
   632  	// function return different errors for this test.
   633  	detailedErrorMismatch func(t *testing.T) bool
   634  }
   635  
   636  var rootConsistencyTestCases = []rootConsistencyTest{{
   637  	name: "file",
   638  	fs: []string{
   639  		"target",
   640  	},
   641  	open: "target",
   642  }, {
   643  	name: "dir slash dot",
   644  	fs: []string{
   645  		"target/file",
   646  	},
   647  	open: "target/.",
   648  }, {
   649  	name: "dot",
   650  	fs: []string{
   651  		"file",
   652  	},
   653  	open: ".",
   654  }, {
   655  	name: "file slash dot",
   656  	fs: []string{
   657  		"target",
   658  	},
   659  	open: "target/.",
   660  	detailedErrorMismatch: func(t *testing.T) bool {
   661  		// FreeBSD returns EPERM in the non-Root case.
   662  		return runtime.GOOS == "freebsd" && strings.HasPrefix(t.Name(), "TestRootConsistencyRemove")
   663  	},
   664  }, {
   665  	name: "dir slash",
   666  	fs: []string{
   667  		"target/file",
   668  	},
   669  	open: "target/",
   670  }, {
   671  	name: "dot slash",
   672  	fs: []string{
   673  		"file",
   674  	},
   675  	open: "./",
   676  }, {
   677  	name: "file slash",
   678  	fs: []string{
   679  		"target",
   680  	},
   681  	open: "target/",
   682  	detailedErrorMismatch: func(t *testing.T) bool {
   683  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
   684  		return runtime.GOOS == "js"
   685  	},
   686  }, {
   687  	name: "file in path",
   688  	fs: []string{
   689  		"file",
   690  	},
   691  	open: "file/target",
   692  }, {
   693  	name: "directory in path missing",
   694  	open: "dir/target",
   695  }, {
   696  	name: "target does not exist",
   697  	open: "target",
   698  }, {
   699  	name: "symlink slash",
   700  	fs: []string{
   701  		"target/file",
   702  		"link => target",
   703  	},
   704  	open: "link/",
   705  }, {
   706  	name: "symlink slash dot",
   707  	fs: []string{
   708  		"target/file",
   709  		"link => target",
   710  	},
   711  	open: "link/.",
   712  }, {
   713  	name: "file symlink slash",
   714  	fs: []string{
   715  		"target",
   716  		"link => target",
   717  	},
   718  	open: "link/",
   719  	detailedErrorMismatch: func(t *testing.T) bool {
   720  		// os.Create returns ENOTDIR or EISDIR depending on the platform.
   721  		return runtime.GOOS == "js"
   722  	},
   723  }, {
   724  	name: "unresolved symlink",
   725  	fs: []string{
   726  		"link => target",
   727  	},
   728  	open: "link",
   729  }, {
   730  	name: "resolved symlink",
   731  	fs: []string{
   732  		"link => target",
   733  		"target",
   734  	},
   735  	open: "link",
   736  }, {
   737  	name: "dotdot in path after symlink",
   738  	fs: []string{
   739  		"a => b/c",
   740  		"b/c/",
   741  		"b/target",
   742  	},
   743  	open: "a/../target",
   744  }, {
   745  	name: "long file name",
   746  	open: strings.Repeat("a", 500),
   747  }, {
   748  	name: "unreadable directory",
   749  	fs: []string{
   750  		"dir/target",
   751  	},
   752  	fsFunc: func(t *testing.T, dir string) string {
   753  		os.Chmod(filepath.Join(dir, "dir"), 0)
   754  		t.Cleanup(func() {
   755  			os.Chmod(filepath.Join(dir, "dir"), 0o700)
   756  		})
   757  		return dir
   758  	},
   759  	open: "dir/target",
   760  }, {
   761  	name: "unix domain socket target",
   762  	fsFunc: func(t *testing.T, dir string) string {
   763  		return tempDirWithUnixSocket(t, "a")
   764  	},
   765  	open: "a",
   766  }, {
   767  	name: "unix domain socket in path",
   768  	fsFunc: func(t *testing.T, dir string) string {
   769  		return tempDirWithUnixSocket(t, "a")
   770  	},
   771  	open: "a/b",
   772  	detailedErrorMismatch: func(t *testing.T) bool {
   773  		// On Windows, os.Root.Open returns "The directory name is invalid."
   774  		// and os.Open returns "The file cannot be accessed by the system.".
   775  		return runtime.GOOS == "windows"
   776  	},
   777  }, {
   778  	name: "question mark",
   779  	open: "?",
   780  }, {
   781  	name: "nul byte",
   782  	open: "\x00",
   783  }}
   784  
   785  func tempDirWithUnixSocket(t *testing.T, name string) string {
   786  	dir, err := os.MkdirTemp("", "")
   787  	if err != nil {
   788  		t.Fatal(err)
   789  	}
   790  	t.Cleanup(func() {
   791  		if err := os.RemoveAll(dir); err != nil {
   792  			t.Error(err)
   793  		}
   794  	})
   795  	addr, err := net.ResolveUnixAddr("unix", filepath.Join(dir, name))
   796  	if err != nil {
   797  		t.Skipf("net.ResolveUnixAddr: %v", err)
   798  	}
   799  	conn, err := net.ListenUnix("unix", addr)
   800  	if err != nil {
   801  		t.Skipf("net.ListenUnix: %v", err)
   802  	}
   803  	t.Cleanup(func() {
   804  		conn.Close()
   805  	})
   806  	return dir
   807  }
   808  
   809  func (test rootConsistencyTest) run(t *testing.T, f func(t *testing.T, path string, r *os.Root) (string, error)) {
   810  	if runtime.GOOS == "wasip1" {
   811  		// On wasip, non-Root functions clean paths before opening them,
   812  		// resulting in inconsistent behavior.
   813  		// https://go.dev/issue/69509
   814  		t.Skip("#69509: inconsistent results on wasip1")
   815  	}
   816  
   817  	t.Run(test.name, func(t *testing.T) {
   818  		dir1 := makefs(t, test.fs)
   819  		dir2 := makefs(t, test.fs)
   820  		if test.fsFunc != nil {
   821  			dir1 = test.fsFunc(t, dir1)
   822  			dir2 = test.fsFunc(t, dir2)
   823  		}
   824  
   825  		r, err := os.OpenRoot(dir1)
   826  		if err != nil {
   827  			t.Fatal(err)
   828  		}
   829  		defer r.Close()
   830  
   831  		res1, err1 := f(t, test.open, r)
   832  		res2, err2 := f(t, dir2+"/"+test.open, nil)
   833  
   834  		if res1 != res2 || ((err1 == nil) != (err2 == nil)) {
   835  			t.Errorf("with root:    res=%v", res1)
   836  			t.Errorf("              err=%v", err1)
   837  			t.Errorf("without root: res=%v", res2)
   838  			t.Errorf("              err=%v", err2)
   839  			t.Errorf("want consistent results, got mismatch")
   840  		}
   841  
   842  		if err1 != nil || err2 != nil {
   843  			e1, ok := err1.(*os.PathError)
   844  			if !ok {
   845  				t.Fatalf("with root, expected PathError; got: %v", err1)
   846  			}
   847  			e2, ok := err2.(*os.PathError)
   848  			if !ok {
   849  				t.Fatalf("without root, expected PathError; got: %v", err1)
   850  			}
   851  			detailedErrorMismatch := false
   852  			if f := test.detailedErrorMismatch; f != nil {
   853  				detailedErrorMismatch = f(t)
   854  			}
   855  			if runtime.GOOS == "plan9" {
   856  				// Plan9 syscall errors aren't comparable.
   857  				detailedErrorMismatch = true
   858  			}
   859  			if !detailedErrorMismatch && e1.Err != e2.Err {
   860  				t.Errorf("with root:    err=%v", e1.Err)
   861  				t.Errorf("without root: err=%v", e2.Err)
   862  				t.Errorf("want consistent results, got mismatch")
   863  			}
   864  		}
   865  	})
   866  }
   867  
   868  func TestRootConsistencyOpen(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 f *os.File
   872  			var err error
   873  			if r == nil {
   874  				f, err = os.Open(path)
   875  			} else {
   876  				f, err = r.Open(path)
   877  			}
   878  			if err != nil {
   879  				return "", err
   880  			}
   881  			defer f.Close()
   882  			fi, err := f.Stat()
   883  			if err == nil && !fi.IsDir() {
   884  				b, err := io.ReadAll(f)
   885  				return string(b), err
   886  			} else {
   887  				names, err := f.Readdirnames(-1)
   888  				slices.Sort(names)
   889  				return fmt.Sprintf("%q", names), err
   890  			}
   891  		})
   892  	}
   893  }
   894  
   895  func TestRootConsistencyCreate(t *testing.T) {
   896  	for _, test := range rootConsistencyTestCases {
   897  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   898  			var f *os.File
   899  			var err error
   900  			if r == nil {
   901  				f, err = os.Create(path)
   902  			} else {
   903  				f, err = r.Create(path)
   904  			}
   905  			if err == nil {
   906  				f.Write([]byte("file contents"))
   907  				f.Close()
   908  			}
   909  			return "", err
   910  		})
   911  	}
   912  }
   913  
   914  func TestRootConsistencyMkdir(t *testing.T) {
   915  	for _, test := range rootConsistencyTestCases {
   916  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   917  			var err error
   918  			if r == nil {
   919  				err = os.Mkdir(path, 0o777)
   920  			} else {
   921  				err = r.Mkdir(path, 0o777)
   922  			}
   923  			return "", err
   924  		})
   925  	}
   926  }
   927  
   928  func TestRootConsistencyRemove(t *testing.T) {
   929  	for _, test := range rootConsistencyTestCases {
   930  		if test.open == "." || test.open == "./" {
   931  			continue // can't remove the root itself
   932  		}
   933  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   934  			var err error
   935  			if r == nil {
   936  				err = os.Remove(path)
   937  			} else {
   938  				err = r.Remove(path)
   939  			}
   940  			return "", err
   941  		})
   942  	}
   943  }
   944  
   945  func TestRootConsistencyStat(t *testing.T) {
   946  	for _, test := range rootConsistencyTestCases {
   947  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   948  			var fi os.FileInfo
   949  			var err error
   950  			if r == nil {
   951  				fi, err = os.Stat(path)
   952  			} else {
   953  				fi, err = r.Stat(path)
   954  			}
   955  			if err != nil {
   956  				return "", err
   957  			}
   958  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
   959  		})
   960  	}
   961  }
   962  
   963  func TestRootConsistencyLstat(t *testing.T) {
   964  	for _, test := range rootConsistencyTestCases {
   965  		test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
   966  			var fi os.FileInfo
   967  			var err error
   968  			if r == nil {
   969  				fi, err = os.Lstat(path)
   970  			} else {
   971  				fi, err = r.Lstat(path)
   972  			}
   973  			if err != nil {
   974  				return "", err
   975  			}
   976  			return fmt.Sprintf("name:%q size:%v mode:%v isdir:%v", fi.Name(), fi.Size(), fi.Mode(), fi.IsDir()), nil
   977  		})
   978  	}
   979  }
   980  
   981  func TestRootRenameAfterOpen(t *testing.T) {
   982  	switch runtime.GOOS {
   983  	case "windows":
   984  		t.Skip("renaming open files not supported on " + runtime.GOOS)
   985  	case "js", "plan9":
   986  		t.Skip("openat not supported on " + runtime.GOOS)
   987  	case "wasip1":
   988  		if os.Getenv("GOWASIRUNTIME") == "wazero" {
   989  			t.Skip("wazero does not track renamed directories")
   990  		}
   991  	}
   992  
   993  	dir := t.TempDir()
   994  
   995  	// Create directory "a" and open it.
   996  	if err := os.Mkdir(filepath.Join(dir, "a"), 0o777); err != nil {
   997  		t.Fatal(err)
   998  	}
   999  	dirf, err := os.OpenRoot(filepath.Join(dir, "a"))
  1000  	if err != nil {
  1001  		t.Fatal(err)
  1002  	}
  1003  	defer dirf.Close()
  1004  
  1005  	// Rename "a" => "b", and create "b/f".
  1006  	if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "b")); err != nil {
  1007  		t.Fatal(err)
  1008  	}
  1009  	if err := os.WriteFile(filepath.Join(dir, "b/f"), []byte("hello"), 0o666); err != nil {
  1010  		t.Fatal(err)
  1011  	}
  1012  
  1013  	// Open "f", and confirm that we see it.
  1014  	f, err := dirf.OpenFile("f", os.O_RDONLY, 0)
  1015  	if err != nil {
  1016  		t.Fatalf("reading file after renaming parent: %v", err)
  1017  	}
  1018  	defer f.Close()
  1019  	b, err := io.ReadAll(f)
  1020  	if err != nil {
  1021  		t.Fatal(err)
  1022  	}
  1023  	if got, want := string(b), "hello"; got != want {
  1024  		t.Fatalf("file contents: %q, want %q", got, want)
  1025  	}
  1026  
  1027  	// f.Name reflects the original path we opened the directory under (".../a"), not "b".
  1028  	if got, want := f.Name(), dirf.Name()+string(os.PathSeparator)+"f"; got != want {
  1029  		t.Errorf("f.Name() = %q, want %q", got, want)
  1030  	}
  1031  }
  1032  
  1033  func TestRootNonPermissionMode(t *testing.T) {
  1034  	r, err := os.OpenRoot(t.TempDir())
  1035  	if err != nil {
  1036  		t.Fatal(err)
  1037  	}
  1038  	defer r.Close()
  1039  	if _, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o1777); err == nil {
  1040  		t.Errorf("r.OpenFile(file, O_RDWR|O_CREATE, 0o1777) succeeded; want error")
  1041  	}
  1042  	if err := r.Mkdir("file", 0o1777); err == nil {
  1043  		t.Errorf("r.Mkdir(file, 0o1777) succeeded; want error")
  1044  	}
  1045  }
  1046  
  1047  func TestRootUseAfterClose(t *testing.T) {
  1048  	r, err := os.OpenRoot(t.TempDir())
  1049  	if err != nil {
  1050  		t.Fatal(err)
  1051  	}
  1052  	r.Close()
  1053  	for _, test := range []struct {
  1054  		name string
  1055  		f    func(r *os.Root, filename string) error
  1056  	}{{
  1057  		name: "Open",
  1058  		f: func(r *os.Root, filename string) error {
  1059  			_, err := r.Open(filename)
  1060  			return err
  1061  		},
  1062  	}, {
  1063  		name: "Create",
  1064  		f: func(r *os.Root, filename string) error {
  1065  			_, err := r.Create(filename)
  1066  			return err
  1067  		},
  1068  	}, {
  1069  		name: "OpenFile",
  1070  		f: func(r *os.Root, filename string) error {
  1071  			_, err := r.OpenFile(filename, os.O_RDWR, 0o666)
  1072  			return err
  1073  		},
  1074  	}, {
  1075  		name: "OpenRoot",
  1076  		f: func(r *os.Root, filename string) error {
  1077  			_, err := r.OpenRoot(filename)
  1078  			return err
  1079  		},
  1080  	}, {
  1081  		name: "Mkdir",
  1082  		f: func(r *os.Root, filename string) error {
  1083  			return r.Mkdir(filename, 0o777)
  1084  		},
  1085  	}} {
  1086  		err := test.f(r, "target")
  1087  		pe, ok := err.(*os.PathError)
  1088  		if !ok || pe.Path != "target" || pe.Err != os.ErrClosed {
  1089  			t.Errorf(`r.%v = %v; want &PathError{Path: "target", Err: ErrClosed}`, test.name, err)
  1090  		}
  1091  	}
  1092  }
  1093  
  1094  func TestRootConcurrentClose(t *testing.T) {
  1095  	r, err := os.OpenRoot(t.TempDir())
  1096  	if err != nil {
  1097  		t.Fatal(err)
  1098  	}
  1099  	ch := make(chan error, 1)
  1100  	go func() {
  1101  		defer close(ch)
  1102  		first := true
  1103  		for {
  1104  			f, err := r.OpenFile("file", os.O_RDWR|os.O_CREATE, 0o666)
  1105  			if err != nil {
  1106  				ch <- err
  1107  				return
  1108  			}
  1109  			if first {
  1110  				ch <- nil
  1111  				first = false
  1112  			}
  1113  			f.Close()
  1114  			if runtime.GOARCH == "wasm" {
  1115  				// TODO(go.dev/issue/71134) can lead to goroutine starvation.
  1116  				runtime.Gosched()
  1117  			}
  1118  		}
  1119  	}()
  1120  	if err := <-ch; err != nil {
  1121  		t.Errorf("OpenFile: %v, want success", err)
  1122  	}
  1123  	r.Close()
  1124  	if err := <-ch; !errors.Is(err, os.ErrClosed) {
  1125  		t.Errorf("OpenFile: %v, want ErrClosed", err)
  1126  	}
  1127  }
  1128  
  1129  // TestRootRaceRenameDir attempts to escape a Root by renaming a path component mid-parse.
  1130  //
  1131  // We create a deeply nested directory:
  1132  //
  1133  //	base/a/a/a/a/ [...] /a
  1134  //
  1135  // And a path that descends into the tree, then returns to the top using ..:
  1136  //
  1137  //	base/a/a/a/a/ [...] /a/../../../ [..] /../a/f
  1138  //
  1139  // While opening this file, we rename base/a/a to base/b.
  1140  // A naive lookup operation will resolve the path to base/f.
  1141  func TestRootRaceRenameDir(t *testing.T) {
  1142  	dir := t.TempDir()
  1143  	r, err := os.OpenRoot(dir)
  1144  	if err != nil {
  1145  		t.Fatal(err)
  1146  	}
  1147  	defer r.Close()
  1148  
  1149  	const depth = 4
  1150  
  1151  	os.MkdirAll(dir+"/base/"+strings.Repeat("/a", depth), 0o777)
  1152  
  1153  	path := "base/" + strings.Repeat("a/", depth) + strings.Repeat("../", depth) + "a/f"
  1154  	os.WriteFile(dir+"/f", []byte("secret"), 0o666)
  1155  	os.WriteFile(dir+"/base/a/f", []byte("public"), 0o666)
  1156  
  1157  	// Compute how long it takes to open the path in the common case.
  1158  	const tries = 10
  1159  	var total time.Duration
  1160  	for range tries {
  1161  		start := time.Now()
  1162  		f, err := r.Open(path)
  1163  		if err != nil {
  1164  			t.Fatal(err)
  1165  		}
  1166  		b, err := io.ReadAll(f)
  1167  		if err != nil {
  1168  			t.Fatal(err)
  1169  		}
  1170  		if string(b) != "public" {
  1171  			t.Fatalf("read %q, want %q", b, "public")
  1172  		}
  1173  		f.Close()
  1174  		total += time.Since(start)
  1175  	}
  1176  	avg := total / tries
  1177  
  1178  	// We're trying to exploit a race, so try this a number of times.
  1179  	for range 100 {
  1180  		// Start a goroutine to open the file.
  1181  		gotc := make(chan []byte)
  1182  		go func() {
  1183  			f, err := r.Open(path)
  1184  			if err != nil {
  1185  				gotc <- nil
  1186  			}
  1187  			defer f.Close()
  1188  			b, _ := io.ReadAll(f)
  1189  			gotc <- b
  1190  		}()
  1191  
  1192  		// Wait for the open operation to partially complete,
  1193  		// and then rename a directory near the root.
  1194  		time.Sleep(avg / 4)
  1195  		if err := os.Rename(dir+"/base/a", dir+"/b"); err != nil {
  1196  			// Windows and Plan9 won't let us rename a directory if we have
  1197  			// an open handle for it, so an error here is expected.
  1198  			switch runtime.GOOS {
  1199  			case "windows", "plan9":
  1200  			default:
  1201  				t.Fatal(err)
  1202  			}
  1203  		}
  1204  
  1205  		got := <-gotc
  1206  		os.Rename(dir+"/b", dir+"/base/a")
  1207  		if len(got) > 0 && string(got) != "public" {
  1208  			t.Errorf("read file: %q; want error or 'public'", got)
  1209  		}
  1210  	}
  1211  }
  1212  
  1213  func TestRootSymlinkToRoot(t *testing.T) {
  1214  	dir := makefs(t, []string{
  1215  		"d/d => ..",
  1216  	})
  1217  	root, err := os.OpenRoot(dir)
  1218  	if err != nil {
  1219  		t.Fatal(err)
  1220  	}
  1221  	defer root.Close()
  1222  	if err := root.Mkdir("d/d/new", 0777); err != nil {
  1223  		t.Fatal(err)
  1224  	}
  1225  	f, err := root.Open("d/d")
  1226  	if err != nil {
  1227  		t.Fatal(err)
  1228  	}
  1229  	defer f.Close()
  1230  	names, err := f.Readdirnames(-1)
  1231  	if err != nil {
  1232  		t.Fatal(err)
  1233  	}
  1234  	slices.Sort(names)
  1235  	if got, want := names, []string{"d", "new"}; !slices.Equal(got, want) {
  1236  		t.Errorf("root contains: %q, want %q", got, want)
  1237  	}
  1238  }
  1239  
  1240  func TestOpenInRoot(t *testing.T) {
  1241  	dir := makefs(t, []string{
  1242  		"file",
  1243  		"link => ../ROOT/file",
  1244  	})
  1245  	f, err := os.OpenInRoot(dir, "file")
  1246  	if err != nil {
  1247  		t.Fatalf("OpenInRoot(`file`) = %v, want success", err)
  1248  	}
  1249  	f.Close()
  1250  	for _, name := range []string{
  1251  		"link",
  1252  		"../ROOT/file",
  1253  		dir + "/file",
  1254  	} {
  1255  		f, err := os.OpenInRoot(dir, name)
  1256  		if err == nil {
  1257  			f.Close()
  1258  			t.Fatalf("OpenInRoot(%q) = nil, want error", name)
  1259  		}
  1260  	}
  1261  }
  1262  

View as plain text