|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +/* |
| 3 | + * Copyright (C) 2024-2025 Aleksa Sarai <[email protected]> |
| 4 | + * Copyright (C) 2024-2025 SUSE LLC |
| 5 | + * |
| 6 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | + * you may not use this file except in compliance with the License. |
| 8 | + * You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, software |
| 13 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | + * See the License for the specific language governing permissions and |
| 16 | + * limitations under the License. |
| 17 | + */ |
| 18 | + |
| 19 | +package pathrs |
| 20 | + |
| 21 | +import ( |
| 22 | + "fmt" |
| 23 | + "os" |
| 24 | + "path/filepath" |
| 25 | + |
| 26 | + "github.com/cyphar/filepath-securejoin/pathrs-lite" |
| 27 | + "github.com/sirupsen/logrus" |
| 28 | + "golang.org/x/sys/unix" |
| 29 | +) |
| 30 | + |
| 31 | +// MkdirAllInRootOpen attempts to make |
| 32 | +// |
| 33 | +// path, _ := securejoin.SecureJoin(root, unsafePath) |
| 34 | +// os.MkdirAll(path, mode) |
| 35 | +// os.Open(path) |
| 36 | +// |
| 37 | +// safer against attacks where components in the path are changed between |
| 38 | +// SecureJoin returning and MkdirAll (or Open) being called. In particular, we |
| 39 | +// try to detect any symlink components in the path while we are doing the |
| 40 | +// MkdirAll. |
| 41 | +// |
| 42 | +// NOTE: If unsafePath is a subpath of root, we assume that you have already |
| 43 | +// called SecureJoin and so we use the provided path verbatim without resolving |
| 44 | +// any symlinks (this is done in a way that avoids symlink-exchange races). |
| 45 | +// This means that the path also must not contain ".." elements, otherwise an |
| 46 | +// error will occur. |
| 47 | +// |
| 48 | +// This uses (pathrs-lite).MkdirAllHandle under the hood, but it has special |
| 49 | +// handling if unsafePath has already been scoped within the rootfs (this is |
| 50 | +// needed for a lot of runc callers and fixing this would require reworking a |
| 51 | +// lot of path logic). |
| 52 | +func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (*os.File, error) { |
| 53 | + // If the path is already "within" the root, get the path relative to the |
| 54 | + // root and use that as the unsafe path. This is necessary because a lot of |
| 55 | + // MkdirAllInRootOpen callers have already done SecureJoin, and refactoring |
| 56 | + // all of them to stop using these SecureJoin'd paths would require a fair |
| 57 | + // amount of work. |
| 58 | + // TODO(cyphar): Do the refactor to libpathrs once it's ready. |
| 59 | + if IsLexicallyInRoot(root, unsafePath) { |
| 60 | + subPath, err := filepath.Rel(root, unsafePath) |
| 61 | + if err != nil { |
| 62 | + return nil, err |
| 63 | + } |
| 64 | + unsafePath = subPath |
| 65 | + } |
| 66 | + |
| 67 | + // Check for any silly mode bits. |
| 68 | + if mode&^0o7777 != 0 { |
| 69 | + return nil, fmt.Errorf("tried to include non-mode bits in MkdirAll mode: 0o%.3o", mode) |
| 70 | + } |
| 71 | + // Linux (and thus os.MkdirAll) silently ignores the suid and sgid bits if |
| 72 | + // passed. While it would make sense to return an error in that case (since |
| 73 | + // the user has asked for a mode that won't be applied), for compatibility |
| 74 | + // reasons we have to ignore these bits. |
| 75 | + if ignoredBits := mode &^ 0o1777; ignoredBits != 0 { |
| 76 | + logrus.Warnf("MkdirAll called with no-op mode bits that are ignored by Linux: 0o%.3o", ignoredBits) |
| 77 | + mode &= 0o1777 |
| 78 | + } |
| 79 | + |
| 80 | + rootDir, err := os.OpenFile(root, unix.O_DIRECTORY|unix.O_CLOEXEC, 0) |
| 81 | + if err != nil { |
| 82 | + return nil, fmt.Errorf("open root handle: %w", err) |
| 83 | + } |
| 84 | + defer rootDir.Close() |
| 85 | + |
| 86 | + return retryEAGAIN(func() (*os.File, error) { |
| 87 | + return pathrs.MkdirAllHandle(rootDir, unsafePath, mode) |
| 88 | + }) |
| 89 | +} |
| 90 | + |
| 91 | +// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the |
| 92 | +// returned handle, for callers that don't need to use it. |
| 93 | +func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error { |
| 94 | + f, err := MkdirAllInRootOpen(root, unsafePath, mode) |
| 95 | + if err == nil { |
| 96 | + _ = f.Close() |
| 97 | + } |
| 98 | + return err |
| 99 | +} |
0 commit comments