mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
merge private security patches into ghsa-release-1.4.0-rc.3
Aleksa Sarai (22): rootfs: re-allow dangling symlinks in mount targets openat2: improve resilience on busy systems selinux: use safe procfs API for labels rootfs: switch to fd-based handling of mountpoint targets libct/system: use securejoin for /proc/$pid/stat init: use securejoin for /proc/self/setgroups init: write sysctls using safe procfs API utils: remove unneeded EnsureProcHandle utils: use safe procfs for /proc/self/fd loop code apparmor: use safe procfs API for labels ci: add lint to forbid the usage of os.Create rootfs: avoid using os.Create for new device inodes internal: add wrappers for securejoin.Proc* go.mod: update to github.com/cyphar/filepath-securejoin@v0.5.0 console: verify /dev/pts/ptmx before use console: avoid trivial symlink attacks for /dev/console console: add fallback for pre-TIOCGPTPEER kernels console: use TIOCGPTPEER when allocating peer PTY *: switch to safer securejoin.Reopen internal: move utils.MkdirAllInRoot to internal/pathrs internal/sys: add VerifyInode helper internal: linux: add package doc-comment Li Fubang (1): libct: align param type for mountCgroupV1/V2 functions Kir Kolyshkin (3): libct: maskPaths: don't rely on ENOTDIR for mount libct: maskPaths: only ignore ENOENT on mount dest libct: add/use isDevNull, verifyDevNull Fixes: CVE-2025-31133 GHSA-9493-h29p-rfm2 Fixes: CVE-2025-52565 GHSA-qw9x-cqr3-wc7r Fixes: CVE-2025-52881 GHSA-cgrx-mc8f-2prm Reported-by: Lei Wang <ssst0n3@gmail.com> Reported-by: Li Fubang <lifubang@acmcoder.com> Reported-by: Tõnis Tiigi <tonistiigi@gmail.com> Reported-by: Aleksa Sarai <cyphar@cyphar.com> Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
This commit is contained in:
@@ -6,6 +6,9 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
)
|
||||
|
||||
@@ -36,19 +39,13 @@ func setProcAttr(attr, value string) error {
|
||||
// Under AppArmor you can only change your own attr, so there's no reason
|
||||
// to not use /proc/thread-self/ (instead of /proc/<tid>/, like libapparmor
|
||||
// does).
|
||||
attrPath, closer := utils.ProcThreadSelf(attrSubPath)
|
||||
defer closer()
|
||||
|
||||
f, err := os.OpenFile(attrPath, os.O_WRONLY, 0)
|
||||
f, closer, err := pathrs.ProcThreadSelfOpen(attrSubPath, unix.O_WRONLY|unix.O_CLOEXEC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closer()
|
||||
defer f.Close()
|
||||
|
||||
if err := utils.EnsureProcHandle(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.WriteString(value)
|
||||
return err
|
||||
}
|
||||
|
||||
+146
-22
@@ -1,40 +1,164 @@
|
||||
package libcontainer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"golang.org/x/sys/unix"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/internal/sys"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
)
|
||||
|
||||
// mount initializes the console inside the rootfs mounting with the specified mount label
|
||||
// and applying the correct ownership of the console.
|
||||
func mountConsole(slavePath string) error {
|
||||
f, err := os.Create("/dev/console")
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
if f != nil {
|
||||
// Ensure permission bits (can be different because of umask).
|
||||
if err := f.Chmod(0o666); err != nil {
|
||||
return err
|
||||
// checkPtmxHandle checks that the given file handle points to a real
|
||||
// /dev/pts/ptmx device inode on a real devpts mount. We cannot (trivially)
|
||||
// check that it is *the* /dev/pts for the container itself, but this is good
|
||||
// enough.
|
||||
func checkPtmxHandle(ptmx *os.File) error {
|
||||
//nolint:revive,staticcheck,nolintlint // ignore "don't use ALL_CAPS" warning // nolintlint is needed to work around the different lint configs
|
||||
const (
|
||||
PTMX_MAJOR = 5 // from TTYAUX_MAJOR in <linux/major.h>
|
||||
PTMX_MINOR = 2 // from mknod_ptmx in fs/devpts/inode.c
|
||||
PTMX_INO = 2 // from mknod_ptmx in fs/devpts/inode.c
|
||||
)
|
||||
return sys.VerifyInode(ptmx, func(stat *unix.Stat_t, statfs *unix.Statfs_t) error {
|
||||
if statfs.Type != unix.DEVPTS_SUPER_MAGIC {
|
||||
return fmt.Errorf("ptmx handle is not on a real devpts mount: super magic is %#x", statfs.Type)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
return mount(slavePath, "/dev/console", "bind", unix.MS_BIND, "")
|
||||
if stat.Ino != PTMX_INO {
|
||||
return fmt.Errorf("ptmx handle has wrong inode number: %v", stat.Ino)
|
||||
}
|
||||
if stat.Mode&unix.S_IFMT != unix.S_IFCHR || stat.Rdev != unix.Mkdev(PTMX_MAJOR, PTMX_MINOR) {
|
||||
return fmt.Errorf("ptmx handle is not a real char ptmx device: ftype %#x %d:%d",
|
||||
stat.Mode&unix.S_IFMT, unix.Major(stat.Rdev), unix.Minor(stat.Rdev))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// dupStdio opens the slavePath for the console and dups the fds to the current
|
||||
// processes stdio, fd 0,1,2.
|
||||
func dupStdio(slavePath string) error {
|
||||
fd, err := linux.Open(slavePath, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
func isPtyNoIoctlError(err error) bool {
|
||||
// The kernel converts -ENOIOCTLCMD to -ENOTTY automatically, but handle
|
||||
// -EINVAL just in case (which some drivers do, include pty).
|
||||
return errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ENOTTY)
|
||||
}
|
||||
|
||||
func getPtyPeer(pty console.Console, unsafePeerPath string, flags int) (*os.File, error) {
|
||||
peer, err := linux.GetPtyPeer(pty.Fd(), unsafePeerPath, flags)
|
||||
if err == nil || !isPtyNoIoctlError(err) {
|
||||
return peer, err
|
||||
}
|
||||
|
||||
// On pre-TIOCGPTPEER kernels (Linux < 4.13), we need to fallback to using
|
||||
// the /dev/pts/$n path generated using TIOCGPTN. We can do some validation
|
||||
// that the inode is correct because the Unix-98 pty has a consistent
|
||||
// numbering scheme for the device number of the peer.
|
||||
|
||||
peerNum, err := unix.IoctlGetUint32(int(pty.Fd()), unix.TIOCGPTN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get peer number of pty: %w", err)
|
||||
}
|
||||
//nolint:revive,staticcheck,nolintlint // ignore "don't use ALL_CAPS" warning // nolintlint is needed to work around the different lint configs
|
||||
const (
|
||||
UNIX98_PTY_SLAVE_MAJOR = 136 // from <linux/major.h>
|
||||
)
|
||||
wantPeerDev := unix.Mkdev(UNIX98_PTY_SLAVE_MAJOR, peerNum)
|
||||
|
||||
// Use O_PATH to avoid opening a bad inode before we validate it.
|
||||
peerHandle, err := os.OpenFile(unsafePeerPath, unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer peerHandle.Close()
|
||||
|
||||
if err := sys.VerifyInode(peerHandle, func(stat *unix.Stat_t, statfs *unix.Statfs_t) error {
|
||||
if statfs.Type != unix.DEVPTS_SUPER_MAGIC {
|
||||
return fmt.Errorf("pty peer handle is not on a real devpts mount: super magic is %#x", statfs.Type)
|
||||
}
|
||||
if stat.Mode&unix.S_IFMT != unix.S_IFCHR || stat.Rdev != wantPeerDev {
|
||||
return fmt.Errorf("pty peer handle is not the real char device for pty %d: ftype %#x %d:%d",
|
||||
peerNum, stat.Mode&unix.S_IFMT, unix.Major(stat.Rdev), unix.Minor(stat.Rdev))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pathrs.Reopen(peerHandle, flags)
|
||||
}
|
||||
|
||||
// safeAllocPty returns a new (ptmx, peer pty) allocation for use inside a
|
||||
// container.
|
||||
func safeAllocPty() (pty console.Console, peer *os.File, Err error) {
|
||||
// TODO: Use openat2(RESOLVE_NO_SYMLINKS|RESOLVE_NO_XDEV).
|
||||
ptmxHandle, err := os.OpenFile("/dev/pts/ptmx", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer ptmxHandle.Close()
|
||||
|
||||
if err := checkPtmxHandle(ptmxHandle); err != nil {
|
||||
return nil, nil, fmt.Errorf("verify ptmx handle: %w", err)
|
||||
}
|
||||
|
||||
ptyFile, err := pathrs.Reopen(ptmxHandle, unix.O_RDWR|unix.O_NOCTTY)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reopen ptmx to get new pty pair: %w", err)
|
||||
}
|
||||
// On success, the ownership is transferred to pty.
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = ptyFile.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
pty, unsafePeerPath, err := console.NewPtyFromFile(ptyFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = pty.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
peer, err = getPtyPeer(pty, unsafePeerPath, unix.O_RDWR|unix.O_NOCTTY)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get peer end of newly-allocated console: %w", err)
|
||||
}
|
||||
return pty, peer, nil
|
||||
}
|
||||
|
||||
// mountConsole bind-mounts the provided pty on top of /dev/console so programs
|
||||
// that operate on /dev/console operate on the correct container pty.
|
||||
func mountConsole(peerPty *os.File) error {
|
||||
console, err := os.OpenFile("/dev/console", unix.O_NOFOLLOW|unix.O_CREAT|unix.O_CLOEXEC, 0o666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create /dev/console mount target: %w", err)
|
||||
}
|
||||
defer console.Close()
|
||||
|
||||
dstFd, closer := utils.ProcThreadSelfFd(console.Fd())
|
||||
defer closer()
|
||||
|
||||
mntSrc := &mountSource{
|
||||
Type: mountSourcePlain,
|
||||
file: peerPty,
|
||||
}
|
||||
return mountViaFds(peerPty.Name(), mntSrc, "/dev/console", dstFd, "bind", unix.MS_BIND, "")
|
||||
}
|
||||
|
||||
// dupStdio replaces stdio with the given peerPty.
|
||||
func dupStdio(peerPty *os.File) error {
|
||||
for _, i := range []int{0, 1, 2} {
|
||||
if err := linux.Dup3(fd, i, 0); err != nil {
|
||||
if err := linux.Dup3(int(peerPty.Fd()), i, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
runtime.KeepAlive(peerPty)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -569,8 +569,12 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error {
|
||||
if isOnTmpfs(m.Destination, mounts) {
|
||||
continue
|
||||
}
|
||||
if _, err := createMountpoint(c.config.Rootfs, mountEntry{Mount: m}); err != nil {
|
||||
return fmt.Errorf("create criu restore mountpoint for %s mount: %w", m.Destination, err)
|
||||
me := mountEntry{Mount: m}
|
||||
if err := me.createOpenMountpoint(c.config.Rootfs); err != nil {
|
||||
return fmt.Errorf("create criu restore mountpoint for %s mount: %w", me.Destination, err)
|
||||
}
|
||||
if me.dstFile != nil {
|
||||
defer me.dstFile.Close()
|
||||
}
|
||||
// If the mount point is a bind mount, we need to mount
|
||||
// it now so that runc can create the necessary mount
|
||||
@@ -582,13 +586,20 @@ func (c *Container) prepareCriuRestoreMounts(mounts []*configs.Mount) error {
|
||||
// because during initial container creation mounts are
|
||||
// set up in the order they are configured.
|
||||
if m.Device == "bind" {
|
||||
if err := utils.WithProcfd(c.config.Rootfs, m.Destination, func(dstFd string) error {
|
||||
if err := utils.WithProcfdFile(me.dstFile, func(dstFd string) error {
|
||||
return mountViaFds(m.Source, nil, m.Destination, dstFd, "", unix.MS_BIND|unix.MS_REC, "")
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
umounts = append(umounts, m.Destination)
|
||||
}
|
||||
if me.dstFile != nil {
|
||||
// As this is being done in a loop, the defer earlier will be
|
||||
// delayed until all mountpoints are handled -- for a config with
|
||||
// many mountpoints this could result in a lot of open files. So we
|
||||
// opportunistically close the file as well as deferring it.
|
||||
_ = me.dstFile.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1077,7 +1088,7 @@ func (c *Container) criuNotifications(resp *criurpc.CriuResp, process *Process,
|
||||
logrus.Debugf("notify: %s\n", script)
|
||||
switch script {
|
||||
case "post-dump":
|
||||
f, err := os.Create(filepath.Join(c.stateDir, "checkpoint"))
|
||||
f, err := os.Create(filepath.Join(c.stateDir, "checkpoint")) //nolint:forbidigo // this is a host-side operation in a runc-controlled directory
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/libcontainer/system"
|
||||
)
|
||||
|
||||
@@ -71,7 +72,7 @@ func sealFile(f **os.File) error {
|
||||
// When sealing an O_TMPFILE-style descriptor we need to
|
||||
// re-open the path as O_PATH to clear the existing write
|
||||
// handle we have.
|
||||
opath, err := os.OpenFile(fmt.Sprintf("/proc/self/fd/%d", (*f).Fd()), unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
opath, err := pathrs.Reopen(*f, unix.O_PATH|unix.O_CLOEXEC)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reopen tmpfile: %w", err)
|
||||
}
|
||||
|
||||
+16
-11
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
|
||||
"github.com/opencontainers/cgroups"
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/libcontainer/capabilities"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/system"
|
||||
@@ -377,12 +379,13 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error {
|
||||
// the UID owner of the console to be the user the process will run as (so
|
||||
// they can actually control their console).
|
||||
|
||||
pty, slavePath, err := console.NewPty()
|
||||
pty, peerPty, err := safeAllocPty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// After we return from here, we don't need the console anymore.
|
||||
defer pty.Close()
|
||||
defer peerPty.Close()
|
||||
|
||||
if config.ConsoleHeight != 0 && config.ConsoleWidth != 0 {
|
||||
err = pty.Resize(console.WinSize{
|
||||
@@ -396,7 +399,7 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error {
|
||||
|
||||
// Mount the console inside our rootfs.
|
||||
if mount {
|
||||
if err := mountConsole(slavePath); err != nil {
|
||||
if err := mountConsole(peerPty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -407,7 +410,7 @@ func setupConsole(socket *os.File, config *initConfig, mount bool) error {
|
||||
runtime.KeepAlive(pty)
|
||||
|
||||
// Now, dup over all the things.
|
||||
return dupStdio(slavePath)
|
||||
return dupStdio(peerPty)
|
||||
}
|
||||
|
||||
// syncParentReady sends to the given pipe a JSON payload which indicates that
|
||||
@@ -469,7 +472,12 @@ func setupUser(config *initConfig) error {
|
||||
// We don't need to use /proc/thread-self here because setgroups is a
|
||||
// per-userns file and thus is global to all threads in a thread-group.
|
||||
// This lets us avoid having to do runtime.LockOSThread.
|
||||
setgroups, err := os.ReadFile("/proc/self/setgroups")
|
||||
var setgroups []byte
|
||||
setgroupsFile, err := pathrs.ProcSelfOpen("setgroups", unix.O_RDONLY)
|
||||
if err == nil {
|
||||
setgroups, err = io.ReadAll(setgroupsFile)
|
||||
_ = setgroupsFile.Close()
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
@@ -505,19 +513,16 @@ func setupUser(config *initConfig) error {
|
||||
// The ownership needs to match because it is created outside of the container and needs to be
|
||||
// localized.
|
||||
func fixStdioPermissions(uid int) error {
|
||||
var null unix.Stat_t
|
||||
if err := unix.Stat("/dev/null", &null); err != nil {
|
||||
return &os.PathError{Op: "stat", Path: "/dev/null", Err: err}
|
||||
}
|
||||
for _, file := range []*os.File{os.Stdin, os.Stdout, os.Stderr} {
|
||||
var s unix.Stat_t
|
||||
if err := unix.Fstat(int(file.Fd()), &s); err != nil {
|
||||
return &os.PathError{Op: "fstat", Path: file.Name(), Err: err}
|
||||
}
|
||||
|
||||
// Skip chown if uid is already the one we want or any of the STDIO descriptors
|
||||
// were redirected to /dev/null.
|
||||
if int(s.Uid) == uid || s.Rdev == null.Rdev {
|
||||
// Skip chown if:
|
||||
// - uid is already the one we want, or
|
||||
// - fd is opened to /dev/null.
|
||||
if int(s.Uid) == uid || isDevNull(&s) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ import (
|
||||
|
||||
"github.com/opencontainers/cgroups"
|
||||
"github.com/opencontainers/cgroups/systemd"
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/libcontainer"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/internal/userns"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -1683,11 +1684,9 @@ func TestFdLeaksSystemd(t *testing.T) {
|
||||
}
|
||||
|
||||
func fdList(t *testing.T) []string {
|
||||
procSelfFd, closer := utils.ProcThreadSelf("fd")
|
||||
defer closer()
|
||||
|
||||
fdDir, err := os.Open(procSelfFd)
|
||||
fdDir, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC)
|
||||
ok(t, err)
|
||||
defer closer()
|
||||
defer fdDir.Close()
|
||||
|
||||
fds, err := fdDir.Readdirnames(-1)
|
||||
@@ -1726,8 +1725,10 @@ func testFdLeaks(t *testing.T, systemd bool) {
|
||||
|
||||
count := 0
|
||||
|
||||
procSelfFd, closer := utils.ProcThreadSelf("fd/")
|
||||
procSelfFd, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC)
|
||||
ok(t, err)
|
||||
defer closer()
|
||||
defer procSelfFd.Close()
|
||||
|
||||
next_fd:
|
||||
for _, fd1 := range fds1 {
|
||||
@@ -1736,7 +1737,7 @@ next_fd:
|
||||
continue next_fd
|
||||
}
|
||||
}
|
||||
dst, _ := os.Readlink(filepath.Join(procSelfFd, fd1))
|
||||
dst, _ := linux.Readlinkat(procSelfFd, fd1)
|
||||
for _, ex := range excludedPaths {
|
||||
if ex == dst {
|
||||
continue next_fd
|
||||
|
||||
+294
-136
@@ -5,14 +5,15 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
|
||||
"github.com/moby/sys/mountinfo"
|
||||
"github.com/moby/sys/userns"
|
||||
"github.com/mrunalp/fileutils"
|
||||
@@ -25,6 +26,8 @@ import (
|
||||
devices "github.com/opencontainers/cgroups/devices/config"
|
||||
"github.com/opencontainers/cgroups/fs2"
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/internal/sys"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/utils"
|
||||
)
|
||||
@@ -44,6 +47,7 @@ type mountConfig struct {
|
||||
type mountEntry struct {
|
||||
*configs.Mount
|
||||
srcFile *mountSource
|
||||
dstFile *os.File
|
||||
}
|
||||
|
||||
// srcName is only meant for error messages, it returns a "friendly" name.
|
||||
@@ -295,8 +299,8 @@ func cleanupTmp(tmpdir string) {
|
||||
_ = os.RemoveAll(tmpdir)
|
||||
}
|
||||
|
||||
func mountCgroupV1(m *configs.Mount, c *mountConfig) error {
|
||||
binds, err := getCgroupMounts(m)
|
||||
func mountCgroupV1(m mountEntry, c *mountConfig) error {
|
||||
binds, err := getCgroupMounts(m.Mount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -327,7 +331,7 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error {
|
||||
// inside the tmpfs, so we don't want to resolve symlinks).
|
||||
subsystemPath := filepath.Join(c.root, b.Destination)
|
||||
subsystemName := filepath.Base(b.Destination)
|
||||
if err := utils.MkdirAllInRoot(c.root, subsystemPath, 0o755); err != nil {
|
||||
if err := pathrs.MkdirAllInRoot(c.root, subsystemPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := utils.WithProcfd(c.root, b.Destination, func(dstFd string) error {
|
||||
@@ -366,8 +370,8 @@ func mountCgroupV1(m *configs.Mount, c *mountConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountCgroupV2(m *configs.Mount, c *mountConfig) error {
|
||||
err := utils.WithProcfd(c.root, m.Destination, func(dstFd string) error {
|
||||
func mountCgroupV2(m mountEntry, c *mountConfig) error {
|
||||
err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
|
||||
return mountViaFds(m.Source, nil, m.Destination, dstFd, "cgroup2", uintptr(m.Flags), m.Data)
|
||||
})
|
||||
if err == nil || (!errors.Is(err, unix.EPERM) && !errors.Is(err, unix.EBUSY)) {
|
||||
@@ -396,14 +400,14 @@ func mountCgroupV2(m *configs.Mount, c *mountConfig) error {
|
||||
//
|
||||
// Mask `/sys/fs/cgroup` to ensure it is read-only, even when `/sys` is mounted
|
||||
// with `rbind,ro` (`runc spec --rootless` produces `rbind,ro` for `/sys`).
|
||||
err = utils.WithProcfd(c.root, m.Destination, func(procfd string) error {
|
||||
return maskPath(procfd, c.label)
|
||||
err = utils.WithProcfdFile(m.dstFile, func(procfd string) error {
|
||||
return maskPaths([]string{procfd}, c.label)
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func doTmpfsCopyUp(m mountEntry, rootfs, mountLabel string) (Err error) {
|
||||
func doTmpfsCopyUp(m mountEntry, mountLabel string) (Err error) {
|
||||
// Set up a scratch dir for the tmpfs on the host.
|
||||
tmpdir, err := prepareTmp("/tmp")
|
||||
if err != nil {
|
||||
@@ -416,13 +420,19 @@ func doTmpfsCopyUp(m mountEntry, rootfs, mountLabel string) (Err error) {
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Configure the *host* tmpdir as if it's the container mount. We change
|
||||
// m.Destination since we are going to mount *on the host*.
|
||||
oldDest := m.Destination
|
||||
m.Destination = tmpDir
|
||||
err = mountPropagate(m, "/", mountLabel)
|
||||
m.Destination = oldDest
|
||||
tmpDirFile, err := os.OpenFile(tmpDir, unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tmpcopyup: %w", err)
|
||||
}
|
||||
defer tmpDirFile.Close()
|
||||
|
||||
// Configure the *host* tmpdir as if it's the container mount. We change
|
||||
// m.dstFile since we are going to mount *on the host*.
|
||||
hostMount := mountEntry{
|
||||
Mount: m.Mount,
|
||||
dstFile: tmpDirFile,
|
||||
}
|
||||
if err := hostMount.mountPropagate("/", mountLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
@@ -433,7 +443,7 @@ func doTmpfsCopyUp(m mountEntry, rootfs, mountLabel string) (Err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
return utils.WithProcfd(rootfs, m.Destination, func(dstFd string) (Err error) {
|
||||
return utils.WithProcfdFile(m.dstFile, func(dstFd string) (Err error) {
|
||||
// Copy the container data to the host tmpdir. We append "/" to force
|
||||
// CopyDirectory to resolve the symlink rather than trying to copy the
|
||||
// symlink itself.
|
||||
@@ -495,72 +505,87 @@ func statfsToMountFlags(st unix.Statfs_t) int {
|
||||
|
||||
var errRootfsToFile = errors.New("config tries to change rootfs to file")
|
||||
|
||||
func createMountpoint(rootfs string, m mountEntry) (string, error) {
|
||||
dest, err := securejoin.SecureJoin(rootfs, m.Destination)
|
||||
func (m *mountEntry) createOpenMountpoint(rootfs string) (Err error) {
|
||||
unsafePath := utils.StripRoot(rootfs, m.Destination)
|
||||
dstFile, err := pathrs.OpenInRoot(rootfs, unsafePath, unix.O_PATH)
|
||||
defer func() {
|
||||
if dstFile != nil && Err != nil {
|
||||
_ = dstFile.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := checkProcMount(rootfs, dest, m); err != nil {
|
||||
return "", fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err)
|
||||
}
|
||||
|
||||
switch m.Device {
|
||||
case "bind":
|
||||
fi, _, err := m.srcStat()
|
||||
if err != nil {
|
||||
// Error out if the source of a bind mount does not exist as we
|
||||
// will be unable to bind anything to it.
|
||||
return "", err
|
||||
if !errors.Is(err, unix.ENOENT) {
|
||||
return fmt.Errorf("lookup mountpoint target: %w", err)
|
||||
}
|
||||
// If the original source is not a directory, make the target a file.
|
||||
if !fi.IsDir() {
|
||||
// Make sure we aren't tricked into trying to make the root a file.
|
||||
if rootfs == dest {
|
||||
return "", fmt.Errorf("%w: file bind mount over rootfs", errRootfsToFile)
|
||||
}
|
||||
// Make the parent directory.
|
||||
destDir, destBase := filepath.Split(dest)
|
||||
destDirFd, err := utils.MkdirAllInRootOpen(rootfs, destDir, 0o755)
|
||||
|
||||
// If the mountpoint doesn't already exist, we want to create a mountpoint
|
||||
// that makes sense for the source. For file bind-mounts this is an empty
|
||||
// file, for everything else it's a directory.
|
||||
dstIsFile := false
|
||||
if m.Device == "bind" {
|
||||
fi, _, err := m.srcStat()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("make parent dir of file bind-mount: %w", err)
|
||||
// Error out if the source of a bind mount does not exist as we
|
||||
// will be unable to bind anything to it.
|
||||
return err
|
||||
}
|
||||
defer destDirFd.Close()
|
||||
// Make the target file. We want to avoid opening any file that is
|
||||
// already there because it could be a "bad" file like an invalid
|
||||
// device or hung tty that might cause a DoS, so we use mknodat.
|
||||
// destBase does not contain any "/" components, and mknodat does
|
||||
// not follow trailing symlinks, so we can safely just call mknodat
|
||||
// here.
|
||||
if err := unix.Mknodat(int(destDirFd.Fd()), destBase, unix.S_IFREG|0o644, 0); err != nil {
|
||||
// If we get EEXIST, there was already an inode there and
|
||||
// we can consider that a success.
|
||||
if !errors.Is(err, unix.EEXIST) {
|
||||
err = &os.PathError{Op: "mknod regular file", Path: dest, Err: err}
|
||||
return "", fmt.Errorf("create target of file bind-mount: %w", err)
|
||||
}
|
||||
}
|
||||
// Nothing left to do.
|
||||
return dest, nil
|
||||
dstIsFile = !fi.IsDir()
|
||||
}
|
||||
|
||||
case "tmpfs":
|
||||
// In previous runc versions, we would tolerate nonsense paths with
|
||||
// dangling symlinks as path components. pathrs-lite does not support
|
||||
// this, so instead we have to emulate this behaviour by doing
|
||||
// SecureJoin *purely to get a semi-reasonable path to use* and then we
|
||||
// use pathrs-lite to operate on the path safely.
|
||||
newUnsafePath, err := securejoin.SecureJoin(rootfs, unsafePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unsafePath = utils.StripRoot(rootfs, newUnsafePath)
|
||||
|
||||
if dstIsFile {
|
||||
dstFile, err = pathrs.CreateInRoot(rootfs, unsafePath, unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW, 0o644)
|
||||
} else {
|
||||
dstFile, err = pathrs.MkdirAllInRootOpen(rootfs, unsafePath, 0o755)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("make mountpoint %q: %w", m.Destination, err)
|
||||
}
|
||||
}
|
||||
|
||||
if m.Device == "tmpfs" {
|
||||
// If the original target exists, copy the mode for the tmpfs mount.
|
||||
if stat, err := os.Stat(dest); err == nil {
|
||||
dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode()))
|
||||
if m.Data != "" {
|
||||
dt = dt + "," + m.Data
|
||||
}
|
||||
m.Data = dt
|
||||
|
||||
// Nothing left to do.
|
||||
return dest, nil
|
||||
stat, err := dstFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("check tmpfs source mode: %w", err)
|
||||
}
|
||||
dt := fmt.Sprintf("mode=%04o", syscallMode(stat.Mode()))
|
||||
if m.Data != "" {
|
||||
dt = dt + "," + m.Data
|
||||
}
|
||||
m.Data = dt
|
||||
}
|
||||
|
||||
if err := utils.MkdirAllInRoot(rootfs, dest, 0o755); err != nil {
|
||||
return "", err
|
||||
dstFullPath, err := procfs.ProcSelfFdReadlink(dstFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get mount destination real path: %w", err)
|
||||
}
|
||||
return dest, nil
|
||||
if !pathrs.IsLexicallyInRoot(rootfs, dstFullPath) {
|
||||
return fmt.Errorf("mountpoint %q is outside of rootfs %q", dstFullPath, rootfs)
|
||||
}
|
||||
if relPath, err := filepath.Rel(rootfs, dstFullPath); err != nil {
|
||||
return fmt.Errorf("get relative path of %q: %w", dstFullPath, err)
|
||||
} else if relPath == "." {
|
||||
return fmt.Errorf("mountpoint %q is on the top of rootfs %q", dstFullPath, rootfs)
|
||||
}
|
||||
// TODO: Make checkProcMount use dstFile directly to avoid the need to
|
||||
// operate on paths here.
|
||||
if err := checkProcMount(rootfs, dstFullPath, *m); err != nil {
|
||||
return fmt.Errorf("check proc-safety of %s mount: %w", m.Destination, err)
|
||||
}
|
||||
// Update mountEntry.
|
||||
m.dstFile = dstFile
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
@@ -576,7 +601,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// TODO: This won't be necessary once we switch to libpathrs and we can
|
||||
// stop all of these symlink-exchange attacks.
|
||||
dest := filepath.Clean(m.Destination)
|
||||
if !utils.IsLexicallyInRoot(rootfs, dest) {
|
||||
if !pathrs.IsLexicallyInRoot(rootfs, dest) {
|
||||
// Do not use securejoin as it resolves symlinks.
|
||||
dest = filepath.Join(rootfs, dest)
|
||||
}
|
||||
@@ -590,36 +615,47 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
} else if !fi.IsDir() {
|
||||
return fmt.Errorf("filesystem %q must be mounted on ordinary directory", m.Device)
|
||||
}
|
||||
if err := utils.MkdirAllInRoot(rootfs, dest, 0o755); err != nil {
|
||||
dstFile, err := pathrs.MkdirAllInRootOpen(rootfs, dest, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Selinux kernels do not support labeling of /proc or /sys.
|
||||
return mountPropagate(m, rootfs, "")
|
||||
defer dstFile.Close()
|
||||
// "proc" and "sys" mounts need special handling (without resolving the
|
||||
// destination) to avoid attacks.
|
||||
m.dstFile = dstFile
|
||||
return m.mountPropagate(rootfs, "")
|
||||
}
|
||||
|
||||
dest, err := createMountpoint(rootfs, m)
|
||||
if err != nil {
|
||||
mountLabel := c.label
|
||||
if err := m.createOpenMountpoint(rootfs); err != nil {
|
||||
return fmt.Errorf("create mountpoint for %s mount: %w", m.Destination, err)
|
||||
}
|
||||
mountLabel := c.label
|
||||
defer func() {
|
||||
if m.dstFile != nil {
|
||||
_ = m.dstFile.Close()
|
||||
m.dstFile = nil
|
||||
}
|
||||
}()
|
||||
|
||||
switch m.Device {
|
||||
case "mqueue":
|
||||
if err := mountPropagate(m, rootfs, ""); err != nil {
|
||||
if err := m.mountPropagate(rootfs, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
return label.SetFileLabel(dest, mountLabel)
|
||||
return utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
|
||||
return label.SetFileLabel(dstFd, mountLabel)
|
||||
})
|
||||
case "tmpfs":
|
||||
var err error
|
||||
if m.Extensions&configs.EXT_COPYUP == configs.EXT_COPYUP {
|
||||
err = doTmpfsCopyUp(m, rootfs, mountLabel)
|
||||
err = doTmpfsCopyUp(m, mountLabel)
|
||||
} else {
|
||||
err = mountPropagate(m, rootfs, mountLabel)
|
||||
err = m.mountPropagate(rootfs, mountLabel)
|
||||
}
|
||||
|
||||
return err
|
||||
case "bind":
|
||||
// open_tree()-related shenanigans are all handled in mountViaFds.
|
||||
if err := mountPropagate(m, rootfs, mountLabel); err != nil {
|
||||
if err := m.mountPropagate(rootfs, mountLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -633,7 +669,7 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
// contrast to mount(8)'s current behaviour, but is what users probably
|
||||
// expect. See <https://github.com/util-linux/util-linux/issues/2433>.
|
||||
if m.Flags & ^(unix.MS_BIND|unix.MS_REC|unix.MS_REMOUNT) != 0 || m.ClearedFlags != 0 {
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error {
|
||||
if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
|
||||
flags := m.Flags | unix.MS_BIND | unix.MS_REMOUNT
|
||||
// The runtime-spec says we SHOULD map to the relevant mount(8)
|
||||
// behaviour. However, it's not clear whether we want the
|
||||
@@ -734,14 +770,14 @@ func mountToRootfs(c *mountConfig, m mountEntry) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return setRecAttr(m.Mount, rootfs)
|
||||
return setRecAttr(m)
|
||||
case "cgroup":
|
||||
if cgroups.IsCgroup2UnifiedMode() {
|
||||
return mountCgroupV2(m.Mount, c)
|
||||
return mountCgroupV2(m, c)
|
||||
}
|
||||
return mountCgroupV1(m.Mount, c)
|
||||
return mountCgroupV1(m, c)
|
||||
default:
|
||||
return mountPropagate(m, rootfs, mountLabel)
|
||||
return m.mountPropagate(rootfs, mountLabel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,20 +924,20 @@ func setupDevSymlinks(rootfs string) error {
|
||||
// needs to be called after we chroot/pivot into the container's rootfs so that any
|
||||
// symlinks are resolved locally.
|
||||
func reOpenDevNull() error {
|
||||
var stat, devNullStat unix.Stat_t
|
||||
file, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := unix.Fstat(int(file.Fd()), &devNullStat); err != nil {
|
||||
return &os.PathError{Op: "fstat", Path: file.Name(), Err: err}
|
||||
if err := verifyDevNull(file); err != nil {
|
||||
return fmt.Errorf("can't reopen /dev/null: %w", err)
|
||||
}
|
||||
for fd := range 3 {
|
||||
var stat unix.Stat_t
|
||||
if err := unix.Fstat(fd, &stat); err != nil {
|
||||
return &os.PathError{Op: "fstat", Path: "fd " + strconv.Itoa(fd), Err: err}
|
||||
}
|
||||
if stat.Rdev == devNullStat.Rdev {
|
||||
if isDevNull(&stat) {
|
||||
// Close and re-open the fd.
|
||||
if err := linux.Dup3(int(file.Fd()), fd, 0); err != nil {
|
||||
return err
|
||||
@@ -930,16 +966,15 @@ func createDevices(config *configs.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindMountDeviceNode(rootfs, dest string, node *devices.Device) error {
|
||||
f, err := os.Create(dest)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
func bindMountDeviceNode(destDir *os.File, destName string, node *devices.Device) error {
|
||||
dstFile, err := utils.Openat(destDir, destName, unix.O_CREAT|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o000)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create device inode %s: %w", node.Path, err)
|
||||
}
|
||||
if f != nil {
|
||||
_ = f.Close()
|
||||
}
|
||||
return utils.WithProcfd(rootfs, dest, func(dstFd string) error {
|
||||
return mountViaFds(node.Path, nil, dest, dstFd, "bind", unix.MS_BIND, "")
|
||||
defer dstFile.Close()
|
||||
|
||||
return utils.WithProcfdFile(dstFile, func(dstFd string) error {
|
||||
return mountViaFds(node.Path, nil, dstFile.Name(), dstFd, "bind", unix.MS_BIND, "")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -949,31 +984,33 @@ func createDeviceNode(rootfs string, node *devices.Device, bind bool) error {
|
||||
// The node only exists for cgroup reasons, ignore it here.
|
||||
return nil
|
||||
}
|
||||
dest, err := securejoin.SecureJoin(rootfs, node.Path)
|
||||
destPath, err := securejoin.SecureJoin(rootfs, node.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dest == rootfs {
|
||||
if destPath == rootfs {
|
||||
return fmt.Errorf("%w: mknod over rootfs", errRootfsToFile)
|
||||
}
|
||||
if err := utils.MkdirAllInRoot(rootfs, filepath.Dir(dest), 0o755); err != nil {
|
||||
return err
|
||||
destDirPath, destName := filepath.Split(destPath)
|
||||
destDir, err := pathrs.MkdirAllInRootOpen(rootfs, destDirPath, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mkdir parent of device inode %q: %w", node.Path, err)
|
||||
}
|
||||
if bind {
|
||||
return bindMountDeviceNode(rootfs, dest, node)
|
||||
return bindMountDeviceNode(destDir, destName, node)
|
||||
}
|
||||
if err := mknodDevice(dest, node); err != nil {
|
||||
if err := mknodDevice(destDir, destName, node); err != nil {
|
||||
if errors.Is(err, os.ErrExist) {
|
||||
return nil
|
||||
} else if errors.Is(err, os.ErrPermission) {
|
||||
return bindMountDeviceNode(rootfs, dest, node)
|
||||
return bindMountDeviceNode(destDir, destName, node)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mknodDevice(dest string, node *devices.Device) error {
|
||||
func mknodDevice(destDir *os.File, destName string, node *devices.Device) error {
|
||||
fileMode := node.FileMode
|
||||
switch node.Type {
|
||||
case devices.BlockDevice:
|
||||
@@ -989,14 +1026,44 @@ func mknodDevice(dest string, node *devices.Device) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unix.Mknod(dest, uint32(fileMode), int(dev)); err != nil {
|
||||
return &os.PathError{Op: "mknod", Path: dest, Err: err}
|
||||
if err := unix.Mknodat(int(destDir.Fd()), destName, uint32(fileMode), int(dev)); err != nil {
|
||||
return &os.PathError{Op: "mknodat", Path: filepath.Join(destDir.Name(), destName), Err: err}
|
||||
}
|
||||
// Ensure permission bits (can be different because of umask).
|
||||
if err := os.Chmod(dest, fileMode); err != nil {
|
||||
|
||||
// Get a handle and verify that it matches the expected inode type and
|
||||
// major:minor before we operate on it.
|
||||
devFile, err := utils.Openat(destDir, destName, unix.O_NOFOLLOW|unix.O_PATH, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open new %c device inode %s: %w", node.Type, node.Path, err)
|
||||
}
|
||||
defer devFile.Close()
|
||||
|
||||
if err := sys.VerifyInode(devFile, func(stat *unix.Stat_t, _ *unix.Statfs_t) error {
|
||||
if stat.Mode&unix.S_IFMT != uint32(fileMode)&unix.S_IFMT {
|
||||
return fmt.Errorf("new %c device inode %s has incorrect ftype: %#x doesn't match expected %#v",
|
||||
node.Type, node.Path,
|
||||
stat.Mode&unix.S_IFMT, fileMode&unix.S_IFMT)
|
||||
}
|
||||
if stat.Rdev != dev {
|
||||
return fmt.Errorf("new %c device inode %s has incorrect major:minor: %d:%d doesn't match expected %d:%d",
|
||||
node.Type, node.Path,
|
||||
unix.Major(stat.Rdev), unix.Minor(stat.Rdev),
|
||||
unix.Major(dev), unix.Minor(dev))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chown(dest, int(node.Uid), int(node.Gid))
|
||||
|
||||
// Ensure permission bits (can be different because of umask).
|
||||
if err := sys.FchmodFile(devFile, uint32(fileMode)); err != nil {
|
||||
return fmt.Errorf("update new %c device inode %s file mode: %w", node.Type, node.Path, err)
|
||||
}
|
||||
if err := sys.FchownFile(devFile, int(node.Uid), int(node.Gid)); err != nil {
|
||||
return fmt.Errorf("update new %c device inode %s owner: %w", node.Type, node.Path, err)
|
||||
}
|
||||
runtime.KeepAlive(devFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// rootfsParentMountPrivate ensures rootfs parent mount is private.
|
||||
@@ -1250,31 +1317,111 @@ func remountReadonly(m *configs.Mount) error {
|
||||
return fmt.Errorf("unable to mount %s as readonly max retries reached", dest)
|
||||
}
|
||||
|
||||
// maskPath masks the top of the specified path inside a container to avoid
|
||||
func isDevNull(st *unix.Stat_t) bool {
|
||||
return st.Mode&unix.S_IFMT == unix.S_IFCHR && st.Rdev == unix.Mkdev(1, 3)
|
||||
}
|
||||
|
||||
func verifyDevNull(f *os.File) error {
|
||||
return sys.VerifyInode(f, func(st *unix.Stat_t, _ *unix.Statfs_t) error {
|
||||
if !isDevNull(st) {
|
||||
return errors.New("container's /dev/null is invalid")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// maskPaths masks the top of the specified paths inside a container to avoid
|
||||
// security issues from processes reading information from non-namespace aware
|
||||
// mounts ( proc/kcore ).
|
||||
// For files, maskPath bind mounts /dev/null over the top of the specified path.
|
||||
// For directories, maskPath mounts read-only tmpfs over the top of the specified path.
|
||||
func maskPath(path string, mountLabel string) error {
|
||||
if err := mount("/dev/null", path, "", unix.MS_BIND, ""); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
if errors.Is(err, unix.ENOTDIR) {
|
||||
return mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel))
|
||||
}
|
||||
return err
|
||||
func maskPaths(paths []string, mountLabel string) error {
|
||||
devNull, err := os.OpenFile("/dev/null", unix.O_PATH, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't mask paths: %w", err)
|
||||
}
|
||||
defer devNull.Close()
|
||||
if err := verifyDevNull(devNull); err != nil {
|
||||
return fmt.Errorf("can't mask paths: %w", err)
|
||||
}
|
||||
devNullSrc := &mountSource{Type: mountSourcePlain, file: devNull}
|
||||
procSelfFd, closer := utils.ProcThreadSelf("fd/")
|
||||
defer closer()
|
||||
|
||||
for _, path := range paths {
|
||||
// Open the target path; skip if it doesn't exist.
|
||||
dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("can't mask path %q: %w", path, err)
|
||||
}
|
||||
st, err := dstFh.Stat()
|
||||
if err != nil {
|
||||
dstFh.Close()
|
||||
return fmt.Errorf("can't mask path %q: %w", path, err)
|
||||
}
|
||||
var dstType string
|
||||
if st.IsDir() {
|
||||
// Destination is a directory: bind mount a ro tmpfs over it.
|
||||
dstType = "dir"
|
||||
err = mount("tmpfs", path, "tmpfs", unix.MS_RDONLY, label.FormatMountLabel("", mountLabel))
|
||||
} else {
|
||||
// Destination is a file: mount it to /dev/null.
|
||||
dstType = "path"
|
||||
dstFd := filepath.Join(procSelfFd, strconv.Itoa(int(dstFh.Fd())))
|
||||
err = mountViaFds("", devNullSrc, path, dstFd, "", unix.MS_BIND, "")
|
||||
}
|
||||
dstFh.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't mask %s %q: %w", dstType, path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeSystemProperty writes the value to a path under /proc/sys as determined from the key.
|
||||
// For e.g. net.ipv4.ip_forward translated to /proc/sys/net/ipv4/ip_forward.
|
||||
func writeSystemProperty(key, value string) error {
|
||||
keyPath := strings.ReplaceAll(key, ".", "/")
|
||||
return os.WriteFile(path.Join("/proc/sys", keyPath), []byte(value), 0o644)
|
||||
func reopenAfterMount(rootfs string, f *os.File, flags int) (_ *os.File, Err error) {
|
||||
fullPath, err := procfs.ProcSelfFdReadlink(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get full path: %w", err)
|
||||
}
|
||||
if !pathrs.IsLexicallyInRoot(rootfs, fullPath) {
|
||||
return nil, fmt.Errorf("mountpoint %q is outside of rootfs %q", fullPath, rootfs)
|
||||
}
|
||||
unsafePath := utils.StripRoot(rootfs, fullPath)
|
||||
reopened, err := pathrs.OpenInRoot(rootfs, unsafePath, flags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("re-open mountpoint %q: %w", unsafePath, err)
|
||||
}
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = reopened.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// NOTE: The best we can do here is confirm that the new mountpoint handle
|
||||
// matches the original target handle, but an attacker could've swapped a
|
||||
// different path to replace it. In the worst case this could result in us
|
||||
// applying later vfsmount flags onto the wrong mount.
|
||||
//
|
||||
// This is far from ideal, but the only way of doing this in a race-free
|
||||
// way is to switch the new mount API (move_mount(2) does not require this
|
||||
// re-opening step, and thus no such races are possible).
|
||||
reopenedFullPath, err := procfs.ProcSelfFdReadlink(reopened)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check full path of re-opened mountpoint: %w", err)
|
||||
}
|
||||
if reopenedFullPath != fullPath {
|
||||
return nil, fmt.Errorf("mountpoint %q was moved while re-opening", unsafePath)
|
||||
}
|
||||
return reopened, nil
|
||||
}
|
||||
|
||||
// Do the mount operation followed by additional mounts required to take care
|
||||
// of propagation flags. This will always be scoped inside the container rootfs.
|
||||
func mountPropagate(m mountEntry, rootfs string, mountLabel string) error {
|
||||
func (m *mountEntry) mountPropagate(rootfs string, mountLabel string) error {
|
||||
var (
|
||||
data = label.FormatMountLabel(m.Data, mountLabel)
|
||||
flags = m.Flags
|
||||
@@ -1287,19 +1434,30 @@ func mountPropagate(m mountEntry, rootfs string, mountLabel string) error {
|
||||
flags &= ^unix.MS_RDONLY
|
||||
}
|
||||
|
||||
// Because the destination is inside a container path which might be
|
||||
// mutating underneath us, we verify that we are actually going to mount
|
||||
// inside the container with WithProcfd() -- mounting through a procfd
|
||||
// mounts on the target.
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error {
|
||||
if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
|
||||
return mountViaFds(m.Source, m.srcFile, m.Destination, dstFd, m.Device, uintptr(flags), data)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We need to re-open the mountpoint after doing the mount, in order for us
|
||||
// to operate on the new mount we just created. However, we cannot use
|
||||
// pathrs.Reopen because we need to re-resolve from the parent directory to
|
||||
// get a new handle to the top mount.
|
||||
//
|
||||
// TODO: Use move_mount(2) on newer kernels so that this is no longer
|
||||
// necessary on modern systems.
|
||||
newDstFile, err := reopenAfterMount(rootfs, m.dstFile, unix.O_PATH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reopen mountpoint after mount: %w", err)
|
||||
}
|
||||
_ = m.dstFile.Close()
|
||||
m.dstFile = newDstFile
|
||||
|
||||
// We have to apply mount propagation flags in a separate WithProcfd() call
|
||||
// because the previous call invalidates the passed procfd -- the mount
|
||||
// target needs to be re-opened.
|
||||
if err := utils.WithProcfd(rootfs, m.Destination, func(dstFd string) error {
|
||||
if err := utils.WithProcfdFile(m.dstFile, func(dstFd string) error {
|
||||
for _, pflag := range m.PropagationFlags {
|
||||
if err := mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), ""); err != nil {
|
||||
return err
|
||||
@@ -1312,11 +1470,11 @@ func mountPropagate(m mountEntry, rootfs string, mountLabel string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRecAttr(m *configs.Mount, rootfs string) error {
|
||||
func setRecAttr(m mountEntry) error {
|
||||
if m.RecAttr == nil {
|
||||
return nil
|
||||
}
|
||||
return utils.WithProcfd(rootfs, m.Destination, func(procfd string) error {
|
||||
return utils.WithProcfdFile(m.dstFile, func(procfd string) error {
|
||||
return unix.MountSetattr(-1, procfd, unix.AT_RECURSIVE, m.RecAttr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/opencontainers/runc/internal/sys"
|
||||
"github.com/opencontainers/runc/libcontainer/apparmor"
|
||||
"github.com/opencontainers/runc/libcontainer/configs"
|
||||
"github.com/opencontainers/runc/libcontainer/keys"
|
||||
@@ -131,20 +133,17 @@ func (l *linuxStandardInit) Init() error {
|
||||
return fmt.Errorf("unable to apply apparmor profile: %w", err)
|
||||
}
|
||||
|
||||
for key, value := range l.config.Config.Sysctl {
|
||||
if err := writeSystemProperty(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sys.WriteSysctls(l.config.Config.Sysctl); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, path := range l.config.Config.ReadonlyPaths {
|
||||
if err := readonlyPath(path); err != nil {
|
||||
return fmt.Errorf("can't make %q read-only: %w", path, err)
|
||||
}
|
||||
}
|
||||
for _, path := range l.config.Config.MaskPaths {
|
||||
if err := maskPath(path, l.config.Config.MountLabel); err != nil {
|
||||
return fmt.Errorf("can't mask path %s: %w", path, err)
|
||||
}
|
||||
|
||||
if err := maskPaths(l.config.Config.MaskPaths, l.config.Config.MountLabel); err != nil {
|
||||
return err
|
||||
}
|
||||
pdeath, err := system.GetParentDeathSignal()
|
||||
if err != nil {
|
||||
@@ -259,19 +258,17 @@ func (l *linuxStandardInit) Init() error {
|
||||
return fmt.Errorf("close log pipe: %w", err)
|
||||
}
|
||||
|
||||
fifoPath, closer := utils.ProcThreadSelfFd(l.fifoFile.Fd())
|
||||
defer closer()
|
||||
|
||||
// Wait for the FIFO to be opened on the other side before exec-ing the
|
||||
// user process. We open it through /proc/self/fd/$fd, because the fd that
|
||||
// was given to us was an O_PATH fd to the fifo itself. Linux allows us to
|
||||
// re-open an O_PATH fd through /proc.
|
||||
fd, err := linux.Open(fifoPath, unix.O_WRONLY|unix.O_CLOEXEC, 0)
|
||||
fifoFile, err := pathrs.Reopen(l.fifoFile, unix.O_WRONLY|unix.O_CLOEXEC)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("reopen exec fifo: %w", err)
|
||||
}
|
||||
if _, err := unix.Write(fd, []byte("0")); err != nil {
|
||||
return &os.PathError{Op: "write exec fifo", Path: fifoPath, Err: err}
|
||||
defer fifoFile.Close()
|
||||
if _, err := fifoFile.Write([]byte("0")); err != nil {
|
||||
return &os.PathError{Op: "write exec fifo", Path: fifoFile.Name(), Err: err}
|
||||
}
|
||||
|
||||
// Close the O_PATH fifofd fd before exec because the kernel resets
|
||||
@@ -280,6 +277,7 @@ func (l *linuxStandardInit) Init() error {
|
||||
// N.B. the core issue itself (passing dirfds to the host filesystem) has
|
||||
// since been resolved.
|
||||
// https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318
|
||||
_ = fifoFile.Close()
|
||||
_ = l.fifoFile.Close()
|
||||
|
||||
if s := l.config.SpecState; s != nil {
|
||||
|
||||
@@ -160,3 +160,23 @@ func SetLinuxPersonality(personality int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPtyPeer is a wrapper for ioctl(TIOCGPTPEER).
|
||||
func GetPtyPeer(ptyFd uintptr, unsafePeerPath string, flags int) (*os.File, error) {
|
||||
// Make sure O_NOCTTY is always set -- otherwise runc might accidentally
|
||||
// gain it as a controlling terminal. O_CLOEXEC also needs to be set to
|
||||
// make sure we don't leak the handle either.
|
||||
flags |= unix.O_NOCTTY | unix.O_CLOEXEC
|
||||
|
||||
// There is no nice wrapper for this kind of ioctl in unix.
|
||||
peerFd, _, errno := unix.Syscall(
|
||||
unix.SYS_IOCTL,
|
||||
ptyFd,
|
||||
uintptr(unix.TIOCGPTPEER),
|
||||
uintptr(flags),
|
||||
)
|
||||
if errno != 0 {
|
||||
return nil, os.NewSyscallError("ioctl TIOCGPTPEER", errno)
|
||||
}
|
||||
return os.NewFile(peerFd, unsafePeerPath), nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
)
|
||||
|
||||
// State is the status of a process.
|
||||
@@ -66,8 +68,16 @@ type Stat_t struct {
|
||||
}
|
||||
|
||||
// Stat returns a Stat_t instance for the specified process.
|
||||
func Stat(pid int) (stat Stat_t, err error) {
|
||||
bytes, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "stat"))
|
||||
func Stat(pid int) (Stat_t, error) {
|
||||
var stat Stat_t
|
||||
|
||||
statFile, err := pathrs.ProcPidOpen(pid, "stat", os.O_RDONLY)
|
||||
if err != nil {
|
||||
return stat, err
|
||||
}
|
||||
defer statFile.Close()
|
||||
|
||||
bytes, err := io.ReadAll(statFile)
|
||||
if err != nil {
|
||||
return stat, err
|
||||
}
|
||||
|
||||
@@ -65,11 +65,11 @@ func CleanPath(path string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
// stripRoot returns the passed path, stripping the root path if it was
|
||||
// StripRoot returns the passed path, stripping the root path if it was
|
||||
// (lexicially) inside it. Note that both passed paths will always be treated
|
||||
// as absolute, and the returned path will also always be absolute. In
|
||||
// addition, the paths are cleaned before stripping the root.
|
||||
func stripRoot(root, path string) string {
|
||||
func StripRoot(root, path string) string {
|
||||
// Make the paths clean and absolute.
|
||||
root, path = CleanPath("/"+root), CleanPath("/"+path)
|
||||
switch {
|
||||
|
||||
@@ -131,9 +131,9 @@ func TestStripRoot(t *testing.T) {
|
||||
{"/foo/bar", "foo/bar/baz/beef", "/baz/beef"},
|
||||
{"foo/bar", "foo/bar/baz/beets", "/baz/beets"},
|
||||
} {
|
||||
got := stripRoot(test.root, test.path)
|
||||
got := StripRoot(test.root, test.path)
|
||||
if got != test.out {
|
||||
t.Errorf("stripRoot(%q, %q) -- got %q, expected %q", test.root, test.path, got, test.out)
|
||||
t.Errorf("StripRoot(%q, %q) -- got %q, expected %q", test.root, test.path, got, test.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,28 +9,16 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/opencontainers/runc/internal/linux"
|
||||
"github.com/opencontainers/runc/internal/pathrs"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// EnsureProcHandle returns whether or not the given file handle is on procfs.
|
||||
func EnsureProcHandle(fh *os.File) error {
|
||||
var buf unix.Statfs_t
|
||||
if err := unix.Fstatfs(int(fh.Fd()), &buf); err != nil {
|
||||
return fmt.Errorf("ensure %s is on procfs: %w", fh.Name(), err)
|
||||
}
|
||||
if buf.Type != unix.PROC_SUPER_MAGIC {
|
||||
return fmt.Errorf("%s is not on procfs", fh.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
haveCloseRangeCloexecBool bool
|
||||
haveCloseRangeCloexecOnce sync.Once
|
||||
@@ -60,19 +48,13 @@ type fdFunc func(fd int)
|
||||
// fdRangeFrom calls the passed fdFunc for each file descriptor that is open in
|
||||
// the current process.
|
||||
func fdRangeFrom(minFd int, fn fdFunc) error {
|
||||
procSelfFd, closer := ProcThreadSelf("fd")
|
||||
defer closer()
|
||||
|
||||
fdDir, err := os.Open(procSelfFd)
|
||||
fdDir, closer, err := pathrs.ProcThreadSelfOpen("fd/", unix.O_DIRECTORY|unix.O_CLOEXEC)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("get handle to /proc/thread-self/fd: %w", err)
|
||||
}
|
||||
defer closer()
|
||||
defer fdDir.Close()
|
||||
|
||||
if err := EnsureProcHandle(fdDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fdList, err := fdDir.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -171,8 +153,8 @@ func NewSockPair(name string) (parent, child *os.File, err error) {
|
||||
// the passed closure (the file handle will be freed once the closure returns).
|
||||
func WithProcfd(root, unsafePath string, fn func(procfd string) error) error {
|
||||
// Remove the root then forcefully resolve inside the root.
|
||||
unsafePath = stripRoot(root, unsafePath)
|
||||
path, err := securejoin.SecureJoin(root, unsafePath)
|
||||
unsafePath = StripRoot(root, unsafePath)
|
||||
fullPath, err := securejoin.SecureJoin(root, unsafePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving path inside rootfs failed: %w", err)
|
||||
}
|
||||
@@ -181,7 +163,7 @@ func WithProcfd(root, unsafePath string, fn func(procfd string) error) error {
|
||||
defer closer()
|
||||
|
||||
// Open the target path.
|
||||
fh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
fh, err := os.OpenFile(fullPath, unix.O_PATH|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open o_path procfd: %w", err)
|
||||
}
|
||||
@@ -191,13 +173,24 @@ func WithProcfd(root, unsafePath string, fn func(procfd string) error) error {
|
||||
// Double-check the path is the one we expected.
|
||||
if realpath, err := os.Readlink(procfd); err != nil {
|
||||
return fmt.Errorf("procfd verification failed: %w", err)
|
||||
} else if realpath != path {
|
||||
} else if realpath != fullPath {
|
||||
return fmt.Errorf("possibly malicious path detected -- refusing to operate on %s", realpath)
|
||||
}
|
||||
|
||||
return fn(procfd)
|
||||
}
|
||||
|
||||
// WithProcfdFile is a very minimal wrapper around [ProcThreadSelfFd], intended
|
||||
// to make migrating from [WithProcfd] and [WithProcfdPath] usage easier. The
|
||||
// caller is responsible for making sure that the provided file handle is
|
||||
// actually safe to operate on.
|
||||
func WithProcfdFile(file *os.File, fn func(procfd string) error) error {
|
||||
fdpath, closer := ProcThreadSelfFd(file.Fd())
|
||||
defer closer()
|
||||
|
||||
return fn(fdpath)
|
||||
}
|
||||
|
||||
type ProcThreadSelfCloser func()
|
||||
|
||||
var (
|
||||
@@ -269,88 +262,6 @@ func ProcThreadSelfFd(fd uintptr) (string, ProcThreadSelfCloser) {
|
||||
return ProcThreadSelf("fd/" + strconv.FormatUint(uint64(fd), 10))
|
||||
}
|
||||
|
||||
// IsLexicallyInRoot is shorthand for strings.HasPrefix(path+"/", root+"/"),
|
||||
// but properly handling the case where path or root are "/".
|
||||
//
|
||||
// NOTE: The return value only make sense if the path doesn't contain "..".
|
||||
func IsLexicallyInRoot(root, path string) bool {
|
||||
if root != "/" {
|
||||
root += "/"
|
||||
}
|
||||
if path != "/" {
|
||||
path += "/"
|
||||
}
|
||||
return strings.HasPrefix(path, root)
|
||||
}
|
||||
|
||||
// MkdirAllInRootOpen attempts to make
|
||||
//
|
||||
// path, _ := securejoin.SecureJoin(root, unsafePath)
|
||||
// os.MkdirAll(path, mode)
|
||||
// os.Open(path)
|
||||
//
|
||||
// safer against attacks where components in the path are changed between
|
||||
// SecureJoin returning and MkdirAll (or Open) being called. In particular, we
|
||||
// try to detect any symlink components in the path while we are doing the
|
||||
// MkdirAll.
|
||||
//
|
||||
// NOTE: If unsafePath is a subpath of root, we assume that you have already
|
||||
// called SecureJoin and so we use the provided path verbatim without resolving
|
||||
// any symlinks (this is done in a way that avoids symlink-exchange races).
|
||||
// This means that the path also must not contain ".." elements, otherwise an
|
||||
// error will occur.
|
||||
//
|
||||
// This uses securejoin.MkdirAllHandle under the hood, but it has special
|
||||
// handling if unsafePath has already been scoped within the rootfs (this is
|
||||
// needed for a lot of runc callers and fixing this would require reworking a
|
||||
// lot of path logic).
|
||||
func MkdirAllInRootOpen(root, unsafePath string, mode os.FileMode) (_ *os.File, Err error) {
|
||||
// If the path is already "within" the root, get the path relative to the
|
||||
// root and use that as the unsafe path. This is necessary because a lot of
|
||||
// MkdirAllInRootOpen callers have already done SecureJoin, and refactoring
|
||||
// all of them to stop using these SecureJoin'd paths would require a fair
|
||||
// amount of work.
|
||||
// TODO(cyphar): Do the refactor to libpathrs once it's ready.
|
||||
if IsLexicallyInRoot(root, unsafePath) {
|
||||
subPath, err := filepath.Rel(root, unsafePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unsafePath = subPath
|
||||
}
|
||||
|
||||
// Check for any silly mode bits.
|
||||
if mode&^0o7777 != 0 {
|
||||
return nil, fmt.Errorf("tried to include non-mode bits in MkdirAll mode: 0o%.3o", mode)
|
||||
}
|
||||
// Linux (and thus os.MkdirAll) silently ignores the suid and sgid bits if
|
||||
// passed. While it would make sense to return an error in that case (since
|
||||
// the user has asked for a mode that won't be applied), for compatibility
|
||||
// reasons we have to ignore these bits.
|
||||
if ignoredBits := mode &^ 0o1777; ignoredBits != 0 {
|
||||
logrus.Warnf("MkdirAll called with no-op mode bits that are ignored by Linux: 0o%.3o", ignoredBits)
|
||||
mode &= 0o1777
|
||||
}
|
||||
|
||||
rootDir, err := os.OpenFile(root, unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open root handle: %w", err)
|
||||
}
|
||||
defer rootDir.Close()
|
||||
|
||||
return securejoin.MkdirAllHandle(rootDir, unsafePath, mode)
|
||||
}
|
||||
|
||||
// MkdirAllInRoot is a wrapper around MkdirAllInRootOpen which closes the
|
||||
// returned handle, for callers that don't need to use it.
|
||||
func MkdirAllInRoot(root, unsafePath string, mode os.FileMode) error {
|
||||
f, err := MkdirAllInRootOpen(root, unsafePath, mode)
|
||||
if err == nil {
|
||||
_ = f.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Openat is a Go-friendly openat(2) wrapper.
|
||||
func Openat(dir *os.File, path string, flags int, mode uint32) (*os.File, error) {
|
||||
dirFd := unix.AT_FDCWD
|
||||
|
||||
Reference in New Issue
Block a user