Source file src/os/root_openat.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 unix || windows || wasip1
     6  
     7  package os
     8  
     9  import (
    10  	"io/fs"
    11  	"runtime"
    12  	"slices"
    13  	"sync"
    14  	"syscall"
    15  	"time"
    16  )
    17  
    18  // root implementation for platforms with a function to open a file
    19  // relative to a directory.
    20  type root struct {
    21  	name string
    22  
    23  	// refs is incremented while an operation is using fd.
    24  	// closed is set when Close is called.
    25  	// fd is closed when closed is true and refs is 0.
    26  	mu     sync.Mutex
    27  	fd     sysfdType
    28  	refs   int  // number of active operations
    29  	closed bool // set when closed
    30  }
    31  
    32  func (r *root) Close() error {
    33  	r.mu.Lock()
    34  	defer r.mu.Unlock()
    35  	if !r.closed && r.refs == 0 {
    36  		syscall.Close(r.fd)
    37  	}
    38  	r.closed = true
    39  	runtime.SetFinalizer(r, nil) // no need for a finalizer any more
    40  	return nil
    41  }
    42  
    43  func (r *root) incref() error {
    44  	r.mu.Lock()
    45  	defer r.mu.Unlock()
    46  	if r.closed {
    47  		return ErrClosed
    48  	}
    49  	r.refs++
    50  	return nil
    51  }
    52  
    53  func (r *root) decref() {
    54  	r.mu.Lock()
    55  	defer r.mu.Unlock()
    56  	if r.refs <= 0 {
    57  		panic("bad Root refcount")
    58  	}
    59  	r.refs--
    60  	if r.closed && r.refs == 0 {
    61  		syscall.Close(r.fd)
    62  	}
    63  }
    64  
    65  func (r *root) Name() string {
    66  	return r.name
    67  }
    68  
    69  func rootChmod(r *Root, name string, mode FileMode) error {
    70  	_, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
    71  		return struct{}{}, chmodat(parent, name, mode)
    72  	})
    73  	if err != nil {
    74  		return &PathError{Op: "chmodat", Path: name, Err: err}
    75  	}
    76  	return nil
    77  }
    78  
    79  func rootChown(r *Root, name string, uid, gid int) error {
    80  	_, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
    81  		return struct{}{}, chownat(parent, name, uid, gid)
    82  	})
    83  	if err != nil {
    84  		return &PathError{Op: "chownat", Path: name, Err: err}
    85  	}
    86  	return nil
    87  }
    88  
    89  func rootLchown(r *Root, name string, uid, gid int) error {
    90  	_, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
    91  		return struct{}{}, lchownat(parent, name, uid, gid)
    92  	})
    93  	if err != nil {
    94  		return &PathError{Op: "lchownat", Path: name, Err: err}
    95  	}
    96  	return nil
    97  }
    98  
    99  func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
   100  	_, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
   101  		return struct{}{}, chtimesat(parent, name, atime, mtime)
   102  	})
   103  	if err != nil {
   104  		return &PathError{Op: "chtimesat", Path: name, Err: err}
   105  	}
   106  	return nil
   107  }
   108  
   109  func rootMkdir(r *Root, name string, perm FileMode) error {
   110  	flags := uint(doInRootCreatingDirectory)
   111  	switch runtime.GOOS {
   112  	case "linux", "windows":
   113  		// These platforms do not follow "symlink" on "mkdir symlink/".
   114  		// (POSIX.1-2024 4.16 says that the trailing slash should cause
   115  		// resolution to follow the symlink, but we're trying to match
   116  		// platform semantics, not implement POSIX.)
   117  		flags |= doInRootNoHandleTerminalSlash
   118  	}
   119  	_, err := doInRoot(r, name, flags, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
   120  		return struct{}{}, mkdirat(parent, name, perm)
   121  	})
   122  	if err != nil {
   123  		return &PathError{Op: "mkdirat", Path: name, Err: err}
   124  	}
   125  	return nil
   126  }
   127  
   128  func rootMkdirAll(r *Root, fullname string, perm FileMode) error {
   129  	// doInRoot opens each path element in turn.
   130  	//
   131  	// openDirFunc opens all but the last path component.
   132  	// The usual default openDirFunc just opens directories with O_DIRECTORY.
   133  	// We replace it here with one that creates missing directories along the way.
   134  	openDirFunc := func(parent sysfdType, name string) (sysfdType, error) {
   135  		for try := range 2 {
   136  			fd, err := rootOpenDir(parent, name)
   137  			switch err.(type) {
   138  			case nil, errSymlink:
   139  				return fd, err
   140  			}
   141  			if try > 0 || !IsNotExist(err) {
   142  				return 0, &PathError{Op: "openat", Err: err}
   143  			}
   144  			// Try again on EEXIST, because the directory may have been created
   145  			// by another process or thread between the rootOpenDir and mkdirat calls.
   146  			if err := mkdirat(parent, name, perm); err != nil && err != syscall.EEXIST {
   147  				return 0, &PathError{Op: "mkdirat", Err: err}
   148  			}
   149  		}
   150  		panic("unreachable")
   151  	}
   152  	// openLastComponentFunc opens the last path component.
   153  	openLastComponentFunc := func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
   154  		err := mkdirat(parent, name, perm)
   155  		if err == syscall.EEXIST {
   156  			mode, e := modeAt(parent, name)
   157  			if e == nil {
   158  				if mode.IsDir() {
   159  					// The target of MkdirAll is an existing directory.
   160  					err = nil
   161  				} else if mode&ModeSymlink != 0 {
   162  					// The target of MkdirAll is a symlink.
   163  					// For consistency with os.MkdirAll,
   164  					// succeed if the link resolves to a directory.
   165  					// We don't return errSymlink here, because we don't
   166  					// want to create the link target if it doesn't exist.
   167  					fi, e := r.Stat(fullname)
   168  					if e == nil && fi.Mode().IsDir() {
   169  						err = nil
   170  					} else if e == nil {
   171  						err = syscall.ENOTDIR
   172  					} else if !IsNotExist(e) {
   173  						// EPERM, ELOOP, etc.,
   174  						// probably more useful than EEXIST.
   175  						err = e
   176  					}
   177  				}
   178  			}
   179  		}
   180  		switch err.(type) {
   181  		case nil, errSymlink:
   182  			return struct{}{}, err
   183  		}
   184  		return struct{}{}, &PathError{Op: "mkdirat", Err: err}
   185  	}
   186  	flags := uint(doInRootCreatingDirectory)
   187  	switch runtime.GOOS {
   188  	case "linux", "windows":
   189  		flags |= doInRootNoHandleTerminalSlash // see rootMkdir
   190  	}
   191  	_, err := doInRoot(r, fullname, flags, openDirFunc, openLastComponentFunc)
   192  	if err != nil {
   193  		if _, ok := err.(*PathError); !ok {
   194  			err = &PathError{Op: "mkdirat", Path: fullname, Err: err}
   195  		}
   196  	}
   197  	return err
   198  }
   199  
   200  func rootReadlink(r *Root, name string) (string, error) {
   201  	target, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (string, error) {
   202  		return readlinkat(parent, name)
   203  	})
   204  	if err != nil {
   205  		return "", &PathError{Op: "readlinkat", Path: name, Err: err}
   206  	}
   207  	return target, nil
   208  }
   209  
   210  func rootRemove(r *Root, name string) error {
   211  	_, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
   212  		return struct{}{}, removeat(parent, name)
   213  	})
   214  	if err != nil {
   215  		return &PathError{Op: "removeat", Path: name, Err: err}
   216  	}
   217  	return nil
   218  }
   219  
   220  func rootRemoveAll(r *Root, name string) error {
   221  	// Consistency with os.RemoveAll: Strip trailing /s from the name,
   222  	// so RemoveAll("not_a_directory/") succeeds.
   223  	for len(name) > 0 && IsPathSeparator(name[len(name)-1]) {
   224  		name = name[:len(name)-1]
   225  	}
   226  	if endsWithDot(name) {
   227  		// Consistency with os.RemoveAll: Return EINVAL when trying to remove .
   228  		return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL}
   229  	}
   230  	_, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) {
   231  		return struct{}{}, removeAllFrom(parent, name)
   232  	})
   233  	if IsNotExist(err) {
   234  		return nil
   235  	}
   236  	if err != nil {
   237  		return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)}
   238  	}
   239  	return err
   240  }
   241  
   242  func rootRename(r *Root, oldname, newname string) error {
   243  	_, err := doInRoot(r, oldname, 0, nil, func(oldparent sysfdType, oldname string, oldEndsInSlash bool) (struct{}, error) {
   244  		flags := uint(doInRootCreatingDirectory)
   245  		if runtime.GOOS == "windows" {
   246  			flags |= doInRootNoHandleTerminalSlash
   247  		}
   248  		_, err := doInRoot(r, newname, flags, nil, func(newparent sysfdType, newname string, newEndsInSlash bool) (struct{}, error) {
   249  			if runtime.GOOS != "windows" && newEndsInSlash {
   250  				oldMode, err := modeAt(oldparent, oldname)
   251  				if err != nil {
   252  					return struct{}{}, err
   253  				}
   254  				if oldMode.Type() != fs.ModeDir {
   255  					return struct{}{}, syscall.ENOTDIR
   256  				}
   257  			}
   258  			// Same checks as applied by rename (in file_unix.go):
   259  			fi, err := lstatat(newparent, newname)
   260  			if err == nil && fi.IsDir() {
   261  				if ofi, err := lstatat(oldparent, oldname); err != nil {
   262  					return struct{}{}, err
   263  				} else if newname == oldname || !SameFile(fi, ofi) {
   264  					return struct{}{}, syscall.EEXIST
   265  				}
   266  			}
   267  			return struct{}{}, renameat(oldparent, oldname, newparent, newname)
   268  		})
   269  		return struct{}{}, err
   270  	})
   271  	if err != nil {
   272  		return &LinkError{"renameat", oldname, newname, err}
   273  	}
   274  	return err
   275  }
   276  
   277  func rootLink(r *Root, oldname, newname string) error {
   278  	_, err := doInRoot(r, oldname, 0, nil, func(oldparent sysfdType, oldname string, oldEndsInSlash bool) (struct{}, error) {
   279  		flags := uint(0)
   280  		if runtime.GOOS == "windows" {
   281  			// Windows doesn't pay attention to trailing slashes in the link target.
   282  			flags |= doInRootNoHandleTerminalSlash
   283  		}
   284  		_, err := doInRoot(r, newname, flags, nil, func(newparent sysfdType, newname string, newEndsInSlash bool) (struct{}, error) {
   285  			return struct{}{}, linkat(oldparent, oldname, newparent, newname)
   286  		})
   287  		return struct{}{}, err
   288  	})
   289  	if err != nil {
   290  		return &LinkError{"linkat", oldname, newname, err}
   291  	}
   292  	return err
   293  }
   294  
   295  // Flags for doInRoot.
   296  const (
   297  	// doInRootNoHandleTerminalSlash prevents doInRoot from applying special handling
   298  	// for paths which end in one or more slashes.
   299  	doInRootNoHandleTerminalSlash = 1 << iota
   300  
   301  	// doInRootCreatingDirectory indicates that the operation is creating a directory.
   302  	// When a path ends in /, the last path component may name a file which does not exist.
   303  	doInRootCreatingDirectory
   304  
   305  	// doInRootAlwaysResolveTerminalSlash causes doInRoot to resolve symlinks in the last
   306  	// path component when a path ends in /, even on Windows. For example, this causes
   307  	// doInRoot to resolve "symlink/" as the link target of "symlink".
   308  	//
   309  	// POSIX path operations resolve symlinks in this case.
   310  	// Most Windows operations do not.
   311  	// This flag enforces the POSIX behavior.
   312  	doInRootAlwaysResolveTerminalSlash
   313  )
   314  
   315  // doInRoot performs an operation on a path in a Root.
   316  //
   317  // It calls f with the FD or handle for the directory containing the last
   318  // path element, the name of the last path element (not including slashes),
   319  // and a boolean indicating whether the original path ended in one or more slashes.
   320  //
   321  // For example, given the path a/b/c it calls f with the FD for a/b and the name "c".
   322  //
   323  // It applies special handling for paths ending in a slash: When a path ends in a slash
   324  // (for example "a/b/"), doInRoot will check the final component ("b") before calling f.
   325  // If the final component is a symlink, doInRoot will resolve it.
   326  // If the final component is neither a symlink nor a directory, doInRoot will return ENOTDIR.
   327  // This behavior may be disabled by passing the doInRootNoHandleTerminalSlash flag.
   328  //
   329  // If openDirFunc is non-nil, it is called to open intermediate path elements.
   330  // For example, given the path a/b/c openDirFunc will be called to open a and a/b in turn.
   331  //
   332  // f or openDirFunc may return errSymlink to indicate that the path element is a symlink
   333  // which should be followed. Note that this can result in f being called multiple times
   334  // with different names. For example, given the path "link" which is a symlink to "target",
   335  // f is called with the path "link", returns errSymlink("target"), and is called again with
   336  // the path "target".
   337  //
   338  // If f or openDirFunc return a *PathError, doInRoot will set PathError.Path to the
   339  // full path which caused the error.
   340  func doInRoot[T any](r *Root, name string, flags uint, openDirFunc func(parent sysfdType, name string) (sysfdType, error), f func(parent sysfdType, name string, endsInSlash bool) (T, error)) (ret T, err error) {
   341  	if err := r.root.incref(); err != nil {
   342  		return ret, err
   343  	}
   344  	defer r.root.decref()
   345  
   346  	parts, endsInSlash, err := splitPathInRoot(name, nil, nil)
   347  	if err != nil {
   348  		return ret, err
   349  	}
   350  	if openDirFunc == nil {
   351  		openDirFunc = rootOpenDir
   352  	}
   353  
   354  	rootfd := r.root.fd
   355  	dirfd := rootfd
   356  	defer func() {
   357  		if dirfd != rootfd {
   358  			syscall.Close(dirfd)
   359  		}
   360  	}()
   361  
   362  	// When resolving .. path components, we restart path resolution from the root.
   363  	// (We can't openat(dir, "..") to move up to the parent directory,
   364  	// because dir may have moved since we opened it.)
   365  	// To limit how many opens a malicious path can cause us to perform, we set
   366  	// a limit on the total number of path steps and the total number of restarts
   367  	// caused by .. components. If *both* limits are exceeded, we halt the operation.
   368  	const maxSteps = 255
   369  	const maxRestarts = 8
   370  
   371  	i := 0
   372  	steps := 0
   373  	restarts := 0
   374  	symlinks := 0
   375  Loop:
   376  	for {
   377  		steps++
   378  		if steps > maxSteps && restarts > maxRestarts {
   379  			return ret, syscall.ENAMETOOLONG
   380  		}
   381  
   382  		if parts[i] == ".." {
   383  			// Resolve one or more parent ("..") path components.
   384  			//
   385  			// Rewrite the original path,
   386  			// removing the elements eliminated by ".." components,
   387  			// and start over from the beginning.
   388  			restarts++
   389  			end := i + 1
   390  			for end < len(parts) && parts[end] == ".." {
   391  				end++
   392  			}
   393  			count := end - i
   394  			if count > i {
   395  				return ret, errPathEscapes
   396  			}
   397  			parts = slices.Delete(parts, i-count, end)
   398  			if len(parts) == 0 {
   399  				parts = []string{"."}
   400  			}
   401  			i = 0
   402  			if dirfd != rootfd {
   403  				syscall.Close(dirfd)
   404  			}
   405  			dirfd = rootfd
   406  			continue
   407  		}
   408  
   409  		if i == len(parts)-1 {
   410  			err = nil
   411  			if endsInSlash && flags&doInRootNoHandleTerminalSlash == 0 {
   412  				var fi FileInfo
   413  				fi, err = lstatat(dirfd, parts[i])
   414  				switch {
   415  				case IsNotExist(err) && flags&doInRootCreatingDirectory != 0:
   416  					// The path ends in a slash, the last path component
   417  					// does not exist, and we creating a directory.
   418  					// This is fine.
   419  					err = nil
   420  				case err != nil:
   421  					return
   422  				case fi.Mode().Type() == fs.ModeDir:
   423  				case fi.Mode().Type() == fs.ModeSymlink:
   424  					if runtime.GOOS != "windows" || flags&doInRootAlwaysResolveTerminalSlash != 0 {
   425  						err = checkSymlink(dirfd, parts[i], syscall.ENOTDIR)
   426  					} else {
   427  						if !isDirectoryLink(fi) {
   428  							err = syscall.ENOTDIR
   429  						}
   430  					}
   431  				default:
   432  					err = syscall.ENOTDIR
   433  					return
   434  				}
   435  			}
   436  
   437  			// This is the last path element.
   438  			// Call f to decide what to do with it.
   439  			// If f returns errSymlink, this element is a symlink
   440  			// which should be followed.
   441  			// suffixSep contains any trailing separator characters.
   442  			if err == nil {
   443  				ret, err = f(dirfd, parts[i], endsInSlash)
   444  				if err == nil {
   445  					return
   446  				}
   447  			}
   448  		} else {
   449  			var fd sysfdType
   450  			fd, err = openDirFunc(dirfd, parts[i])
   451  			if err == nil {
   452  				if dirfd != rootfd {
   453  					syscall.Close(dirfd)
   454  				}
   455  				dirfd = fd
   456  			}
   457  		}
   458  
   459  		switch e := err.(type) {
   460  		case nil:
   461  		case errSymlink:
   462  			symlinks++
   463  			if symlinks > rootMaxSymlinks {
   464  				return ret, syscall.ELOOP
   465  			}
   466  			lastPart := i == len(parts)-1
   467  			newparts, newEndsInSlash, err := splitPathInRoot(string(e), parts[:i], parts[i+1:])
   468  			if err != nil {
   469  				return ret, err
   470  			}
   471  			if lastPart && newEndsInSlash {
   472  				// If a link target in the final path component ends in a slash,
   473  				// then the path now ends in a slash.
   474  				endsInSlash = true
   475  			}
   476  			if len(newparts) < i || !slices.Equal(parts[:i], newparts[:i]) {
   477  				// Some component in the path which we have already traversed
   478  				// has changed. We need to restart parsing from the root.
   479  				i = 0
   480  				if dirfd != rootfd {
   481  					syscall.Close(dirfd)
   482  				}
   483  				dirfd = rootfd
   484  			}
   485  			parts = newparts
   486  			continue Loop
   487  		case *PathError:
   488  			// This is strings.Join(parts[:i+1], PathSeparator).
   489  			e.Path = parts[0]
   490  			for _, part := range parts[1 : i+1] {
   491  				e.Path += string(PathSeparator) + part
   492  			}
   493  			return ret, e
   494  		default:
   495  			return ret, err
   496  		}
   497  
   498  		i++
   499  	}
   500  }
   501  
   502  func modeAt(parent sysfdType, name string) (FileMode, error) {
   503  	fi, err := lstatat(parent, name)
   504  	if err != nil {
   505  		return 0, err
   506  	}
   507  	return fi.Mode(), nil
   508  }
   509  
   510  // errSymlink reports that a file being operated on is actually a symlink,
   511  // and the target of that symlink.
   512  type errSymlink string
   513  
   514  func (errSymlink) Error() string { panic("errSymlink is not user-visible") }
   515  

View as plain text