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

View as plain text