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/filepath"
 14	"strings"
 15
 16	"golang.org/x/sys/unix"
 17)
 18
 19var (
 20	errInvalidMode    = errors.New("invalid permission mode")
 21	errPossibleAttack = errors.New("possible attack detected")
 22)
 23
 24// modePermExt is like os.ModePerm except that it also includes the set[ug]id
 25// and sticky bits.
 26const modePermExt = os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
 27
 28//nolint:cyclop // this function needs to handle a lot of cases
 29func toUnixMode(mode os.FileMode) (uint32, error) {
 30	sysMode := uint32(mode.Perm())
 31	if mode&os.ModeSetuid != 0 {
 32		sysMode |= unix.S_ISUID
 33	}
 34	if mode&os.ModeSetgid != 0 {
 35		sysMode |= unix.S_ISGID
 36	}
 37	if mode&os.ModeSticky != 0 {
 38		sysMode |= unix.S_ISVTX
 39	}
 40	// We don't allow file type bits.
 41	if mode&os.ModeType != 0 {
 42		return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", errInvalidMode, mode, mode)
 43	}
 44	// We don't allow other unknown modes.
 45	if mode&^modePermExt != 0 || sysMode&unix.S_IFMT != 0 {
 46		return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", errInvalidMode, mode, mode)
 47	}
 48	return sysMode, nil
 49}
 50
 51// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
 52// in two respects:
 53//
 54//   - The caller provides the root directory as an *[os.File] (preferably O_PATH)
 55//     handle. This means that the caller can be sure which root directory is
 56//     being used. Note that this can be emulated by using /proc/self/fd/... as
 57//     the root path with [os.MkdirAll].
 58//
 59//   - Once all of the directories have been created, an *[os.File] O_PATH handle
 60//     to the directory at unsafePath is returned to the caller. This is done in
 61//     an effectively-race-free way (an attacker would only be able to swap the
 62//     final directory component), which is not possible to emulate with
 63//     [MkdirAll].
 64//
 65// In addition, the returned handle is obtained far more efficiently than doing
 66// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
 67// doing [MkdirAll]. If you intend to open the directory after creating it, you
 68// should use MkdirAllHandle.
 69func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) {
 70	unixMode, err := toUnixMode(mode)
 71	if err != nil {
 72		return nil, err
 73	}
 74	// On Linux, mkdirat(2) (and os.Mkdir) silently ignore the suid and sgid
 75	// bits. We could also silently ignore them but since we have very few
 76	// users it seems more prudent to return an error so users notice that
 77	// these bits will not be set.
 78	if unixMode&^0o1777 != 0 {
 79		return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
 80	}
 81
 82	// Try to open as much of the path as possible.
 83	currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath)
 84	defer func() {
 85		if Err != nil {
 86			_ = currentDir.Close()
 87		}
 88	}()
 89	if err != nil && !errors.Is(err, unix.ENOENT) {
 90		return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err)
 91	}
 92
 93	// If there is an attacker deleting directories as we walk into them,
 94	// detect this proactively. Note this is guaranteed to detect if the
 95	// attacker deleted any part of the tree up to currentDir.
 96	//
 97	// Once we walk into a dead directory, partialLookupInRoot would not be
 98	// able to walk further down the tree (directories must be empty before
 99	// they are deleted), and if the attacker has removed the entire tree we
100	// can be sure that anything that was originally inside a dead directory
101	// must also be deleted and thus is a dead directory in its own right.
102	//
103	// This is mostly a quality-of-life check, because mkdir will simply fail
104	// later if the attacker deletes the tree after this check.
105	if err := isDeadInode(currentDir); err != nil {
106		return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err)
107	}
108
109	// Re-open the path to match the O_DIRECTORY reopen loop later (so that we
110	// always return a non-O_PATH handle). We also check that we actually got a
111	// directory.
112	if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
113		return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
114	} else if err != nil {
115		return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
116	} else {
117		_ = currentDir.Close()
118		currentDir = reopenDir
119	}
120
121	remainingParts := strings.Split(remainingPath, string(filepath.Separator))
122	if slices_Contains(remainingParts, "..") {
123		// The path contained ".." components after the end of the "real"
124		// components. We could try to safely resolve ".." here but that would
125		// add a bunch of extra logic for something that it's not clear even
126		// needs to be supported. So just return an error.
127		//
128		// If we do filepath.Clean(remainingPath) then we end up with the
129		// problem that ".." can erase a trailing dangling symlink and produce
130		// a path that doesn't quite match what the user asked for.
131		return nil, fmt.Errorf("%w: yet-to-be-created path %q contains '..' components", unix.ENOENT, remainingPath)
132	}
133
134	// Create the remaining components.
135	for _, part := range remainingParts {
136		switch part {
137		case "", ".":
138			// Skip over no-op paths.
139			continue
140		}
141
142		// NOTE: mkdir(2) will not follow trailing symlinks, so we can safely
143		// create the final component without worrying about symlink-exchange
144		// attacks.
145		//
146		// If we get -EEXIST, it's possible that another program created the
147		// directory at the same time as us. In that case, just continue on as
148		// if we created it (if the created inode is not a directory, the
149		// following open call will fail).
150		if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) {
151			err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
152			// Make the error a bit nicer if the directory is dead.
153			if deadErr := isDeadInode(currentDir); deadErr != nil {
154				// TODO: Once we bump the minimum Go version to 1.20, we can use
155				// multiple %w verbs for this wrapping. For now we need to use a
156				// compatibility shim for older Go versions.
157				//err = fmt.Errorf("%w (%w)", err, deadErr)
158				err = wrapBaseError(err, deadErr)
159			}
160			return nil, err
161		}
162
163		// Get a handle to the next component. O_DIRECTORY means we don't need
164		// to use O_PATH.
165		var nextDir *os.File
166		if hasOpenat2() {
167			nextDir, err = openat2File(currentDir, part, &unix.OpenHow{
168				Flags:   unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC,
169				Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
170			})
171		} else {
172			nextDir, err = openatFile(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
173		}
174		if err != nil {
175			return nil, err
176		}
177		_ = currentDir.Close()
178		currentDir = nextDir
179
180		// It's possible that the directory we just opened was swapped by an
181		// attacker. Unfortunately there isn't much we can do to protect
182		// against this, and MkdirAll's behaviour is that we will reuse
183		// existing directories anyway so the need to protect against this is
184		// incredibly limited (and arguably doesn't even deserve mention here).
185		//
186		// Ideally we might want to check that the owner and mode match what we
187		// would've created -- unfortunately, it is non-trivial to verify that
188		// the owner and mode of the created directory match. While plain Unix
189		// DAC rules seem simple enough to emulate, there are a bunch of other
190		// factors that can change the mode or owner of created directories
191		// (default POSIX ACLs, mount options like uid=1,gid=2,umask=0 on
192		// filesystems like vfat, etc etc). We used to try to verify this but
193		// it just lead to a series of spurious errors.
194		//
195		// We could also check that the directory is non-empty, but
196		// unfortunately some pseduofilesystems (like cgroupfs) create
197		// non-empty directories, which would result in different spurious
198		// errors.
199	}
200	return currentDir, nil
201}
202
203// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
204// where the new directory is guaranteed to be within the root directory (if an
205// attacker can move directories from inside the root to outside the root, the
206// created directory tree might be outside of the root but the key constraint
207// is that at no point will we walk outside of the directory tree we are
208// creating).
209//
210// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
211//
212//	path, _ := securejoin.SecureJoin(root, unsafePath)
213//	err := os.MkdirAll(path, mode)
214//
215// But is much safer. The above implementation is unsafe because if an attacker
216// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
217// possible for MkdirAll to resolve unsafe symlink components and create
218// directories outside of the root.
219//
220// If you plan to open the directory after you have created it or want to use
221// an open directory handle as the root, you should use [MkdirAllHandle] instead.
222// This function is a wrapper around [MkdirAllHandle].
223func MkdirAll(root, unsafePath string, mode os.FileMode) error {
224	rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
225	if err != nil {
226		return err
227	}
228	defer rootDir.Close()
229
230	f, err := MkdirAllHandle(rootDir, unsafePath, mode)
231	if err != nil {
232		return err
233	}
234	_ = f.Close()
235	return nil
236}