main
Raw Download raw file
  1//go:build linux
  2
  3// Copyright (C) 2024 SUSE LLC. All rights reserved.
  4// Use of this source code is governed by a BSD-style
  5// license that can be found in the LICENSE file.
  6
  7package securejoin
  8
  9import (
 10	"fmt"
 11	"os"
 12	"strconv"
 13
 14	"golang.org/x/sys/unix"
 15)
 16
 17// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
 18// using an *[os.File] handle, to ensure that the correct root directory is used.
 19func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
 20	handle, err := completeLookupInRoot(root, unsafePath)
 21	if err != nil {
 22		return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
 23	}
 24	return handle, nil
 25}
 26
 27// OpenInRoot safely opens the provided unsafePath within the root.
 28// Effectively, OpenInRoot(root, unsafePath) is equivalent to
 29//
 30//	path, _ := securejoin.SecureJoin(root, unsafePath)
 31//	handle, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
 32//
 33// But is much safer. The above implementation is unsafe because if an attacker
 34// can modify the filesystem tree between [SecureJoin] and [os.OpenFile], it is
 35// possible for the returned file to be outside of the root.
 36//
 37// Note that the returned handle is an O_PATH handle, meaning that only a very
 38// limited set of operations will work on the handle. This is done to avoid
 39// accidentally opening an untrusted file that could cause issues (such as a
 40// disconnected TTY that could cause a DoS, or some other issue). In order to
 41// use the returned handle, you can "upgrade" it to a proper handle using
 42// [Reopen].
 43func OpenInRoot(root, unsafePath string) (*os.File, error) {
 44	rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
 45	if err != nil {
 46		return nil, err
 47	}
 48	defer rootDir.Close()
 49	return OpenatInRoot(rootDir, unsafePath)
 50}
 51
 52// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
 53// Reopen(file, flags) is effectively equivalent to
 54//
 55//	fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
 56//	os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
 57//
 58// But with some extra hardenings to ensure that we are not tricked by a
 59// maliciously-configured /proc mount. While this attack scenario is not
 60// common, in container runtimes it is possible for higher-level runtimes to be
 61// tricked into configuring an unsafe /proc that can be used to attack file
 62// operations. See [CVE-2019-19921] for more details.
 63//
 64// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
 65func Reopen(handle *os.File, flags int) (*os.File, error) {
 66	procRoot, err := getProcRoot()
 67	if err != nil {
 68		return nil, err
 69	}
 70
 71	// We can't operate on /proc/thread-self/fd/$n directly when doing a
 72	// re-open, so we need to open /proc/thread-self/fd and then open a single
 73	// final component.
 74	procFdDir, closer, err := procThreadSelf(procRoot, "fd/")
 75	if err != nil {
 76		return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err)
 77	}
 78	defer procFdDir.Close()
 79	defer closer()
 80
 81	// Try to detect if there is a mount on top of the magic-link we are about
 82	// to open. If we are using unsafeHostProcRoot(), this could change after
 83	// we check it (and there's nothing we can do about that) but for
 84	// privateProcRoot() this should be guaranteed to be safe (at least since
 85	// Linux 5.12[1], when anonymous mount namespaces were completely isolated
 86	// from external mounts including mount propagation events).
 87	//
 88	// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
 89	// onto targets that reside on shared mounts").
 90	fdStr := strconv.Itoa(int(handle.Fd()))
 91	if err := checkSymlinkOvermount(procRoot, procFdDir, fdStr); err != nil {
 92		return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err)
 93	}
 94
 95	flags |= unix.O_CLOEXEC
 96	// Rather than just wrapping openatFile, open-code it so we can copy
 97	// handle.Name().
 98	reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0)
 99	if err != nil {
100		return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err)
101	}
102	return os.NewFile(uintptr(reopenFd), handle.Name()), nil
103}