main
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}