Source file src/os/root_windows.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
     8  
     9  import (
    10  	"errors"
    11  	"internal/filepathlite"
    12  	"internal/stringslite"
    13  	"internal/syscall/windows"
    14  	"runtime"
    15  	"syscall"
    16  	"time"
    17  	"unsafe"
    18  )
    19  
    20  // rootCleanPath uses GetFullPathName to perform lexical path cleaning.
    21  //
    22  // On Windows, file names are lexically cleaned at the start of a file operation.
    23  // For example, on Windows the path `a\..\b` is exactly equivalent to `b` alone,
    24  // even if `a` does not exist or is not a directory.
    25  //
    26  // We use the Windows API function GetFullPathName to perform this cleaning.
    27  // We could do this ourselves, but there are a number of subtle behaviors here,
    28  // and deferring to the OS maintains consistency.
    29  // (For example, `a\.\` cleans to `a\`.)
    30  //
    31  // GetFullPathName operates on absolute paths, and our input path is relative.
    32  // We make the path absolute by prepending a fixed prefix of \\?\?\.
    33  //
    34  // We want to detect paths which use .. components to escape the root.
    35  // We do this by ensuring the cleaned path still begins with \\?\?\.
    36  // We catch the corner case of a path which includes a ..\?\. component
    37  // by rejecting any input paths which contain a ?, which is not a valid character
    38  // in a Windows filename.
    39  func rootCleanPath(s string, prefix, suffix []string) (string, error) {
    40  	// Reject paths which include a ? component (see above).
    41  	if stringslite.IndexByte(s, '?') >= 0 {
    42  		return "", windows.ERROR_INVALID_NAME
    43  	}
    44  
    45  	const fixedPrefix = `\\?\?`
    46  	buf := []byte(fixedPrefix)
    47  	for _, p := range prefix {
    48  		buf = append(buf, '\\')
    49  		buf = append(buf, []byte(p)...)
    50  	}
    51  	buf = append(buf, '\\')
    52  	buf = append(buf, []byte(s)...)
    53  	for _, p := range suffix {
    54  		buf = append(buf, '\\')
    55  		buf = append(buf, []byte(p)...)
    56  	}
    57  	s = string(buf)
    58  
    59  	s, err := syscall.FullPath(s)
    60  	if err != nil {
    61  		return "", err
    62  	}
    63  
    64  	s, ok := stringslite.CutPrefix(s, fixedPrefix)
    65  	if !ok {
    66  		return "", errPathEscapes
    67  	}
    68  	s = stringslite.TrimPrefix(s, `\`)
    69  	if s == "" {
    70  		s = "."
    71  	}
    72  
    73  	if !filepathlite.IsLocal(s) {
    74  		return "", errPathEscapes
    75  	}
    76  
    77  	return s, nil
    78  }
    79  
    80  // sysfdType is the native type of a file handle
    81  // (int on Unix, syscall.Handle on Windows),
    82  // permitting helper functions to be written portably.
    83  type sysfdType = syscall.Handle
    84  
    85  // openRootNolog is OpenRoot.
    86  func openRootNolog(name string) (*Root, error) {
    87  	if name == "" {
    88  		return nil, &PathError{Op: "open", Path: name, Err: syscall.ENOENT}
    89  	}
    90  	path := fixLongPath(name)
    91  	fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
    92  	if err != nil {
    93  		return nil, &PathError{Op: "open", Path: name, Err: err}
    94  	}
    95  	return newRoot(fd, name)
    96  }
    97  
    98  // newRoot returns a new Root.
    99  // If fd is not a directory, it closes it and returns an error.
   100  func newRoot(fd syscall.Handle, name string) (*Root, error) {
   101  	// Check that this is a directory.
   102  	//
   103  	// If we get any errors here, ignore them; worst case we create a Root
   104  	// which returns errors when you try to use it.
   105  	var fi syscall.ByHandleFileInformation
   106  	err := syscall.GetFileInformationByHandle(fd, &fi)
   107  	if err == nil && fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 {
   108  		syscall.CloseHandle(fd)
   109  		return nil, &PathError{Op: "open", Path: name, Err: errors.New("not a directory")}
   110  	}
   111  
   112  	r := &Root{&root{
   113  		fd:   fd,
   114  		name: name,
   115  	}}
   116  	runtime.SetFinalizer(r.root, (*root).Close)
   117  	return r, nil
   118  }
   119  
   120  // openRootInRoot is Root.OpenRoot.
   121  func openRootInRoot(r *Root, name string) (*Root, error) {
   122  	fd, err := doInRoot(r, name, 0, nil, func(parent syscall.Handle, name string, endsInSlash bool) (syscall.Handle, error) {
   123  		return rootOpenDir(parent, name)
   124  	})
   125  	if err != nil {
   126  		return nil, &PathError{Op: "openat", Path: name, Err: err}
   127  	}
   128  	return newRoot(fd, joinPath(r.Name(), name))
   129  }
   130  
   131  // rootOpenFileNolog is Root.OpenFile.
   132  func rootOpenFileNolog(root *Root, name string, flag int, perm FileMode) (*File, error) {
   133  	fd, err := doInRoot(root, name, doInRootNoHandleTerminalSlash, nil, func(parent syscall.Handle, name string, endsInSlash bool) (syscall.Handle, error) {
   134  		if endsInSlash {
   135  			flag |= windows.O_DIRECTORY
   136  		}
   137  		return openat(parent, name, uint64(flag), perm)
   138  	})
   139  	if err != nil {
   140  		return nil, &PathError{Op: "openat", Path: name, Err: err}
   141  	}
   142  	nonblocking := flag&windows.O_FILE_FLAG_OVERLAPPED != 0
   143  	return newFile(fd, joinPath(root.Name(), name), kindOpenFile, nonblocking), nil
   144  }
   145  
   146  func openat(dirfd syscall.Handle, name string, flag uint64, perm FileMode) (syscall.Handle, error) {
   147  	h, err := windows.Openat(dirfd, name, flag|syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY, syscallMode(perm))
   148  	if err == syscall.ELOOP || err == syscall.ENOTDIR {
   149  		if link, err := readReparseLinkAt(dirfd, name); err == nil {
   150  			return syscall.InvalidHandle, errSymlink(link)
   151  		}
   152  	}
   153  	return h, err
   154  }
   155  
   156  func readReparseLinkAt(dirfd syscall.Handle, name string) (string, error) {
   157  	objectName, err := windows.NewNTUnicodeString(name)
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  	objAttrs := &windows.OBJECT_ATTRIBUTES{
   162  		ObjectName: objectName,
   163  	}
   164  	if dirfd != syscall.InvalidHandle {
   165  		objAttrs.RootDirectory = dirfd
   166  	}
   167  	objAttrs.Length = uint32(unsafe.Sizeof(*objAttrs))
   168  	var h syscall.Handle
   169  	err = windows.NtCreateFile(
   170  		&h,
   171  		windows.FILE_GENERIC_READ,
   172  		objAttrs,
   173  		&windows.IO_STATUS_BLOCK{},
   174  		nil,
   175  		uint32(syscall.FILE_ATTRIBUTE_NORMAL),
   176  		syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
   177  		windows.FILE_OPEN,
   178  		windows.FILE_SYNCHRONOUS_IO_NONALERT|windows.FILE_OPEN_REPARSE_POINT,
   179  		nil,
   180  		0,
   181  	)
   182  	if err != nil {
   183  		return "", err
   184  	}
   185  	defer syscall.CloseHandle(h)
   186  	return readReparseLinkHandle(h)
   187  }
   188  
   189  func rootOpenDir(parent syscall.Handle, name string) (syscall.Handle, error) {
   190  	h, err := openat(parent, name, syscall.O_RDONLY|syscall.O_CLOEXEC|windows.O_DIRECTORY, 0)
   191  	if err == syscall.ERROR_FILE_NOT_FOUND {
   192  		// Windows returns:
   193  		//   - ERROR_PATH_NOT_FOUND if any path compoenent before the leaf
   194  		//     does not exist or is not a directory.
   195  		//   - ERROR_FILE_NOT_FOUND if the leaf does not exist.
   196  		//
   197  		// This differs from Unix behavior, which is:
   198  		//   - ENOENT if any path component does not exist, including the leaf.
   199  		//   - ENOTDIR if any path component before the leaf is not a directory.
   200  		//
   201  		// We map syscall.ENOENT to ERROR_FILE_NOT_FOUND and syscall.ENOTDIR
   202  		// to ERROR_PATH_NOT_FOUND, but the Windows errors don't quite match.
   203  		//
   204  		// For consistency with os.Open, convert ERROR_FILE_NOT_FOUND here into
   205  		// ERROR_PATH_NOT_FOUND, since we're opening a non-leaf path component.
   206  		err = syscall.ERROR_PATH_NOT_FOUND
   207  	}
   208  	return h, err
   209  }
   210  
   211  func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
   212  	var flags uint
   213  	if lstat {
   214  		// Follow symlinks in the last path component when the path
   215  		// ends with a path separator.
   216  		//
   217  		// This is not the usual behavior for Windows path resolution,
   218  		// but empirically os.Lstat behaves this way.
   219  		flags = doInRootAlwaysResolveTerminalSlash
   220  	}
   221  	fi, err := doInRoot(r, name, flags, nil, func(parent syscall.Handle, n string, endsInSlash bool) (FileInfo, error) {
   222  		fd, err := openat(parent, n, windows.O_FILE_FLAG_OPEN_REPARSE_POINT, 0)
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  		defer syscall.CloseHandle(fd)
   227  		fi, err := statHandle(name, fd)
   228  		if err != nil {
   229  			return nil, err
   230  		}
   231  		if !lstat && fi.(*fileStat).isReparseTagNameSurrogate() {
   232  			link, err := readReparseLinkHandle(fd)
   233  			if err != nil {
   234  				return nil, err
   235  			}
   236  			return nil, errSymlink(link)
   237  		}
   238  		return fi, nil
   239  	})
   240  	if err != nil {
   241  		return nil, &PathError{Op: "statat", Path: name, Err: err}
   242  	}
   243  	return fi, nil
   244  }
   245  
   246  func rootSymlink(r *Root, oldname, newname string) error {
   247  	if oldname == "" {
   248  		return syscall.EINVAL
   249  	}
   250  
   251  	// CreateSymbolicLinkW converts volume-relative paths into absolute ones.
   252  	// Do the same.
   253  	if filepathlite.VolumeNameLen(oldname) > 0 && !filepathlite.IsAbs(oldname) {
   254  		p, err := syscall.FullPath(oldname)
   255  		if err == nil {
   256  			oldname = p
   257  		}
   258  	}
   259  
   260  	// If oldname can be resolved to a directory in the root, create a directory link.
   261  	// Otherwise, create a file link.
   262  	var flags windows.SymlinkatFlags
   263  	if filepathlite.VolumeNameLen(oldname) == 0 && !IsPathSeparator(oldname[0]) {
   264  		// oldname is a path relative to the directory containing newname.
   265  		// Prepend newname's directory to it to make a path relative to the root.
   266  		// For example, if oldname=old and newname=a\new, destPath=a\old.
   267  		destPath := oldname
   268  		if dir := dirname(newname); dir != "." {
   269  			destPath = dir + `\` + oldname
   270  		}
   271  		fi, err := r.Stat(destPath)
   272  		if err == nil && fi.IsDir() {
   273  			flags |= windows.SYMLINKAT_DIRECTORY
   274  		}
   275  	}
   276  
   277  	// Empirically, CreateSymbolicLinkW appears to set the relative flag iff
   278  	// the target does not contain a volume name.
   279  	if filepathlite.VolumeNameLen(oldname) == 0 {
   280  		flags |= windows.SYMLINKAT_RELATIVE
   281  	}
   282  
   283  	_, err := doInRoot(r, newname, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
   284  		return struct{}{}, windows.Symlinkat(oldname, parent, name, flags)
   285  	})
   286  	if err != nil {
   287  		return &LinkError{"symlinkat", oldname, newname, err}
   288  	}
   289  	return nil
   290  }
   291  
   292  func chmodat(parent syscall.Handle, name string, mode FileMode) error {
   293  	// Currently, on Windows os.Chmod("symlink") will act on "symlink",
   294  	// not on any file it points to.
   295  	//
   296  	// This may or may not be the desired behavior: https://go.dev/issue/71492
   297  	//
   298  	// For now, be consistent with os.Symlink.
   299  	// Passing O_FILE_FLAG_OPEN_REPARSE_POINT causes us to open the named file itself,
   300  	// not any file that it links to.
   301  	//
   302  	// If we want to change this in the future, pass O_NOFOLLOW_ANY instead
   303  	// and return errSymlink when encountering a symlink:
   304  	//
   305  	//     if err == syscall.ELOOP || err == syscall.ENOTDIR {
   306  	//         if link, err := readReparseLinkAt(parent, name); err == nil {
   307  	//                 return errSymlink(link)
   308  	//         }
   309  	//     }
   310  	h, err := windows.Openat(parent, name, syscall.O_CLOEXEC|windows.O_FILE_FLAG_OPEN_REPARSE_POINT|windows.O_WRITE_ATTRS, 0)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	defer syscall.CloseHandle(h)
   315  
   316  	var d syscall.ByHandleFileInformation
   317  	if err := syscall.GetFileInformationByHandle(h, &d); err != nil {
   318  		return err
   319  	}
   320  	attrs := d.FileAttributes
   321  
   322  	if mode&syscall.S_IWRITE != 0 {
   323  		attrs &^= syscall.FILE_ATTRIBUTE_READONLY
   324  	} else {
   325  		attrs |= syscall.FILE_ATTRIBUTE_READONLY
   326  	}
   327  	if attrs == d.FileAttributes {
   328  		return nil
   329  	}
   330  
   331  	var fbi windows.FILE_BASIC_INFO
   332  	fbi.FileAttributes = attrs
   333  	return windows.SetFileInformationByHandle(h, windows.FileBasicInfo, unsafe.Pointer(&fbi), uint32(unsafe.Sizeof(fbi)))
   334  }
   335  
   336  func chownat(parent syscall.Handle, name string, uid, gid int) error {
   337  	return syscall.EWINDOWS // matches syscall.Chown
   338  }
   339  
   340  func lchownat(parent syscall.Handle, name string, uid, gid int) error {
   341  	return syscall.EWINDOWS // matches syscall.Lchown
   342  }
   343  
   344  func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
   345  	return windows.Mkdirat(dirfd, name, syscallMode(perm))
   346  }
   347  
   348  func removeat(dirfd syscall.Handle, name string) error {
   349  	return windows.Deleteat(dirfd, name, 0)
   350  }
   351  
   352  func removefileat(dirfd syscall.Handle, name string) error {
   353  	return windows.Deleteat(dirfd, name, windows.FILE_NON_DIRECTORY_FILE)
   354  }
   355  
   356  func removedirat(dirfd syscall.Handle, name string) error {
   357  	return windows.Deleteat(dirfd, name, windows.FILE_DIRECTORY_FILE)
   358  }
   359  
   360  func chtimesat(dirfd syscall.Handle, name string, atime time.Time, mtime time.Time) error {
   361  	h, err := windows.Openat(dirfd, name, syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY|windows.O_WRITE_ATTRS, 0)
   362  	if err == syscall.ELOOP || err == syscall.ENOTDIR {
   363  		if link, err := readReparseLinkAt(dirfd, name); err == nil {
   364  			return errSymlink(link)
   365  		}
   366  	}
   367  	if err != nil {
   368  		return err
   369  	}
   370  	defer syscall.CloseHandle(h)
   371  	a := syscall.Filetime{}
   372  	w := syscall.Filetime{}
   373  	if !atime.IsZero() {
   374  		a = syscall.NsecToFiletime(atime.UnixNano())
   375  	}
   376  	if !mtime.IsZero() {
   377  		w = syscall.NsecToFiletime(mtime.UnixNano())
   378  	}
   379  	return syscall.SetFileTime(h, nil, &a, &w)
   380  }
   381  
   382  func renameat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newname string) error {
   383  	return windows.Renameat(oldfd, oldname, newfd, newname)
   384  }
   385  
   386  func linkat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newname string) error {
   387  	return windows.Linkat(oldfd, oldname, newfd, newname)
   388  }
   389  
   390  // checkSymlink resolves the symlink name in parent,
   391  // and returns errSymlink with the link contents.
   392  //
   393  // If name is not a symlink, return origError.
   394  func checkSymlink(parent syscall.Handle, name string, origError error) error {
   395  	link, err := readlinkat(parent, name)
   396  	if err != nil {
   397  		return origError
   398  	}
   399  	return errSymlink(link)
   400  }
   401  
   402  func readlinkat(dirfd syscall.Handle, name string) (string, error) {
   403  	fd, err := openat(dirfd, name, windows.O_FILE_FLAG_OPEN_REPARSE_POINT, 0)
   404  	if err != nil {
   405  		return "", err
   406  	}
   407  	defer syscall.CloseHandle(fd)
   408  	return readReparseLinkHandle(fd)
   409  }
   410  
   411  func lstatat(parent syscall.Handle, name string) (FileInfo, error) {
   412  	fd, err := openat(parent, name, windows.O_FILE_FLAG_OPEN_REPARSE_POINT, 0)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  	defer syscall.CloseHandle(fd)
   417  	fi, err := statHandle(name, fd)
   418  	if err != nil {
   419  		return nil, err
   420  	}
   421  	return fi, nil
   422  }
   423  
   424  // isDirectoryLink reports whether fi (assumed to be a symlink) is a directory link.
   425  // Windows symlinks come in two flavors: file and directory. This function distinguishes
   426  // between the two.
   427  func isDirectoryLink(fi FileInfo) bool {
   428  	fs, ok := fi.(*fileStat)
   429  	return ok && fs.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0
   430  }
   431  

View as plain text