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  // Path separators following the last component are returned in suffixSep.
   301  func splitPathInRoot(s string, prefix, suffix []string) (_ []string, suffixSep string, err error) {
   302  	if len(s) == 0 {
   303  		return nil, "", errors.New("empty path")
   304  	}
   305  	if IsPathSeparator(s[0]) {
   306  		return nil, "", 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, "", 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  			// preserve any trailing path separators.
   336  			suffixSep = s[partEnd:]
   337  			break
   338  		}
   339  		if parts[len(parts)-1] == "." {
   340  			// Remove "." components, except at the end.
   341  			parts = parts[:len(parts)-1]
   342  		}
   343  		i = j
   344  	}
   345  	if len(suffix) > 0 && len(parts) > 0 && parts[len(parts)-1] == "." {
   346  		// Remove a trailing "." component if we're joining to a suffix.
   347  		parts = parts[:len(parts)-1]
   348  	}
   349  	parts = append(parts, suffix...)
   350  	return parts, suffixSep, nil
   351  }
   352  
   353  // FS returns a file system (an fs.FS) for the tree of files in the root.
   354  //
   355  // The result implements [io/fs.StatFS], [io/fs.ReadFileFS],
   356  // [io/fs.ReadDirFS], and [io/fs.ReadLinkFS].
   357  func (r *Root) FS() fs.FS {
   358  	return (*rootFS)(r)
   359  }
   360  
   361  type rootFS Root
   362  
   363  func (rfs *rootFS) Open(name string) (fs.File, error) {
   364  	r := (*Root)(rfs)
   365  	if !isValidRootFSPath(name) {
   366  		return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
   367  	}
   368  	f, err := r.Open(name)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  	return f, nil
   373  }
   374  
   375  func (rfs *rootFS) ReadDir(name string) ([]DirEntry, error) {
   376  	r := (*Root)(rfs)
   377  	if !isValidRootFSPath(name) {
   378  		return nil, &PathError{Op: "readdir", Path: name, Err: ErrInvalid}
   379  	}
   380  
   381  	// This isn't efficient: We just open a regular file and ReadDir it.
   382  	// Ideally, we would skip creating a *File entirely and operate directly
   383  	// on the file descriptor, but that will require some extensive reworking
   384  	// of directory reading in general.
   385  	//
   386  	// This suffices for the moment.
   387  	f, err := r.Open(name)
   388  	if err != nil {
   389  		return nil, err
   390  	}
   391  	defer f.Close()
   392  	dirs, err := f.ReadDir(-1)
   393  	slices.SortFunc(dirs, func(a, b DirEntry) int {
   394  		return bytealg.CompareString(a.Name(), b.Name())
   395  	})
   396  	return dirs, err
   397  }
   398  
   399  func (rfs *rootFS) ReadFile(name string) ([]byte, error) {
   400  	r := (*Root)(rfs)
   401  	if !isValidRootFSPath(name) {
   402  		return nil, &PathError{Op: "readfile", Path: name, Err: ErrInvalid}
   403  	}
   404  	f, err := r.Open(name)
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  	defer f.Close()
   409  	return readFileContents(statOrZero(f), f.Read)
   410  }
   411  
   412  func (rfs *rootFS) ReadLink(name string) (string, error) {
   413  	r := (*Root)(rfs)
   414  	if !isValidRootFSPath(name) {
   415  		return "", &PathError{Op: "readlink", Path: name, Err: ErrInvalid}
   416  	}
   417  	return r.Readlink(name)
   418  }
   419  
   420  func (rfs *rootFS) Stat(name string) (FileInfo, error) {
   421  	r := (*Root)(rfs)
   422  	if !isValidRootFSPath(name) {
   423  		return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
   424  	}
   425  	return r.Stat(name)
   426  }
   427  
   428  func (rfs *rootFS) Lstat(name string) (FileInfo, error) {
   429  	r := (*Root)(rfs)
   430  	if !isValidRootFSPath(name) {
   431  		return nil, &PathError{Op: "lstat", Path: name, Err: ErrInvalid}
   432  	}
   433  	return r.Lstat(name)
   434  }
   435  
   436  // isValidRootFSPath reports whether name is a valid filename to pass a Root.FS method.
   437  func isValidRootFSPath(name string) bool {
   438  	if !fs.ValidPath(name) {
   439  		return false
   440  	}
   441  	if runtime.GOOS == "windows" {
   442  		// fs.FS paths are /-separated.
   443  		// On Windows, reject the path if it contains any \ separators.
   444  		// Other forms of invalid path (for example, "NUL") are handled by
   445  		// Root's usual file lookup mechanisms.
   446  		if stringslite.IndexByte(name, '\\') >= 0 {
   447  			return false
   448  		}
   449  	}
   450  	return true
   451  }
   452  

View as plain text