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