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 // OpenFile 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 // Chown changes the numeric uid and gid of the named file in the root. 157 // See [Chown] for more details. 158 func (r *Root) Chown(name string, uid, gid int) error { 159 return rootChown(r, name, uid, gid) 160 } 161 162 // Lchown changes the numeric uid and gid of the named file in the root. 163 // See [Lchown] for more details. 164 func (r *Root) Lchown(name string, uid, gid int) error { 165 return rootLchown(r, name, uid, gid) 166 } 167 168 // Chtimes changes the access and modification times of the named file in the root. 169 // See [Chtimes] for more details. 170 func (r *Root) Chtimes(name string, atime time.Time, mtime time.Time) error { 171 return rootChtimes(r, name, atime, mtime) 172 } 173 174 // Remove removes the named file or (empty) directory in the root. 175 // See [Remove] for more details. 176 func (r *Root) Remove(name string) error { 177 return rootRemove(r, name) 178 } 179 180 // Stat returns a [FileInfo] describing the named file in the root. 181 // See [Stat] for more details. 182 func (r *Root) Stat(name string) (FileInfo, error) { 183 r.logStat(name) 184 return rootStat(r, name, false) 185 } 186 187 // Lstat returns a [FileInfo] describing the named file in the root. 188 // If the file is a symbolic link, the returned FileInfo 189 // describes the symbolic link. 190 // See [Lstat] for more details. 191 func (r *Root) Lstat(name string) (FileInfo, error) { 192 r.logStat(name) 193 return rootStat(r, name, true) 194 } 195 196 // Readlink returns the destination of the named symbolic link in the root. 197 // See [Readlink] for more details. 198 func (r *Root) Readlink(name string) (string, error) { 199 return rootReadlink(r, name) 200 } 201 202 // Rename renames (moves) oldname to newname. 203 // Both paths are relative to the root. 204 // See [Rename] for more details. 205 func (r *Root) Rename(oldname, newname string) error { 206 return rootRename(r, oldname, newname) 207 } 208 209 // Link creates newname as a hard link to the oldname file. 210 // Both paths are relative to the root. 211 // See [Link] for more details. 212 // 213 // If oldname is a symbolic link, Link creates new link to oldname and not its target. 214 // This behavior may differ from that of [Link] on some platforms. 215 // 216 // When GOOS=js, Link returns an error if oldname is a symbolic link. 217 func (r *Root) Link(oldname, newname string) error { 218 return rootLink(r, oldname, newname) 219 } 220 221 // Symlink creates newname as a symbolic link to oldname. 222 // See [Symlink] for more details. 223 // 224 // Symlink does not validate oldname, 225 // which may reference a location outside the root. 226 // 227 // On Windows, a directory link is created if oldname references 228 // a directory within the root. Otherwise a file link is created. 229 func (r *Root) Symlink(oldname, newname string) error { 230 return rootSymlink(r, oldname, newname) 231 } 232 233 func (r *Root) logOpen(name string) { 234 if log := testlog.Logger(); log != nil { 235 // This won't be right if r's name has changed since it was opened, 236 // but it's the best we can do. 237 log.Open(joinPath(r.Name(), name)) 238 } 239 } 240 241 func (r *Root) logStat(name string) { 242 if log := testlog.Logger(); log != nil { 243 // This won't be right if r's name has changed since it was opened, 244 // but it's the best we can do. 245 log.Stat(joinPath(r.Name(), name)) 246 } 247 } 248 249 // splitPathInRoot splits a path into components 250 // and joins it with the given prefix and suffix. 251 // 252 // The path is relative to a Root, and must not be 253 // absolute, volume-relative, or "". 254 // 255 // "." components are removed, except in the last component. 256 // 257 // Path separators following the last component are preserved. 258 func splitPathInRoot(s string, prefix, suffix []string) (_ []string, err error) { 259 if len(s) == 0 { 260 return nil, errors.New("empty path") 261 } 262 if IsPathSeparator(s[0]) { 263 return nil, errPathEscapes 264 } 265 266 if runtime.GOOS == "windows" { 267 // Windows cleans paths before opening them. 268 s, err = rootCleanPath(s, prefix, suffix) 269 if err != nil { 270 return nil, err 271 } 272 prefix = nil 273 suffix = nil 274 } 275 276 parts := slices.Clone(prefix) 277 i, j := 0, 1 278 for { 279 if j < len(s) && !IsPathSeparator(s[j]) { 280 // Keep looking for the end of this component. 281 j++ 282 continue 283 } 284 parts = append(parts, s[i:j]) 285 // Advance to the next component, or end of the path. 286 for j < len(s) && IsPathSeparator(s[j]) { 287 j++ 288 } 289 if j == len(s) { 290 // If this is the last path component, 291 // preserve any trailing path separators. 292 parts[len(parts)-1] = s[i:] 293 break 294 } 295 if parts[len(parts)-1] == "." { 296 // Remove "." components, except at the end. 297 parts = parts[:len(parts)-1] 298 } 299 i = j 300 } 301 if len(suffix) > 0 && len(parts) > 0 && parts[len(parts)-1] == "." { 302 // Remove a trailing "." component if we're joining to a suffix. 303 parts = parts[:len(parts)-1] 304 } 305 parts = append(parts, suffix...) 306 return parts, nil 307 } 308 309 // FS returns a file system (an fs.FS) for the tree of files in the root. 310 // 311 // The result implements [io/fs.StatFS], [io/fs.ReadFileFS] and 312 // [io/fs.ReadDirFS]. 313 func (r *Root) FS() fs.FS { 314 return (*rootFS)(r) 315 } 316 317 type rootFS Root 318 319 func (rfs *rootFS) Open(name string) (fs.File, error) { 320 r := (*Root)(rfs) 321 if !isValidRootFSPath(name) { 322 return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid} 323 } 324 f, err := r.Open(name) 325 if err != nil { 326 return nil, err 327 } 328 return f, nil 329 } 330 331 func (rfs *rootFS) ReadDir(name string) ([]DirEntry, error) { 332 r := (*Root)(rfs) 333 if !isValidRootFSPath(name) { 334 return nil, &PathError{Op: "readdir", Path: name, Err: ErrInvalid} 335 } 336 337 // This isn't efficient: We just open a regular file and ReadDir it. 338 // Ideally, we would skip creating a *File entirely and operate directly 339 // on the file descriptor, but that will require some extensive reworking 340 // of directory reading in general. 341 // 342 // This suffices for the moment. 343 f, err := r.Open(name) 344 if err != nil { 345 return nil, err 346 } 347 defer f.Close() 348 dirs, err := f.ReadDir(-1) 349 slices.SortFunc(dirs, func(a, b DirEntry) int { 350 return bytealg.CompareString(a.Name(), b.Name()) 351 }) 352 return dirs, err 353 } 354 355 func (rfs *rootFS) ReadFile(name string) ([]byte, error) { 356 r := (*Root)(rfs) 357 if !isValidRootFSPath(name) { 358 return nil, &PathError{Op: "readfile", Path: name, Err: ErrInvalid} 359 } 360 f, err := r.Open(name) 361 if err != nil { 362 return nil, err 363 } 364 defer f.Close() 365 return readFileContents(statOrZero(f), f.Read) 366 } 367 368 func (rfs *rootFS) Stat(name string) (FileInfo, error) { 369 r := (*Root)(rfs) 370 if !isValidRootFSPath(name) { 371 return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid} 372 } 373 return r.Stat(name) 374 } 375 376 // isValidRootFSPath reports whether name is a valid filename to pass a Root.FS method. 377 func isValidRootFSPath(name string) bool { 378 if !fs.ValidPath(name) { 379 return false 380 } 381 if runtime.GOOS == "windows" { 382 // fs.FS paths are /-separated. 383 // On Windows, reject the path if it contains any \ separators. 384 // Other forms of invalid path (for example, "NUL") are handled by 385 // Root's usual file lookup mechanisms. 386 if stringslite.IndexByte(name, '\\') >= 0 { 387 return false 388 } 389 } 390 return true 391 } 392