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  )
    16  
    17  // OpenInRoot opens the file name in the directory dir.
    18  // It is equivalent to OpenRoot(dir) followed by opening the file in the root.
    19  //
    20  // OpenInRoot returns an error if any component of the name
    21  // references a location outside of dir.
    22  //
    23  // See [Root] for details and limitations.
    24  func OpenInRoot(dir, name string) (*File, error) {
    25  	r, err := OpenRoot(dir)
    26  	if err != nil {
    27  		return nil, err
    28  	}
    29  	defer r.Close()
    30  	return r.Open(name)
    31  }
    32  
    33  // Root may be used to only access files within a single directory tree.
    34  //
    35  // Methods on Root can only access files and directories beneath a root directory.
    36  // If any component of a file name passed to a method of Root references a location
    37  // outside the root, the method returns an error.
    38  // File names may reference the directory itself (.).
    39  //
    40  // Methods on Root will follow symbolic links, but symbolic links may not
    41  // reference a location outside the root.
    42  // Symbolic links must not be absolute.
    43  //
    44  // Methods on Root do not prohibit traversal of filesystem boundaries,
    45  // Linux bind mounts, /proc special files, or access to Unix device files.
    46  //
    47  // Methods on Root are safe to be used from multiple goroutines simultaneously.
    48  //
    49  // On most platforms, creating a Root opens a file descriptor or handle referencing
    50  // the directory. If the directory is moved, methods on Root reference the original
    51  // directory in its new location.
    52  //
    53  // Root's behavior differs on some platforms:
    54  //
    55  //   - When GOOS=windows, file names may not reference Windows reserved device names
    56  //     such as NUL and COM1.
    57  //   - When GOOS=js, Root is vulnerable to TOCTOU (time-of-check-time-of-use)
    58  //     attacks in symlink validation, and cannot ensure that operations will not
    59  //     escape the root.
    60  //   - When GOOS=plan9 or GOOS=js, Root does not track directories across renames.
    61  //     On these platforms, a Root references a directory name, not a file descriptor.
    62  type Root struct {
    63  	root root
    64  }
    65  
    66  const (
    67  	// Maximum number of symbolic links we will follow when resolving a file in a root.
    68  	// 8 is __POSIX_SYMLOOP_MAX (the minimum allowed value for SYMLOOP_MAX),
    69  	// and a common limit.
    70  	rootMaxSymlinks = 8
    71  )
    72  
    73  // OpenRoot opens the named directory.
    74  // If there is an error, it will be of type *PathError.
    75  func OpenRoot(name string) (*Root, error) {
    76  	testlog.Open(name)
    77  	return openRootNolog(name)
    78  }
    79  
    80  // Name returns the name of the directory presented to OpenRoot.
    81  //
    82  // It is safe to call Name after [Close].
    83  func (r *Root) Name() string {
    84  	return r.root.Name()
    85  }
    86  
    87  // Close closes the Root.
    88  // After Close is called, methods on Root return errors.
    89  func (r *Root) Close() error {
    90  	return r.root.Close()
    91  }
    92  
    93  // Open opens the named file in the root for reading.
    94  // See [Open] for more details.
    95  func (r *Root) Open(name string) (*File, error) {
    96  	return r.OpenFile(name, O_RDONLY, 0)
    97  }
    98  
    99  // Create creates or truncates the named file in the root.
   100  // See [Create] for more details.
   101  func (r *Root) Create(name string) (*File, error) {
   102  	return r.OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
   103  }
   104  
   105  // OpenFile opens the named file in the root.
   106  // See [OpenFile] for more details.
   107  //
   108  // If perm contains bits other than the nine least-significant bits (0o777),
   109  // OpenFile returns an error.
   110  func (r *Root) OpenFile(name string, flag int, perm FileMode) (*File, error) {
   111  	if perm&0o777 != perm {
   112  		return nil, &PathError{Op: "openat", Path: name, Err: errors.New("unsupported file mode")}
   113  	}
   114  	r.logOpen(name)
   115  	rf, err := rootOpenFileNolog(r, name, flag, perm)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	rf.appendMode = flag&O_APPEND != 0
   120  	return rf, nil
   121  }
   122  
   123  // OpenRoot opens the named directory in the root.
   124  // If there is an error, it will be of type *PathError.
   125  func (r *Root) OpenRoot(name string) (*Root, error) {
   126  	r.logOpen(name)
   127  	return openRootInRoot(r, name)
   128  }
   129  
   130  // Mkdir creates a new directory in the root
   131  // with the specified name and permission bits (before umask).
   132  // See [Mkdir] for more details.
   133  //
   134  // If perm contains bits other than the nine least-significant bits (0o777),
   135  // OpenFile returns an error.
   136  func (r *Root) Mkdir(name string, perm FileMode) error {
   137  	if perm&0o777 != perm {
   138  		return &PathError{Op: "mkdirat", Path: name, Err: errors.New("unsupported file mode")}
   139  	}
   140  	return rootMkdir(r, name, perm)
   141  }
   142  
   143  // Remove removes the named file or (empty) directory in the root.
   144  // See [Remove] for more details.
   145  func (r *Root) Remove(name string) error {
   146  	return rootRemove(r, name)
   147  }
   148  
   149  // Stat returns a [FileInfo] describing the named file in the root.
   150  // See [Stat] for more details.
   151  func (r *Root) Stat(name string) (FileInfo, error) {
   152  	r.logStat(name)
   153  	return rootStat(r, name, false)
   154  }
   155  
   156  // Lstat returns a [FileInfo] describing the named file in the root.
   157  // If the file is a symbolic link, the returned FileInfo
   158  // describes the symbolic link.
   159  // See [Lstat] for more details.
   160  func (r *Root) Lstat(name string) (FileInfo, error) {
   161  	r.logStat(name)
   162  	return rootStat(r, name, true)
   163  }
   164  
   165  func (r *Root) logOpen(name string) {
   166  	if log := testlog.Logger(); log != nil {
   167  		// This won't be right if r's name has changed since it was opened,
   168  		// but it's the best we can do.
   169  		log.Open(joinPath(r.Name(), name))
   170  	}
   171  }
   172  
   173  func (r *Root) logStat(name string) {
   174  	if log := testlog.Logger(); log != nil {
   175  		// This won't be right if r's name has changed since it was opened,
   176  		// but it's the best we can do.
   177  		log.Stat(joinPath(r.Name(), name))
   178  	}
   179  }
   180  
   181  // splitPathInRoot splits a path into components
   182  // and joins it with the given prefix and suffix.
   183  //
   184  // The path is relative to a Root, and must not be
   185  // absolute, volume-relative, or "".
   186  //
   187  // "." components are removed, except in the last component.
   188  //
   189  // Path separators following the last component are preserved.
   190  func splitPathInRoot(s string, prefix, suffix []string) (_ []string, err error) {
   191  	if len(s) == 0 {
   192  		return nil, errors.New("empty path")
   193  	}
   194  	if IsPathSeparator(s[0]) {
   195  		return nil, errPathEscapes
   196  	}
   197  
   198  	if runtime.GOOS == "windows" {
   199  		// Windows cleans paths before opening them.
   200  		s, err = rootCleanPath(s, prefix, suffix)
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  		prefix = nil
   205  		suffix = nil
   206  	}
   207  
   208  	parts := append([]string{}, prefix...)
   209  	i, j := 0, 1
   210  	for {
   211  		if j < len(s) && !IsPathSeparator(s[j]) {
   212  			// Keep looking for the end of this component.
   213  			j++
   214  			continue
   215  		}
   216  		parts = append(parts, s[i:j])
   217  		// Advance to the next component, or end of the path.
   218  		for j < len(s) && IsPathSeparator(s[j]) {
   219  			j++
   220  		}
   221  		if j == len(s) {
   222  			// If this is the last path component,
   223  			// preserve any trailing path separators.
   224  			parts[len(parts)-1] = s[i:]
   225  			break
   226  		}
   227  		if parts[len(parts)-1] == "." {
   228  			// Remove "." components, except at the end.
   229  			parts = parts[:len(parts)-1]
   230  		}
   231  		i = j
   232  	}
   233  	if len(suffix) > 0 && len(parts) > 0 && parts[len(parts)-1] == "." {
   234  		// Remove a trailing "." component if we're joining to a suffix.
   235  		parts = parts[:len(parts)-1]
   236  	}
   237  	parts = append(parts, suffix...)
   238  	return parts, nil
   239  }
   240  
   241  // FS returns a file system (an fs.FS) for the tree of files in the root.
   242  //
   243  // The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and
   244  // [io/fs.ReadDirFS].
   245  func (r *Root) FS() fs.FS {
   246  	return (*rootFS)(r)
   247  }
   248  
   249  type rootFS Root
   250  
   251  func (rfs *rootFS) Open(name string) (fs.File, error) {
   252  	r := (*Root)(rfs)
   253  	if !isValidRootFSPath(name) {
   254  		return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
   255  	}
   256  	f, err := r.Open(name)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	return f, nil
   261  }
   262  
   263  func (rfs *rootFS) ReadDir(name string) ([]DirEntry, error) {
   264  	r := (*Root)(rfs)
   265  	if !isValidRootFSPath(name) {
   266  		return nil, &PathError{Op: "readdir", Path: name, Err: ErrInvalid}
   267  	}
   268  
   269  	// This isn't efficient: We just open a regular file and ReadDir it.
   270  	// Ideally, we would skip creating a *File entirely and operate directly
   271  	// on the file descriptor, but that will require some extensive reworking
   272  	// of directory reading in general.
   273  	//
   274  	// This suffices for the moment.
   275  	f, err := r.Open(name)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	defer f.Close()
   280  	dirs, err := f.ReadDir(-1)
   281  	slices.SortFunc(dirs, func(a, b DirEntry) int {
   282  		return bytealg.CompareString(a.Name(), b.Name())
   283  	})
   284  	return dirs, err
   285  }
   286  
   287  func (rfs *rootFS) ReadFile(name string) ([]byte, error) {
   288  	r := (*Root)(rfs)
   289  	if !isValidRootFSPath(name) {
   290  		return nil, &PathError{Op: "readfile", Path: name, Err: ErrInvalid}
   291  	}
   292  	f, err := r.Open(name)
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  	defer f.Close()
   297  	return readFileContents(f)
   298  }
   299  
   300  func (rfs *rootFS) Stat(name string) (FileInfo, error) {
   301  	r := (*Root)(rfs)
   302  	if !isValidRootFSPath(name) {
   303  		return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
   304  	}
   305  	return r.Stat(name)
   306  }
   307  
   308  // isValidRootFSPath reprots whether name is a valid filename to pass a Root.FS method.
   309  func isValidRootFSPath(name string) bool {
   310  	if !fs.ValidPath(name) {
   311  		return false
   312  	}
   313  	if runtime.GOOS == "windows" {
   314  		// fs.FS paths are /-separated.
   315  		// On Windows, reject the path if it contains any \ separators.
   316  		// Other forms of invalid path (for example, "NUL") are handled by
   317  		// Root's usual file lookup mechanisms.
   318  		if stringslite.IndexByte(name, '\\') >= 0 {
   319  			return false
   320  		}
   321  	}
   322  	return true
   323  }
   324  

View as plain text