| // Copyright 2024 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| //go:build unix || windows || wasip1 | |
| package os | |
| import ( | |
| "io/fs" | |
| "runtime" | |
| "slices" | |
| "sync" | |
| "syscall" | |
| "time" | |
| ) | |
| // root implementation for platforms with a function to open a file | |
| // relative to a directory. | |
| type root struct { | |
| name string | |
| // refs is incremented while an operation is using fd. | |
| // closed is set when Close is called. | |
| // fd is closed when closed is true and refs is 0. | |
| mu sync.Mutex | |
| fd sysfdType | |
| refs int // number of active operations | |
| closed bool // set when closed | |
| } | |
| func (r *root) Close() error { | |
| r.mu.Lock() | |
| defer r.mu.Unlock() | |
| if !r.closed && r.refs == 0 { | |
| syscall.Close(r.fd) | |
| } | |
| r.closed = true | |
| runtime.SetFinalizer(r, nil) // no need for a finalizer any more | |
| return nil | |
| } | |
| func (r *root) incref() error { | |
| r.mu.Lock() | |
| defer r.mu.Unlock() | |
| if r.closed { | |
| return ErrClosed | |
| } | |
| r.refs++ | |
| return nil | |
| } | |
| func (r *root) decref() { | |
| r.mu.Lock() | |
| defer r.mu.Unlock() | |
| if r.refs <= 0 { | |
| panic("bad Root refcount") | |
| } | |
| r.refs-- | |
| if r.closed && r.refs == 0 { | |
| syscall.Close(r.fd) | |
| } | |
| } | |
| func (r *root) Name() string { | |
| return r.name | |
| } | |
| func rootChmod(r *Root, name string, mode FileMode) error { | |
| _, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| return struct{}{}, chmodat(parent, name, mode) | |
| }) | |
| if err != nil { | |
| return &PathError{Op: "chmodat", Path: name, Err: err} | |
| } | |
| return nil | |
| } | |
| func rootChown(r *Root, name string, uid, gid int) error { | |
| _, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| return struct{}{}, chownat(parent, name, uid, gid) | |
| }) | |
| if err != nil { | |
| return &PathError{Op: "chownat", Path: name, Err: err} | |
| } | |
| return nil | |
| } | |
| func rootLchown(r *Root, name string, uid, gid int) error { | |
| _, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| return struct{}{}, lchownat(parent, name, uid, gid) | |
| }) | |
| if err != nil { | |
| return &PathError{Op: "lchownat", Path: name, Err: err} | |
| } | |
| return nil | |
| } | |
| func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error { | |
| _, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| return struct{}{}, chtimesat(parent, name, atime, mtime) | |
| }) | |
| if err != nil { | |
| return &PathError{Op: "chtimesat", Path: name, Err: err} | |
| } | |
| return nil | |
| } | |
| func rootMkdir(r *Root, name string, perm FileMode) error { | |
| flags := uint(doInRootCreatingDirectory) | |
| switch runtime.GOOS { | |
| case "linux", "windows", "openbsd": | |
| // These platforms do not follow "symlink" on "mkdir symlink/". | |
| // (POSIX.1-2024 4.16 says that the trailing slash should cause | |
| // resolution to follow the symlink, but we're trying to match | |
| // platform semantics, not implement POSIX.) | |
| flags |= doInRootNoHandleTerminalSlash | |
| } | |
| _, err := doInRoot(r, name, flags, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| return struct{}{}, mkdirat(parent, name, perm) | |
| }) | |
| if err != nil { | |
| return &PathError{Op: "mkdirat", Path: name, Err: err} | |
| } | |
| return nil | |
| } | |
| func rootMkdirAll(r *Root, fullname string, perm FileMode) error { | |
| // doInRoot opens each path element in turn. | |
| // | |
| // openDirFunc opens all but the last path component. | |
| // The usual default openDirFunc just opens directories with O_DIRECTORY. | |
| // We replace it here with one that creates missing directories along the way. | |
| openDirFunc := func(parent sysfdType, name string) (sysfdType, error) { | |
| for try := range 2 { | |
| fd, err := rootOpenDir(parent, name) | |
| switch err.(type) { | |
| case nil, errSymlink: | |
| return fd, err | |
| } | |
| if try > 0 || !IsNotExist(err) { | |
| return 0, &PathError{Op: "openat", Err: err} | |
| } | |
| // Try again on EEXIST, because the directory may have been created | |
| // by another process or thread between the rootOpenDir and mkdirat calls. | |
| if err := mkdirat(parent, name, perm); err != nil && err != syscall.EEXIST { | |
| return 0, &PathError{Op: "mkdirat", Err: err} | |
| } | |
| } | |
| panic("unreachable") | |
| } | |
| // openLastComponentFunc opens the last path component. | |
| openLastComponentFunc := func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| err := mkdirat(parent, name, perm) | |
| if err == syscall.EEXIST { | |
| mode, e := modeAt(parent, name) | |
| if e == nil { | |
| if mode.IsDir() { | |
| // The target of MkdirAll is an existing directory. | |
| err = nil | |
| } else if mode&ModeSymlink != 0 { | |
| // The target of MkdirAll is a symlink. | |
| // For consistency with os.MkdirAll, | |
| // succeed if the link resolves to a directory. | |
| // We don't return errSymlink here, because we don't | |
| // want to create the link target if it doesn't exist. | |
| fi, e := r.Stat(fullname) | |
| if e == nil && fi.Mode().IsDir() { | |
| err = nil | |
| } else if e == nil { | |
| err = syscall.ENOTDIR | |
| } else if !IsNotExist(e) { | |
| // EPERM, ELOOP, etc., | |
| // probably more useful than EEXIST. | |
| err = e | |
| } | |
| } | |
| } | |
| } | |
| switch err.(type) { | |
| case nil, errSymlink: | |
| return struct{}{}, err | |
| } | |
| return struct{}{}, &PathError{Op: "mkdirat", Err: err} | |
| } | |
| flags := uint(doInRootCreatingDirectory) | |
| switch runtime.GOOS { | |
| case "linux", "windows", "openbsd": | |
| flags |= doInRootNoHandleTerminalSlash // see rootMkdir | |
| } | |
| _, err := doInRoot(r, fullname, flags, openDirFunc, openLastComponentFunc) | |
| if err != nil { | |
| if _, ok := err.(*PathError); !ok { | |
| err = &PathError{Op: "mkdirat", Path: fullname, Err: err} | |
| } | |
| } | |
| return err | |
| } | |
| func rootReadlink(r *Root, name string) (string, error) { | |
| target, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (string, error) { | |
| return readlinkat(parent, name) | |
| }) | |
| if err != nil { | |
| return "", &PathError{Op: "readlinkat", Path: name, Err: err} | |
| } | |
| return target, nil | |
| } | |
| func rootRemove(r *Root, name string) error { | |
| _, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| return struct{}{}, removeat(parent, name) | |
| }) | |
| if err != nil { | |
| return &PathError{Op: "removeat", Path: name, Err: err} | |
| } | |
| return nil | |
| } | |
| func rootRemoveAll(r *Root, name string) error { | |
| // Consistency with os.RemoveAll: Strip trailing /s from the name, | |
| // so RemoveAll("not_a_directory/") succeeds. | |
| for len(name) > 0 && IsPathSeparator(name[len(name)-1]) { | |
| name = name[:len(name)-1] | |
| } | |
| if endsWithDot(name) { | |
| // Consistency with os.RemoveAll: Return EINVAL when trying to remove . | |
| return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL} | |
| } | |
| _, err := doInRoot(r, name, 0, nil, func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { | |
| return struct{}{}, removeAllFrom(parent, name) | |
| }) | |
| if IsNotExist(err) { | |
| return nil | |
| } | |
| if err != nil { | |
| return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)} | |
| } | |
| return err | |
| } | |
| func rootRename(r *Root, oldname, newname string) error { | |
| _, err := doInRoot(r, oldname, 0, nil, func(oldparent sysfdType, oldname string, oldEndsInSlash bool) (struct{}, error) { | |
| flags := uint(doInRootCreatingDirectory) | |
| if runtime.GOOS == "windows" { | |
| flags |= doInRootNoHandleTerminalSlash | |
| } | |
| _, err := doInRoot(r, newname, flags, nil, func(newparent sysfdType, newname string, newEndsInSlash bool) (struct{}, error) { | |
| if runtime.GOOS != "windows" && newEndsInSlash { | |
| oldMode, err := modeAt(oldparent, oldname) | |
| if err != nil { | |
| return struct{}{}, err | |
| } | |
| if oldMode.Type() != fs.ModeDir { | |
| return struct{}{}, syscall.ENOTDIR | |
| } | |
| } | |
| // Same checks as applied by rename (in file_unix.go): | |
| fi, err := lstatat(newparent, newname) | |
| if err == nil && fi.IsDir() { | |
| if ofi, err := lstatat(oldparent, oldname); err != nil { | |
| return struct{}{}, err | |
| } else if newname == oldname || !SameFile(fi, ofi) { | |
| return struct{}{}, syscall.EEXIST | |
| } | |
| } | |
| return struct{}{}, renameat(oldparent, oldname, newparent, newname) | |
| }) | |
| return struct{}{}, err | |
| }) | |
| if err != nil { | |
| return &LinkError{"renameat", oldname, newname, err} | |
| } | |
| return err | |
| } | |
| func rootLink(r *Root, oldname, newname string) error { | |
| _, err := doInRoot(r, oldname, 0, nil, func(oldparent sysfdType, oldname string, oldEndsInSlash bool) (struct{}, error) { | |
| flags := uint(0) | |
| if runtime.GOOS == "windows" { | |
| // Windows doesn't pay attention to trailing slashes in the link target. | |
| flags |= doInRootNoHandleTerminalSlash | |
| } | |
| _, err := doInRoot(r, newname, flags, nil, func(newparent sysfdType, newname string, newEndsInSlash bool) (struct{}, error) { | |
| return struct{}{}, linkat(oldparent, oldname, newparent, newname) | |
| }) | |
| return struct{}{}, err | |
| }) | |
| if err != nil { | |
| return &LinkError{"linkat", oldname, newname, err} | |
| } | |
| return err | |
| } | |
| // Flags for doInRoot. | |
| const ( | |
| // doInRootNoHandleTerminalSlash prevents doInRoot from applying special handling | |
| // for paths which end in one or more slashes. | |
| doInRootNoHandleTerminalSlash = 1 << iota | |
| // doInRootCreatingDirectory indicates that the operation is creating a directory. | |
| // When a path ends in /, the last path component may name a file which does not exist. | |
| doInRootCreatingDirectory | |
| // doInRootAlwaysResolveTerminalSlash causes doInRoot to resolve symlinks in the last | |
| // path component when a path ends in /, even on Windows. For example, this causes | |
| // doInRoot to resolve "symlink/" as the link target of "symlink". | |
| // | |
| // POSIX path operations resolve symlinks in this case. | |
| // Most Windows operations do not. | |
| // This flag enforces the POSIX behavior. | |
| doInRootAlwaysResolveTerminalSlash | |
| ) | |
| // doInRoot performs an operation on a path in a Root. | |
| // | |
| // It calls f with the FD or handle for the directory containing the last | |
| // path element, the name of the last path element (not including slashes), | |
| // and a boolean indicating whether the original path ended in one or more slashes. | |
| // | |
| // For example, given the path a/b/c it calls f with the FD for a/b and the name "c". | |
| // | |
| // It applies special handling for paths ending in a slash: When a path ends in a slash | |
| // (for example "a/b/"), doInRoot will check the final component ("b") before calling f. | |
| // If the final component is a symlink, doInRoot will resolve it. | |
| // If the final component is neither a symlink nor a directory, doInRoot will return ENOTDIR. | |
| // This behavior may be disabled by passing the doInRootNoHandleTerminalSlash flag. | |
| // | |
| // If openDirFunc is non-nil, it is called to open intermediate path elements. | |
| // For example, given the path a/b/c openDirFunc will be called to open a and a/b in turn. | |
| // | |
| // f or openDirFunc may return errSymlink to indicate that the path element is a symlink | |
| // which should be followed. Note that this can result in f being called multiple times | |
| // with different names. For example, given the path "link" which is a symlink to "target", | |
| // f is called with the path "link", returns errSymlink("target"), and is called again with | |
| // the path "target". | |
| // | |
| // If f or openDirFunc return a *PathError, doInRoot will set PathError.Path to the | |
| // full path which caused the error. | |
| 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) { | |
| if err := r.root.incref(); err != nil { | |
| return ret, err | |
| } | |
| defer r.root.decref() | |
| parts, endsInSlash, err := splitPathInRoot(name, nil, nil) | |
| if err != nil { | |
| return ret, err | |
| } | |
| if openDirFunc == nil { | |
| openDirFunc = rootOpenDir | |
| } | |
| rootfd := r.root.fd | |
| dirfd := rootfd | |
| defer func() { | |
| if dirfd != rootfd { | |
| syscall.Close(dirfd) | |
| } | |
| }() | |
| // When resolving .. path components, we restart path resolution from the root. | |
| // (We can't openat(dir, "..") to move up to the parent directory, | |
| // because dir may have moved since we opened it.) | |
| // To limit how many opens a malicious path can cause us to perform, we set | |
| // a limit on the total number of path steps and the total number of restarts | |
| // caused by .. components. If *both* limits are exceeded, we halt the operation. | |
| const maxSteps = 255 | |
| const maxRestarts = 8 | |
| i := 0 | |
| steps := 0 | |
| restarts := 0 | |
| symlinks := 0 | |
| Loop: | |
| for { | |
| steps++ | |
| if steps > maxSteps && restarts > maxRestarts { | |
| return ret, syscall.ENAMETOOLONG | |
| } | |
| if parts[i] == ".." { | |
| // Resolve one or more parent ("..") path components. | |
| // | |
| // Rewrite the original path, | |
| // removing the elements eliminated by ".." components, | |
| // and start over from the beginning. | |
| restarts++ | |
| n := 1 // number of .. segments to remove | |
| for i+n < len(parts) && parts[i+n] == ".." { | |
| n++ | |
| } | |
| switch { | |
| case n > i: | |
| return ret, errPathEscapes | |
| case len(parts) == 2*n: | |
| parts = []string{"."} | |
| default: | |
| parts = slices.Delete(parts, i-n, i+n) | |
| } | |
| i = 0 | |
| if dirfd != rootfd { | |
| syscall.Close(dirfd) | |
| } | |
| dirfd = rootfd | |
| continue | |
| } | |
| if i == len(parts)-1 { | |
| err = nil | |
| if endsInSlash && flags&doInRootNoHandleTerminalSlash == 0 { | |
| var fi FileInfo | |
| fi, err = lstatat(dirfd, parts[i]) | |
| switch { | |
| case IsNotExist(err) && flags&doInRootCreatingDirectory != 0: | |
| // The path ends in a slash, the last path component | |
| // does not exist, and we creating a directory. | |
| // This is fine. | |
| err = nil | |
| case err != nil: | |
| return | |
| case fi.Mode().Type() == fs.ModeDir: | |
| case fi.Mode().Type() == fs.ModeSymlink: | |
| if runtime.GOOS != "windows" || flags&doInRootAlwaysResolveTerminalSlash != 0 { | |
| err = checkSymlink(dirfd, parts[i], syscall.ENOTDIR) | |
| } else { | |
| if !isDirectoryLink(fi) { | |
| err = syscall.ENOTDIR | |
| } | |
| } | |
| default: | |
| err = syscall.ENOTDIR | |
| return | |
| } | |
| } | |
| // This is the last path element. | |
| // Call f to decide what to do with it. | |
| // If f returns errSymlink, this element is a symlink | |
| // which should be followed. | |
| // suffixSep contains any trailing separator characters. | |
| if err == nil { | |
| ret, err = f(dirfd, parts[i], endsInSlash) | |
| if err == nil { | |
| return | |
| } | |
| } | |
| } else { | |
| var fd sysfdType | |
| fd, err = openDirFunc(dirfd, parts[i]) | |
| if err == nil { | |
| if dirfd != rootfd { | |
| syscall.Close(dirfd) | |
| } | |
| dirfd = fd | |
| } | |
| } | |
| switch e := err.(type) { | |
| case nil: | |
| case errSymlink: | |
| symlinks++ | |
| if symlinks > rootMaxSymlinks { | |
| return ret, syscall.ELOOP | |
| } | |
| lastPart := i == len(parts)-1 | |
| newparts, newEndsInSlash, err := splitPathInRoot(string(e), parts[:i], parts[i+1:]) | |
| if err != nil { | |
| return ret, err | |
| } | |
| if lastPart && newEndsInSlash { | |
| // If a link target in the final path component ends in a slash, | |
| // then the path now ends in a slash. | |
| endsInSlash = true | |
| } | |
| if len(newparts) < i || !slices.Equal(parts[:i], newparts[:i]) { | |
| // Some component in the path which we have already traversed | |
| // has changed. We need to restart parsing from the root. | |
| i = 0 | |
| if dirfd != rootfd { | |
| syscall.Close(dirfd) | |
| } | |
| dirfd = rootfd | |
| } | |
| parts = newparts | |
| continue Loop | |
| case *PathError: | |
| // This is strings.Join(parts[:i+1], PathSeparator). | |
| e.Path = parts[0] | |
| for _, part := range parts[1 : i+1] { | |
| e.Path += string(PathSeparator) + part | |
| } | |
| return ret, e | |
| default: | |
| return ret, err | |
| } | |
| i++ | |
| } | |
| } | |
| func modeAt(parent sysfdType, name string) (FileMode, error) { | |
| fi, err := lstatat(parent, name) | |
| if err != nil { | |
| return 0, err | |
| } | |
| return fi.Mode(), nil | |
| } | |
| // errSymlink reports that a file being operated on is actually a symlink, | |
| // and the target of that symlink. | |
| type errSymlink string | |
| func (errSymlink) Error() string { panic("errSymlink is not user-visible") } |