Source file src/os/root_windows_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  //go:build windows
     6  
     7  package os_test
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  	"internal/strconv"
    13  	"internal/syscall/windows"
    14  	"internal/testenv"
    15  	"os"
    16  	"path/filepath"
    17  	"syscall"
    18  	"testing"
    19  	"unsafe"
    20  )
    21  
    22  func init() {
    23  	createJunction = func(t *testing.T, link, target string) {
    24  		if !filepath.IsAbs(target) {
    25  			target = filepath.Dir(link) + `\` + target
    26  		}
    27  		target, err := syscall.FullPath(target)
    28  		if err != nil {
    29  			t.Fatal(err)
    30  		}
    31  		var rd reparseData
    32  		rd.addSubstituteName(`\??\` + target)
    33  		rd.addPrintName(target)
    34  		if err := createMountPoint(link, &rd); err != nil {
    35  			t.Fatal(err)
    36  		}
    37  	}
    38  }
    39  
    40  func TestRootOpenatFallback(t *testing.T) {
    41  	windows.TestOpenatFallback = true
    42  	t.Cleanup(func() { windows.TestOpenatFallback = false })
    43  
    44  	// Exercise the existing path traversal and symlink confinement cases
    45  	// with OBJ_DONT_REPARSE unavailable, as on Windows 10 build 10240.
    46  	t.Run("OpenFile", TestRootOpen_File)
    47  	t.Run("OpenDirectory", TestRootOpen_Directory)
    48  	t.Run("Create", TestRootCreate)
    49  	t.Run("Stat", TestRootStat)
    50  	t.Run("Lstat", TestRootLstat)
    51  	t.Run("RemoveAll", TestRootRemoveAll)
    52  	t.Run("RemoveAllNoRoot", TestRemoveAll)
    53  	t.Run("DeleteOnClose", testRootOpenFileDeleteOnClose)
    54  	t.Run("LegacyDelete", func(t *testing.T) {
    55  		windows.TestDeleteatFallback = true
    56  		t.Cleanup(func() { windows.TestDeleteatFallback = false })
    57  		t.Run("RemoveAll", TestRootRemoveAll)
    58  		t.Run("RemoveAllNoRoot", TestRemoveAll)
    59  	})
    60  }
    61  
    62  // Verify that Root.Open rejects Windows reserved names.
    63  func TestRootWindowsDeviceNames(t *testing.T) {
    64  	r, err := os.OpenRoot(t.TempDir())
    65  	if err != nil {
    66  		t.Fatal(err)
    67  	}
    68  	defer r.Close()
    69  	if f, err := r.Open("NUL"); err == nil {
    70  		t.Errorf(`r.Open("NUL") succeeded; want error"`)
    71  		f.Close()
    72  	}
    73  }
    74  
    75  // Verify that Root.Open is case-insensitive.
    76  // (The wrong options to NtOpenFile could make operations case-sensitive,
    77  // so this is worth checking.)
    78  func TestRootWindowsCaseInsensitivity(t *testing.T) {
    79  	dir := t.TempDir()
    80  	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
    81  		t.Fatal(err)
    82  	}
    83  	r, err := os.OpenRoot(dir)
    84  	if err != nil {
    85  		t.Fatal(err)
    86  	}
    87  	defer r.Close()
    88  	f, err := r.Open("FILE")
    89  	if err != nil {
    90  		t.Fatal(err)
    91  	}
    92  	f.Close()
    93  	if err := r.Remove("FILE"); err != nil {
    94  		t.Fatal(err)
    95  	}
    96  	if _, err := os.Stat(filepath.Join(dir, "file")); !errors.Is(err, os.ErrNotExist) {
    97  		t.Fatalf("os.Stat(file) after deletion: %v, want ErrNotFound", err)
    98  	}
    99  }
   100  
   101  // TestRootSymlinkRelativity tests that symlinks created using Root.Symlink have the
   102  // same SYMLINK_FLAG_RELATIVE value as ones creates using os.Symlink.
   103  func TestRootSymlinkRelativity(t *testing.T) {
   104  	testenv.MustHaveSymlink(t)
   105  
   106  	dir := t.TempDir()
   107  	root, err := os.OpenRoot(dir)
   108  	if err != nil {
   109  		t.Fatal(err)
   110  	}
   111  	defer root.Close()
   112  
   113  	for i, test := range []struct {
   114  		name   string
   115  		target string
   116  	}{{
   117  		name:   "relative",
   118  		target: `foo`,
   119  	}, {
   120  		name:   "absolute",
   121  		target: `C:\foo`,
   122  	}, {
   123  		name:   "current working directory-relative",
   124  		target: `C:foo`,
   125  	}, {
   126  		name:   "root-relative",
   127  		target: `\foo`,
   128  	}, {
   129  		name:   "question prefix",
   130  		target: `\\?\foo`,
   131  	}, {
   132  		name:   "relative with dot dot",
   133  		target: `a\..\b`, // could be cleaned (but isn't)
   134  	}} {
   135  		t.Run(test.name, func(t *testing.T) {
   136  			name := fmt.Sprintf("symlink_%v", i)
   137  			if err := os.Symlink(test.target, filepath.Join(dir, name)); err != nil {
   138  				t.Fatal(err)
   139  			}
   140  			if err := root.Symlink(test.target, name+"_at"); err != nil {
   141  				t.Fatal(err)
   142  			}
   143  
   144  			osRDB, err := readSymlinkReparseData(filepath.Join(dir, name))
   145  			if err != nil {
   146  				t.Fatal(err)
   147  			}
   148  			rootRDB, err := readSymlinkReparseData(filepath.Join(dir, name+"_at"))
   149  			if err != nil {
   150  				t.Fatal(err)
   151  			}
   152  			if osRDB.Flags != rootRDB.Flags {
   153  				t.Errorf("symlink target %q: Symlink flags = %x, Root.Symlink flags = %x", test.target, osRDB.Flags, rootRDB.Flags)
   154  			}
   155  
   156  			// Compare the link target.
   157  			// os.Symlink converts current working directory-relative links
   158  			// such as c:foo into absolute links.
   159  			osTarget, err := os.Readlink(filepath.Join(dir, name))
   160  			if err != nil {
   161  				t.Fatal(err)
   162  			}
   163  			rootTarget, err := os.Readlink(filepath.Join(dir, name+"_at"))
   164  			if err != nil {
   165  				t.Fatal(err)
   166  			}
   167  			if osTarget != rootTarget {
   168  				t.Errorf("symlink created with target %q: Symlink target = %q, Root.Symlink target = %q", test.target, osTarget, rootTarget)
   169  			}
   170  		})
   171  	}
   172  }
   173  
   174  func readSymlinkReparseData(name string) (*windows.SymbolicLinkReparseBuffer, error) {
   175  	nameu16, err := syscall.UTF16FromString(name)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	h, err := syscall.CreateFile(&nameu16[0], syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING,
   180  		syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	defer syscall.CloseHandle(h)
   185  
   186  	var rdbbuf [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte
   187  	var bytesReturned uint32
   188  	err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
   194  	if rdb.ReparseTag != syscall.IO_REPARSE_TAG_SYMLINK {
   195  		return nil, fmt.Errorf("%q: not a symlink", name)
   196  	}
   197  
   198  	bufoff := unsafe.Offsetof(rdb.DUMMYUNIONNAME)
   199  	symlinkBuf := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdbbuf[bufoff]))
   200  
   201  	return symlinkBuf, nil
   202  }
   203  
   204  // TestRootSymlinkToDirectory tests that Root.Symlink creates directory links
   205  // when the target is a directory contained within the root.
   206  func TestRootSymlinkToDirectory(t *testing.T) {
   207  	testenv.MustHaveSymlink(t)
   208  
   209  	dir := t.TempDir()
   210  	root, err := os.OpenRoot(dir)
   211  	if err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	defer root.Close()
   215  
   216  	if err := os.Mkdir(filepath.Join(dir, "dir"), 0777); err != nil {
   217  		t.Fatal(err)
   218  	}
   219  	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
   220  		t.Fatal(err)
   221  	}
   222  
   223  	dir2 := t.TempDir()
   224  
   225  	for i, test := range []struct {
   226  		name    string
   227  		target  string
   228  		wantDir bool
   229  	}{{
   230  		name:    "directory outside root",
   231  		target:  dir2,
   232  		wantDir: false,
   233  	}, {
   234  		name:    "directory inside root",
   235  		target:  "dir",
   236  		wantDir: true,
   237  	}, {
   238  		name:    "file inside root",
   239  		target:  "file",
   240  		wantDir: false,
   241  	}, {
   242  		name:    "nonexistent inside root",
   243  		target:  "nonexistent",
   244  		wantDir: false,
   245  	}} {
   246  		t.Run(test.name, func(t *testing.T) {
   247  			name := fmt.Sprintf("symlink_%v", i)
   248  			if err := root.Symlink(test.target, name); err != nil {
   249  				t.Fatal(err)
   250  			}
   251  
   252  			// Lstat strips the directory mode bit from reparse points,
   253  			// so we need to use GetFileInformationByHandle directly to
   254  			// determine if this is a directory link.
   255  			nameu16, err := syscall.UTF16PtrFromString(filepath.Join(dir, name))
   256  			if err != nil {
   257  				t.Fatal(err)
   258  			}
   259  			h, err := syscall.CreateFile(nameu16, 0, 0, nil, syscall.OPEN_EXISTING,
   260  				syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
   261  			if err != nil {
   262  				t.Fatal(err)
   263  			}
   264  			defer syscall.CloseHandle(h)
   265  			var fi syscall.ByHandleFileInformation
   266  			if err := syscall.GetFileInformationByHandle(h, &fi); err != nil {
   267  				t.Fatal(err)
   268  			}
   269  			gotDir := fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0
   270  
   271  			if got, want := gotDir, test.wantDir; got != want {
   272  				t.Errorf("link target %q: isDir = %v, want %v", test.target, got, want)
   273  			}
   274  		})
   275  	}
   276  }
   277  
   278  func TestRootSymlinkNormalization(t *testing.T) {
   279  	if !testenv.HasSymlink() {
   280  		t.Skip("skipping test; no symlink support")
   281  	}
   282  	const content = "dir/target" // same as file name
   283  	dir := makefs(t, []string{
   284  		"dir/target",
   285  	})
   286  	root, err := os.OpenRoot(dir)
   287  	if err != nil {
   288  		t.Fatal(err)
   289  	}
   290  	defer root.Close()
   291  	if err := root.Symlink("dir/target", "link"); err != nil {
   292  		t.Fatal(err)
   293  	}
   294  	got, err := os.ReadFile(dir + "/link")
   295  	if err != nil {
   296  		t.Fatal(err)
   297  	}
   298  	if string(got) != content {
   299  		t.Fatalf("read link contents %q, want %q", got, content)
   300  	}
   301  }
   302  
   303  func TestRootOpenFileTruncateNamedPipe(t *testing.T) {
   304  	t.Parallel()
   305  	name := pipeName()
   306  	pipe := newBytePipe(t, name, false)
   307  	defer pipe.Close()
   308  
   309  	root, err := os.OpenRoot(filepath.Dir(name))
   310  	if err != nil {
   311  		t.Fatal(err)
   312  	}
   313  	defer root.Close()
   314  
   315  	f, err := root.OpenFile(filepath.Base(name), os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0666)
   316  	if err != nil {
   317  		t.Fatal(err)
   318  	}
   319  	f.Close()
   320  }
   321  
   322  func TestRootOpenFileFlags(t *testing.T) {
   323  	t.Parallel()
   324  
   325  	dir := t.TempDir()
   326  	root, err := os.OpenRoot(dir)
   327  	if err != nil {
   328  		t.Fatal(err)
   329  	}
   330  	defer root.Close()
   331  
   332  	// The only way to retrieve some of the flags passed in CreateFile
   333  	// is using NtQueryInformationFile, which returns the file flags
   334  	// NT equivalent. Note that FILE_SYNCHRONOUS_IO_NONALERT is always
   335  	// set when FILE_FLAG_OVERLAPPED is not passed.
   336  	// The flags that can't be retrieved using NtQueryInformationFile won't
   337  	// be tested in here, but we at least know that the logic to handle them is correct.
   338  	tests := []struct {
   339  		flag     uint32
   340  		wantMode uint32
   341  	}{
   342  		{0, windows.FILE_SYNCHRONOUS_IO_NONALERT},
   343  		{windows.O_FILE_FLAG_OVERLAPPED, 0},
   344  		{windows.O_FILE_FLAG_NO_BUFFERING, windows.FILE_NO_INTERMEDIATE_BUFFERING | windows.FILE_SYNCHRONOUS_IO_NONALERT},
   345  		{windows.O_FILE_FLAG_NO_BUFFERING | windows.O_FILE_FLAG_OVERLAPPED, windows.FILE_NO_INTERMEDIATE_BUFFERING},
   346  		{windows.O_FILE_FLAG_SEQUENTIAL_SCAN, windows.FILE_SEQUENTIAL_ONLY | windows.FILE_SYNCHRONOUS_IO_NONALERT},
   347  		{windows.O_FILE_FLAG_WRITE_THROUGH, windows.FILE_WRITE_THROUGH | windows.FILE_SYNCHRONOUS_IO_NONALERT},
   348  	}
   349  	for i, tt := range tests {
   350  		t.Run(strconv.Itoa(i), func(t *testing.T) {
   351  			f, err := root.OpenFile(strconv.Itoa(i)+".txt", syscall.O_RDWR|syscall.O_CREAT|int(tt.flag), 0666)
   352  			if err != nil {
   353  				t.Fatal(err)
   354  			}
   355  			defer f.Close()
   356  			var info windows.FILE_MODE_INFORMATION
   357  			if err := windows.NtQueryInformationFile(syscall.Handle(f.Fd()), &windows.IO_STATUS_BLOCK{},
   358  				unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), windows.FileModeInformation); err != nil {
   359  				t.Fatal(err)
   360  			}
   361  			if info.Mode != tt.wantMode {
   362  				t.Errorf("file mode = 0x%x; want 0x%x", info.Mode, tt.wantMode)
   363  			}
   364  		})
   365  	}
   366  }
   367  
   368  func TestRootOpenFileDeleteOnClose(t *testing.T) {
   369  	t.Parallel()
   370  	testRootOpenFileDeleteOnClose(t)
   371  }
   372  
   373  func testRootOpenFileDeleteOnClose(t *testing.T) {
   374  	dir := t.TempDir()
   375  	root, err := os.OpenRoot(dir)
   376  	if err != nil {
   377  		t.Fatal(err)
   378  	}
   379  	defer root.Close()
   380  	const name = "test.txt"
   381  	f, err := root.OpenFile(name, syscall.O_RDWR|syscall.O_CREAT|windows.O_FILE_FLAG_DELETE_ON_CLOSE, 0666)
   382  	if err != nil {
   383  		t.Fatal(err)
   384  	}
   385  	if err := f.Close(); err != nil {
   386  		t.Fatal(err)
   387  	}
   388  	// The file should be deleted after closing.
   389  	if _, err := os.Stat(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) {
   390  		t.Errorf("expected file to be deleted, got %v", err)
   391  	}
   392  }
   393  
   394  func TestRootOpenFileFlagInvalid(t *testing.T) {
   395  	t.Parallel()
   396  	dir := t.TempDir()
   397  	root, err := os.OpenRoot(dir)
   398  	if err != nil {
   399  		t.Fatal(err)
   400  	}
   401  	defer root.Close()
   402  	// invalidFileFlag is the only value in the file flag range that is not supported,
   403  	// as it is not defined in the Windows API.
   404  	const invalidFileFlag = 0x00400000
   405  	f, err := root.OpenFile("test.txt", syscall.O_RDWR|syscall.O_CREAT|invalidFileFlag, 0666)
   406  	if !errors.Is(err, os.ErrInvalid) {
   407  		t.Fatalf("expected os.ErrInvalid, got %v", err)
   408  	}
   409  	f.Close()
   410  }
   411  

View as plain text