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 "errors"
11 "fmt"
12 "os"
13 "path/filepath"
14 "strings"
15
16 "golang.org/x/sys/unix"
17)
18
19var hasOpenat2 = sync_OnceValue(func() bool {
20 fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{
21 Flags: unix.O_PATH | unix.O_CLOEXEC,
22 Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT,
23 })
24 if err != nil {
25 return false
26 }
27 _ = unix.Close(fd)
28 return true
29})
30
31func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
32 // RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
33 // ".." while a mount or rename occurs anywhere on the system. This could
34 // happen spuriously, or as the result of an attacker trying to mess with
35 // us during lookup.
36 //
37 // In addition, scoped lookups have a "safety check" at the end of
38 // complete_walk which will return -EXDEV if the final path is not in the
39 // root.
40 return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
41 (errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
42}
43
44const scopedLookupMaxRetries = 10
45
46func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) {
47 fullPath := dir.Name() + "/" + path
48 // Make sure we always set O_CLOEXEC.
49 how.Flags |= unix.O_CLOEXEC
50 var tries int
51 for tries < scopedLookupMaxRetries {
52 fd, err := unix.Openat2(int(dir.Fd()), path, how)
53 if err != nil {
54 if scopedLookupShouldRetry(how, err) {
55 // We retry a couple of times to avoid the spurious errors, and
56 // if we are being attacked then returning -EAGAIN is the best
57 // we can do.
58 tries++
59 continue
60 }
61 return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
62 }
63 // If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
64 // NOTE: The procRoot code MUST NOT use RESOLVE_IN_ROOT, otherwise
65 // you'll get infinite recursion here.
66 if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT {
67 if actualPath, err := rawProcSelfFdReadlink(fd); err == nil {
68 fullPath = actualPath
69 }
70 }
71 return os.NewFile(uintptr(fd), fullPath), nil
72 }
73 return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: errPossibleAttack}
74}
75
76func lookupOpenat2(root *os.File, unsafePath string, partial bool) (*os.File, string, error) {
77 if !partial {
78 file, err := openat2File(root, unsafePath, &unix.OpenHow{
79 Flags: unix.O_PATH | unix.O_CLOEXEC,
80 Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
81 })
82 return file, "", err
83 }
84 return partialLookupOpenat2(root, unsafePath)
85}
86
87// partialLookupOpenat2 is an alternative implementation of
88// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a
89// handle to the deepest existing child of the requested path within the root.
90func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, error) {
91 // TODO: Implement this as a git-bisect-like binary search.
92
93 unsafePath = filepath.ToSlash(unsafePath) // noop
94 endIdx := len(unsafePath)
95 var lastError error
96 for endIdx > 0 {
97 subpath := unsafePath[:endIdx]
98
99 handle, err := openat2File(root, subpath, &unix.OpenHow{
100 Flags: unix.O_PATH | unix.O_CLOEXEC,
101 Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
102 })
103 if err == nil {
104 // Jump over the slash if we have a non-"" remainingPath.
105 if endIdx < len(unsafePath) {
106 endIdx += 1
107 }
108 // We found a subpath!
109 return handle, unsafePath[endIdx:], lastError
110 }
111 if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
112 // That path doesn't exist, let's try the next directory up.
113 endIdx = strings.LastIndexByte(subpath, '/')
114 lastError = err
115 continue
116 }
117 return nil, "", fmt.Errorf("open subpath: %w", err)
118 }
119 // If we couldn't open anything, the whole subpath is missing. Return a
120 // copy of the root fd so that the caller doesn't close this one by
121 // accident.
122 rootClone, err := dupFile(root)
123 if err != nil {
124 return nil, "", err
125 }
126 return rootClone, unsafePath, lastError
127}