+// OpenParentDirectoryNoSymlinks opens the dirname of path without following
+// any symlinks and returns a file descriptor to it and the basename of path.
+// To prevent symlink attacks in earlier path components when these are
+// writable by other users it starts at the root (or current directory for
+// relative paths) and uses openat (with O_NOFOLLOW) for each path component.
+// If a symlink is encountered it returns an error. However, it's impossible
+// to guarantee that the returned descriptor refers to the same location as
+// given in path because users with write access can rename path components.
+// But this is not required to prevent the mentioned attacks.
+func OpenParentDirectoryNoSymlinks(path string) (int, string, error) {
+ // Slash separated paths are used for the configuration
+ parts := strings.Split(path, "/")
+
+ var dir string
+ if path == "/" {
+ // Root: use root itself as base name because root is the
+ // parent of itself
+ dir = "/"
+ parts = []string{"/"}
+ } else if parts[0] == "" {
+ // Absolute path
+ dir = "/"
+ parts = parts[1:]
+ } else if path == "." {
+ // Current directory: open parent directory and use current
+ // directory name as base name
+ wd, err := os.Getwd()
+ if err != nil {
+ return -1, "", fmt.Errorf(
+ "failed to get working directory: %w", err)
+ }
+ dir = ".."
+ parts = []string{filepath.Base(wd)}
+ } else if parts[0] != "." {
+ // Relative path: start at the current directory
+ dir = "."
+ }
+
+ dirFd, err := unix.Openat(unix.AT_FDCWD, dir, openReadonlyFlags, 0)
+ if err != nil {
+ return -1, "", err
+ }
+ // Walk path one directory at a time to ensure there are no symlinks
+ // in the path. This prevents users with write access to change the
+ // path to point to arbitrary locations. O_NOFOLLOW when opening the
+ // path is not enough as only the last path component is checked.
+ for i, name := range parts[:len(parts)-1] {
+ fd, err := unix.Openat(dirFd, name, openReadonlyFlags, 0)
+ if err != nil {
+ unix.Close(dirFd)
+ if err == unix.ELOOP || err == unix.EMLINK {
+ x := filepath.Join(append([]string{dir},
+ parts[:i+1]...)...)
+ return -1, "", fmt.Errorf(
+ "symlink not permitted in path: %q",
+ x)
+ }
+ return -1, "", err
+ }
+ unix.Close(dirFd)
+ dirFd = fd
+ }
+
+ return dirFd, parts[len(parts)-1], nil
+}
+