Source file src/os/root.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
     6  
     7  import (
     8  	"errors"
     9  	"internal/bytealg"
    10  	"internal/stringslite"
    11  	"internal/testlog"
    12  	"io/fs"
    13  	"runtime"
    14  	"slices"
    15  	"time"
    16  )
    17  
    18  // OpenInRoot opens the file name in the directory dir.
    19  // It is equivalent to OpenRoot(dir) followed by opening the file in the root.
    20  //
    21  // OpenInRoot returns an error if any component of the name
    22  // references a location outside of dir.
    23  //
    24  // See [Root] for details and limitations.
    25  func OpenInRoot(dir, name string) (*File, error) {
    26  	r, err := OpenRoot(dir)
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  	defer r.Close()
    31  	return r.Open(name)
    32  }
    33  
    34  // Root may be used to only access files within a single directory tree.
    35  //
    36  // Methods on Root can only access files and directories beneath a root directory.
    37  // If any component of a file name passed to a method of Root references a location
    38  // outside the root, the method returns an error.
    39  // File names may reference the directory itself (.).
    40  //
    41  // Methods on Root will follow symbolic links, but symbolic links may not
    42  // reference a location outside the root.
    43  // Symbolic links must not be absolute.
    44  //
    45  // Methods on Root do not prohibit traversal of filesystem boundaries,
    46  // Linux bind mounts, /proc special files, or access to Unix device files.
    47  //
    48  // Methods on Root are safe to be used from multiple goroutines simultaneously.
    49  //
    50  // On most platforms, creating a Root opens a file descriptor or handle referencing
    51  // the directory. If the directory is moved, methods on Root reference the original
    52  // directory in its new location.
    53  //
    54  // Root's behavior differs on some platforms:
    55  //
    56  //   - When GOOS=windows, file names may not reference Windows reserved device names
    57  //     such as NUL and COM1.
    58  //   - On Unix, [Root.Chmod], [Root.Chown], and [Root.Chtimes] are vulnerable to a race condition.
    59  //     If the target of the operation is changed from a regular file to a symlink
    60  //     while the operation is in progress, the operation may be performed on the link
    61  //     rather than the link target.
    62  //   - When GOOS=js, Root is vulnerable to TOCTOU (time-of-check-time-of-use)
    63  //     attacks in symlink validation, and cannot ensure that operations will not
    64  //     escape the root.
    65  //   - When GOOS=plan9 or GOOS=js, Root does not track directories across renames.
    66  //     On these platforms, a Root references a directory name, not a file descriptor.
    67  //   - WASI preview 1 (GOOS=wasip1) does not support [Root.Chmod].
    68  type Root struct {
    69  	root *root
    70  }
    71  
    72  const (
    73  	// Maximum number of symbolic links we will follow when resolving a file in a root.
    74  	// 8 is __POSIX_SYMLOOP_MAX (the minimum allowed value for SYMLOOP_MAX),
    75  	// and a common limit.
    76  	rootMaxSymlinks = 8
    77  )
    78  
    79  // OpenRoot opens the named directory.
    80  // It follows symbolic links in the directory name.
    81  // If there is an error, it will be of type [*PathError].
    82  func OpenRoot(name string) (*Root, error) {
    83  	testlog.Open(name)
    84  	return openRootNolog(name)
    85  }
    86  
    87  // Name returns the name of the directory presented to OpenRoot.
    88  //
    89  // It is safe to call Name after [Close].
    90  func (r *Root) Name() string {
    91  	return r.root.Name()
    92  }
    93  
    94  // Close closes the Root.
    95  // After Close is called, methods on Root return errors.
    96  func (r *Root) Close() error {
    97  	return r.root.Close()
    98  }
    99  
   100  // Open opens the named file in the root for reading.
   101  // See [Open] for more details.
   102  func (r *Root) Open(name string) (*File, error) {
   103  	return r.OpenFile(name, O_RDONLY, 0)
   104  }
   105  
   106  // Create creates or truncates the named file in the root.
   107  // See [Create] for more details.
   108  func (r *Root) Create(name string) (*File, error) {
   109  	return r.OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
   110  }
   111  
   112  // OpenFile opens the named file in the root.
   113  // See [OpenFile] for more details.
   114  //
   115  // If perm contains bits other than the nine least-significant bits (0o777),
   116  // OpenFile returns an error.
   117  func (r *Root) OpenFile(name string, flag int, perm FileMode) (*File, error) {
   118  	if perm&0o777 != perm {
   119  		return nil, &PathError{Op: "openat", Path: name, Err: errors.New("unsupported file mode")}
   120  	}
   121  	r.logOpen(name)
   122  	rf, err := rootOpenFileNolog(r, name, flag, perm)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	rf.appendMode = flag&O_APPEND != 0
   127  	return rf, nil
   128  }
   129  
   130  // OpenRoot opens the named directory in the root.
   131  // If there is an error, it will be of type [*PathError].
   132  func (r *Root) OpenRoot(name string) (*Root, error) {
   133  	r.logOpen(name)
   134  	return openRootInRoot(r, name)
   135  }
   136  
   137  // Chmod changes the mode of the named file in the root to mode.
   138  // See [Chmod] for more details.
   139  func (r *Root) Chmod(name string, mode FileMode) error {
   140  	return rootChmod(r, name, mode)
   141  }
   142  
   143  // Mkdir creates a new directory in the root
   144  // with the specified name and permission bits (before umask).
   145  // See [Mkdir] for more details.
   146  //
   147  // If perm contains bits other than the nine least-significant bits (0o777),
   148  // Mkdir returns an error.
   149  func (r *Root) Mkdir(name string, perm FileMode) error {
   150  	if perm&0o777 != perm {
   151  		return &PathError{Op: "mkdirat", Path: name, Err: errors.New("unsupported file mode")}
   152  	}
   153  	return rootMkdir(r, name, perm)
   154  }
   155  
   156  // MkdirAll creates a new directory in the root, along with any necessary parents.
   157  // See [MkdirAll] for more details.
   158  //
   159  // If perm contains bits other than the nine least-significant bits (0o777),
   160  // MkdirAll returns an error.
   161  func (r *Root) MkdirAll(name string, perm FileMode) error {
   162  	if perm&0o777 != perm {
   163  		return &PathError{Op: "mkdirat", Path: name, Err: errors.New("unsupported file mode")}
   164  	}
   165  	return rootMkdirAll(r, name, perm)
   166  }
   167  
   168  // Chown changes the numeric uid and gid of the named file in the root.
   169  // See [Chown] for more details.
   170  func (r *Root) Chown(name string, uid, gid int) error {
   171  	return rootChown(r, name, uid, gid)
   172  }
   173  
   174  // Lchown changes the numeric uid and gid of the named file in the root.
   175  // See [Lchown] for more details.
   176  func (r *Root) Lchown(name string, uid, gid int) error {
   177  	return rootLchown(r, name, uid, gid)
   178  }
   179  
   180  // Chtimes changes the access and modification times of the named file in the root.
   181  // See [Chtimes] for more details.
   182  func (r *Root) Chtimes(name string, atime time.Time, mtime time.Time) error {
   183  	return rootChtimes(r, name, atime, mtime)
   184  }
   185  
   186  // Remove removes the named file or (empty) directory in the root.
   187  // See [Remove] for more details.
   188  func (r *Root) Remove(name string) error {
   189  	return rootRemove(r, name)
   190  }
   191  
   192  // RemoveAll removes the named file or directory and any children that it contains.
   193  // See [RemoveAll] for more details.
   194  func (r *Root) RemoveAll(name string) error {
   195  	return rootRemoveAll(r, name)
   196  }
   197  
   198  // Stat returns a [FileInfo] describing the named file in the root.
   199  // See [Stat] for more details.
   200  func (r *Root) Stat(name string) (FileInfo, error) {
   201  	r.logStat(name)
   202  	return rootStat(r, name, false)
   203  }
   204  
   205  // Lstat returns a [FileInfo] describing the named file in the root.
   206  // If the file is a symbolic link, the returned FileInfo
   207  // describes the symbolic link.
   208  // See [Lstat] for more details.
   209  func (r *Root) Lstat(name string) (FileInfo, error) {
   210  	r.logStat(name)
   211  	return rootStat(r, name, true)
   212  }
   213  
   214  // Readlink returns the destination of the named symbolic link in the root.
   215  // See [Readlink] for more details.
   216  func (r *Root) Readlink(name string) (string, error) {
   217  	return rootReadlink(r, name)
   218  }
   219  
   220  // Rename renames (moves) oldname to newname.
   221  // Both paths are relative to the root.
   222  // See [Rename] for more details.
   223  func (r *Root) Rename(oldname, newname string) error {
   224  	return rootRename(r, oldname, newname)
   225  }
   226  
   227  // Link creates newname as a hard link to the oldname file.
   228  // Both paths are relative to the root.
   229  // See [Link] for more details.
   230  //
   231  // If oldname is a symbolic link, Link creates new link to oldname and not its target.
   232  // This behavior may differ from that of [Link] on some platforms.
   233  //
   234  // When GOOS=js, Link returns an error if oldname is a symbolic link.
   235  func (r *Root) Link(oldname, newname string) error {
   236  	return rootLink(r, oldname, newname)
   237  }
   238  
   239  // Symlink creates newname as a symbolic link to oldname.
   240  // See [Symlink] for more details.
   241  //
   242  // Symlink does not validate oldname,
   243  // which may reference a location outside the root.
   244  //
   245  // On Windows, a directory link is created if oldname references
   246  // a directory within the root. Otherwise a file link is created.
   247  func (r *Root) Symlink(oldname, newname string) error {
   248  	return rootSymlink(r, oldname, newname)
   249  }
   250  
   251  // ReadFile reads the named file in the root and returns its contents.
   252  // See [ReadFile] for more details.
   253  func (r *Root) ReadFile(name string) ([]byte, error) {
   254  	f, err := r.Open(name)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  	defer f.Close()
   259  	return readFileContents(statOrZero(f), f.Read)
   260  }
   261  
   262  // WriteFile writes data to the named file in the root, creating it if necessary.
   263  // See [WriteFile] for more details.
   264  func (r *Root) WriteFile(name string, data []byte, perm FileMode) error {
   265  	f, err := r.OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	_, err = f.Write(data)
   270  	if err1 := f.Close(); err == nil {
   271  		err = err1
   272  	}
   273  	return err
   274  }
   275  
   276  func (r *Root) logOpen(name string) {
   277  	if log := testlog.Logger(); log != nil {
   278  		// This won't be right if r's name has changed since it was opened,
   279  		// but it's the best we can do.
   280  		log.Open(joinPath(r.Name(), name))
   281  	}
   282  }
   283  
   284  func (r *Root) logStat(name string) {
   285  	if log := testlog.Logger(); log != nil {
   286  		// This won't be right if r's name has changed since it was opened,
   287  		// but it's the best we can do.
   288  		log.Stat(joinPath(r.Name(), name))
   289  	}
   290  }
   291  
   292  // splitPathInRoot splits a path into components
   293  // and joins it with the given prefix and suffix.
   294  //
   295  // The path is relative to a Root, and must not be
   296  // absolute, volume-relative, or "".
   297  //
   298  // "." components are removed, except in the last component.
   299  //
   300  // endsInSlash reports whether the path ends in one or more slashes.
   301  func splitPathInRoot(s string, prefix, suffix []string) (_ []string, endsInSlash bool, err error) {
   302  	if len(s) == 0 {
   303  		return nil, false, errors.New("empty path")
   304  	}
   305  	if IsPathSeparator(s[0]) {
   306  		return nil, false, errPathEscapes
   307  	}
   308  
   309  	if runtime.GOOS == "windows" {
   310  		// Windows cleans paths before opening them.
   311  		s, err = rootCleanPath(s, prefix, suffix)
   312  		if err != nil {
   313  			return nil, false, err
   314  		}
   315  		prefix = nil
   316  		suffix = nil
   317  	}
   318  
   319  	parts := slices.Clone(prefix)
   320  	i, j := 0, 1
   321  	for {
   322  		if j < len(s) && !IsPathSeparator(s[j]) {
   323  			// Keep looking for the end of this component.
   324  			j++
   325  			continue
   326  		}
   327  		parts = append(parts, s[i:j])
   328  		// Advance to the next component, or end of the path.
   329  		partEnd := j
   330  		for j < len(s) && IsPathSeparator(s[j]) {
   331  			j++
   332  		}
   333  		if j == len(s) {
   334  			// If this is the last path component,
   335  			// check for any trailing path separators.
   336  			if len(s[partEnd:]) > 0 {
   337  				endsInSlash = true
   338  			}
   339  			break
   340  		}
   341  		if parts[len(parts)-1] == "." {
   342  			// Remove "." components, except at the end.
   343  			parts = parts[:len(parts)-1]
   344  		}
   345  		i = j
   346  	}
   347  	if len(suffix) > 0 && len(parts) > 0 && parts[len(parts)-1] == "." {
   348  		// Remove a trailing "." component if we're joining to a suffix.
   349  		parts = parts[:len(parts)-1]
   350  	}
   351  	parts = append(parts, suffix...)
   352  	return parts, endsInSlash, nil
   353  }
   354  
   355  // FS returns a file system (an fs.FS) for the tree of files in the root.
   356  //
   357  // The result implements [io/fs.StatFS], [io/fs.ReadFileFS],
   358  // [io/fs.ReadDirFS], and [io/fs.ReadLinkFS].
   359  func (r *Root) FS() fs.FS {
   360  	return (*rootFS)(r)
   361  }
   362  
   363  type rootFS Root
   364  
   365  func (rfs *rootFS) Open(name string) (fs.File, error) {
   366  	r := (*Root)(rfs)
   367  	if !isValidRootFSPath(name) {
   368  		return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
   369  	}
   370  	f, err := r.Open(name)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  	return f, nil
   375  }
   376  
   377  func (rfs *rootFS) ReadDir(name string) ([]DirEntry, error) {
   378  	r := (*Root)(rfs)
   379  	if !isValidRootFSPath(name) {
   380  		return nil, &PathError{Op: "readdir", Path: name, Err: ErrInvalid}
   381  	}
   382  
   383  	// This isn't efficient: We just open a regular file and ReadDir it.
   384  	// Ideally, we would skip creating a *File entirely and operate directly
   385  	// on the file descriptor, but that will require some extensive reworking
   386  	// of directory reading in general.
   387  	//
   388  	// This suffices for the moment.
   389  	f, err := r.Open(name)
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	defer f.Close()
   394  	dirs, err := f.ReadDir(-1)
   395  	slices.SortFunc(dirs, func(a, b DirEntry) int {
   396  		return bytealg.CompareString(a.Name(), b.Name())
   397  	})
   398  	return dirs, err
   399  }
   400  
   401  func (rfs *rootFS) ReadFile(name string) ([]byte, error) {
   402  	r := (*Root)(rfs)
   403  	if !isValidRootFSPath(name) {
   404  		return nil, &PathError{Op: "readfile", Path: name, Err: ErrInvalid}
   405  	}
   406  	f, err := r.Open(name)
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  	defer f.Close()
   411  	return readFileContents(statOrZero(f), f.Read)
   412  }
   413  
   414  func (rfs *rootFS) ReadLink(name string) (string, error) {
   415  	r := (*Root)(rfs)
   416  	if !isValidRootFSPath(name) {
   417  		return "", &PathError{Op: "readlink", Path: name, Err: ErrInvalid}
   418  	}
   419  	return r.Readlink(name)
   420  }
   421  
   422  func (rfs *rootFS) Stat(name string) (FileInfo, error) {
   423  	r := (*Root)(rfs)
   424  	if !isValidRootFSPath(name) {
   425  		return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
   426  	}
   427  	return r.Stat(name)
   428  }
   429  
   430  func (rfs *rootFS) Lstat(name string) (FileInfo, error) {
   431  	r := (*Root)(rfs)
   432  	if !isValidRootFSPath(name) {
   433  		return nil, &PathError{Op: "lstat", Path: name, Err: ErrInvalid}
   434  	}
   435  	return r.Lstat(name)
   436  }
   437  
   438  // isValidRootFSPath reports whether name is a valid filename to pass a Root.FS method.
   439  func isValidRootFSPath(name string) bool {
   440  	if !fs.ValidPath(name) {
   441  		return false
   442  	}
   443  	if runtime.GOOS == "windows" {
   444  		// fs.FS paths are /-separated.
   445  		// On Windows, reject the path if it contains any \ separators.
   446  		// Other forms of invalid path (for example, "NUL") are handled by
   447  		// Root's usual file lookup mechanisms.
   448  		if stringslite.IndexByte(name, '\\') >= 0 {
   449  			return false
   450  		}
   451  	}
   452  	return true
   453  }
   454  

View as plain text