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"
14 "path/filepath"
15 "strings"
16
17 "golang.org/x/sys/unix"
18)
19
20type symlinkStackEntry struct {
21 // (dir, remainingPath) is what we would've returned if the link didn't
22 // exist. This matches what openat2(RESOLVE_IN_ROOT) would return in
23 // this case.
24 dir *os.File
25 remainingPath string
26 // linkUnwalked is the remaining path components from the original
27 // Readlink which we have yet to walk. When this slice is empty, we
28 // drop the link from the stack.
29 linkUnwalked []string
30}
31
32func (se symlinkStackEntry) String() string {
33 return fmt.Sprintf("<%s>/%s [->%s]", se.dir.Name(), se.remainingPath, strings.Join(se.linkUnwalked, "/"))
34}
35
36func (se symlinkStackEntry) Close() {
37 _ = se.dir.Close()
38}
39
40type symlinkStack []*symlinkStackEntry
41
42func (s *symlinkStack) IsEmpty() bool {
43 return s == nil || len(*s) == 0
44}
45
46func (s *symlinkStack) Close() {
47 if s != nil {
48 for _, link := range *s {
49 link.Close()
50 }
51 // TODO: Switch to clear once we switch to Go 1.21.
52 *s = nil
53 }
54}
55
56var (
57 errEmptyStack = errors.New("[internal] stack is empty")
58 errBrokenSymlinkStack = errors.New("[internal error] broken symlink stack")
59)
60
61func (s *symlinkStack) popPart(part string) error {
62 if s == nil || s.IsEmpty() {
63 // If there is nothing in the symlink stack, then the part was from the
64 // real path provided by the user, and this is a no-op.
65 return errEmptyStack
66 }
67 if part == "." {
68 // "." components are no-ops -- we drop them when doing SwapLink.
69 return nil
70 }
71
72 tailEntry := (*s)[len(*s)-1]
73
74 // Double-check that we are popping the component we expect.
75 if len(tailEntry.linkUnwalked) == 0 {
76 return fmt.Errorf("%w: trying to pop component %q of empty stack entry %s", errBrokenSymlinkStack, part, tailEntry)
77 }
78 headPart := tailEntry.linkUnwalked[0]
79 if headPart != part {
80 return fmt.Errorf("%w: trying to pop component %q but the last stack entry is %s (%q)", errBrokenSymlinkStack, part, tailEntry, headPart)
81 }
82
83 // Drop the component, but keep the entry around in case we are dealing
84 // with a "tail-chained" symlink.
85 tailEntry.linkUnwalked = tailEntry.linkUnwalked[1:]
86 return nil
87}
88
89func (s *symlinkStack) PopPart(part string) error {
90 if err := s.popPart(part); err != nil {
91 if errors.Is(err, errEmptyStack) {
92 // Skip empty stacks.
93 err = nil
94 }
95 return err
96 }
97
98 // Clean up any of the trailing stack entries that are empty.
99 for lastGood := len(*s) - 1; lastGood >= 0; lastGood-- {
100 entry := (*s)[lastGood]
101 if len(entry.linkUnwalked) > 0 {
102 break
103 }
104 entry.Close()
105 (*s) = (*s)[:lastGood]
106 }
107 return nil
108}
109
110func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) error {
111 if s == nil {
112 return nil
113 }
114 // Split the link target and clean up any "" parts.
115 linkTargetParts := slices_DeleteFunc(
116 strings.Split(linkTarget, "/"),
117 func(part string) bool { return part == "" || part == "." })
118
119 // Copy the directory so the caller doesn't close our copy.
120 dirCopy, err := dupFile(dir)
121 if err != nil {
122 return err
123 }
124
125 // Add to the stack.
126 *s = append(*s, &symlinkStackEntry{
127 dir: dirCopy,
128 remainingPath: remainingPath,
129 linkUnwalked: linkTargetParts,
130 })
131 return nil
132}
133
134func (s *symlinkStack) SwapLink(linkPart string, dir *os.File, remainingPath, linkTarget string) error {
135 // If we are currently inside a symlink resolution, remove the symlink
136 // component from the last symlink entry, but don't remove the entry even
137 // if it's empty. If we are a "tail-chained" symlink (a trailing symlink we
138 // hit during a symlink resolution) we need to keep the old symlink until
139 // we finish the resolution.
140 if err := s.popPart(linkPart); err != nil {
141 if !errors.Is(err, errEmptyStack) {
142 return err
143 }
144 // Push the component regardless of whether the stack was empty.
145 }
146 return s.push(dir, remainingPath, linkTarget)
147}
148
149func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
150 if s == nil || s.IsEmpty() {
151 return nil, "", false
152 }
153 tailEntry := (*s)[0]
154 *s = (*s)[1:]
155 return tailEntry.dir, tailEntry.remainingPath, true
156}
157
158// partialLookupInRoot tries to lookup as much of the request path as possible
159// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
160// component of the requested path, returning a file handle to the final
161// existing component and a string containing the remaining path components.
162func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) {
163 return lookupInRoot(root, unsafePath, true)
164}
165
166func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) {
167 handle, remainingPath, err := lookupInRoot(root, unsafePath, false)
168 if remainingPath != "" && err == nil {
169 // should never happen
170 err = fmt.Errorf("[bug] non-empty remaining path when doing a non-partial lookup: %q", remainingPath)
171 }
172 // lookupInRoot(partial=false) will always close the handle if an error is
173 // returned, so no need to double-check here.
174 return handle, err
175}
176
177func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
178 unsafePath = filepath.ToSlash(unsafePath) // noop
179
180 // This is very similar to SecureJoin, except that we operate on the
181 // components using file descriptors. We then return the last component we
182 // managed open, along with the remaining path components not opened.
183
184 // Try to use openat2 if possible.
185 if hasOpenat2() {
186 return lookupOpenat2(root, unsafePath, partial)
187 }
188
189 // Get the "actual" root path from /proc/self/fd. This is necessary if the
190 // root is some magic-link like /proc/$pid/root, in which case we want to
191 // make sure when we do checkProcSelfFdPath that we are using the correct
192 // root path.
193 logicalRootPath, err := procSelfFdReadlink(root)
194 if err != nil {
195 return nil, "", fmt.Errorf("get real root path: %w", err)
196 }
197
198 currentDir, err := dupFile(root)
199 if err != nil {
200 return nil, "", fmt.Errorf("clone root fd: %w", err)
201 }
202 defer func() {
203 // If a handle is not returned, close the internal handle.
204 if Handle == nil {
205 _ = currentDir.Close()
206 }
207 }()
208
209 // symlinkStack is used to emulate how openat2(RESOLVE_IN_ROOT) treats
210 // dangling symlinks. If we hit a non-existent path while resolving a
211 // symlink, we need to return the (dir, remainingPath) that we had when we
212 // hit the symlink (treating the symlink as though it were a regular file).
213 // The set of (dir, remainingPath) sets is stored within the symlinkStack
214 // and we add and remove parts when we hit symlink and non-symlink
215 // components respectively. We need a stack because of recursive symlinks
216 // (symlinks that contain symlink components in their target).
217 //
218 // Note that the stack is ONLY used for book-keeping. All of the actual
219 // path walking logic is still based on currentPath/remainingPath and
220 // currentDir (as in SecureJoin).
221 var symStack *symlinkStack
222 if partial {
223 symStack = new(symlinkStack)
224 defer symStack.Close()
225 }
226
227 var (
228 linksWalked int
229 currentPath string
230 remainingPath = unsafePath
231 )
232 for remainingPath != "" {
233 // Save the current remaining path so if the part is not real we can
234 // return the path including the component.
235 oldRemainingPath := remainingPath
236
237 // Get the next path component.
238 var part string
239 if i := strings.IndexByte(remainingPath, '/'); i == -1 {
240 part, remainingPath = remainingPath, ""
241 } else {
242 part, remainingPath = remainingPath[:i], remainingPath[i+1:]
243 }
244 // If we hit an empty component, we need to treat it as though it is
245 // "." so that trailing "/" and "//" components on a non-directory
246 // correctly return the right error code.
247 if part == "" {
248 part = "."
249 }
250
251 // Apply the component lexically to the path we are building.
252 // currentPath does not contain any symlinks, and we are lexically
253 // dealing with a single component, so it's okay to do a filepath.Clean
254 // here.
255 nextPath := path.Join("/", currentPath, part)
256 // If we logically hit the root, just clone the root rather than
257 // opening the part and doing all of the other checks.
258 if nextPath == "/" {
259 if err := symStack.PopPart(part); err != nil {
260 return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err)
261 }
262 // Jump to root.
263 rootClone, err := dupFile(root)
264 if err != nil {
265 return nil, "", fmt.Errorf("clone root fd: %w", err)
266 }
267 _ = currentDir.Close()
268 currentDir = rootClone
269 currentPath = nextPath
270 continue
271 }
272
273 // Try to open the next component.
274 nextDir, err := openatFile(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
275 switch {
276 case err == nil:
277 st, err := nextDir.Stat()
278 if err != nil {
279 _ = nextDir.Close()
280 return nil, "", fmt.Errorf("stat component %q: %w", part, err)
281 }
282
283 switch st.Mode() & os.ModeType {
284 case os.ModeSymlink:
285 // readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
286 // Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
287 // fstatat() with empty relative pathnames").
288 linkDest, err := readlinkatFile(nextDir, "")
289 // We don't need the handle anymore.
290 _ = nextDir.Close()
291 if err != nil {
292 return nil, "", err
293 }
294
295 linksWalked++
296 if linksWalked > maxSymlinkLimit {
297 return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP}
298 }
299
300 // Swap out the symlink's component for the link entry itself.
301 if err := symStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil {
302 return nil, "", fmt.Errorf("walking into symlink %q failed: push symlink: %w", part, err)
303 }
304
305 // Update our logical remaining path.
306 remainingPath = linkDest + "/" + remainingPath
307 // Absolute symlinks reset any work we've already done.
308 if path.IsAbs(linkDest) {
309 // Jump to root.
310 rootClone, err := dupFile(root)
311 if err != nil {
312 return nil, "", fmt.Errorf("clone root fd: %w", err)
313 }
314 _ = currentDir.Close()
315 currentDir = rootClone
316 currentPath = "/"
317 }
318
319 default:
320 // If we are dealing with a directory, simply walk into it.
321 _ = currentDir.Close()
322 currentDir = nextDir
323 currentPath = nextPath
324
325 // The part was real, so drop it from the symlink stack.
326 if err := symStack.PopPart(part); err != nil {
327 return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err)
328 }
329
330 // If we are operating on a .., make sure we haven't escaped.
331 // We only have to check for ".." here because walking down
332 // into a regular component component cannot cause you to
333 // escape. This mirrors the logic in RESOLVE_IN_ROOT, except we
334 // have to check every ".." rather than only checking after a
335 // rename or mount on the system.
336 if part == ".." {
337 // Make sure the root hasn't moved.
338 if err := checkProcSelfFdPath(logicalRootPath, root); err != nil {
339 return nil, "", fmt.Errorf("root path moved during lookup: %w", err)
340 }
341 // Make sure the path is what we expect.
342 fullPath := logicalRootPath + nextPath
343 if err := checkProcSelfFdPath(fullPath, currentDir); err != nil {
344 return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err)
345 }
346 }
347 }
348
349 default:
350 if !partial {
351 return nil, "", err
352 }
353 // If there are any remaining components in the symlink stack, we
354 // are still within a symlink resolution and thus we hit a dangling
355 // symlink. So pretend that the first symlink in the stack we hit
356 // was an ENOENT (to match openat2).
357 if oldDir, remainingPath, ok := symStack.PopTopSymlink(); ok {
358 _ = currentDir.Close()
359 return oldDir, remainingPath, err
360 }
361 // We have hit a final component that doesn't exist, so we have our
362 // partial open result. Note that we have to use the OLD remaining
363 // path, since the lookup failed.
364 return currentDir, oldRemainingPath, err
365 }
366 }
367
368 // If the unsafePath had a trailing slash, we need to make sure we try to
369 // do a relative "." open so that we will correctly return an error when
370 // the final component is a non-directory (to match openat2). In the
371 // context of openat2, a trailing slash and a trailing "/." are completely
372 // equivalent.
373 if strings.HasSuffix(unsafePath, "/") {
374 nextDir, err := openatFile(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
375 if err != nil {
376 if !partial {
377 _ = currentDir.Close()
378 currentDir = nil
379 }
380 return currentDir, "", err
381 }
382 _ = currentDir.Close()
383 currentDir = nextDir
384 }
385
386 // All of the components existed!
387 return currentDir, "", nil
388}