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:
Aleksa Sarai
2025-11-05 20:07:36 +11:00
112 changed files with 9467 additions and 1354 deletions
+5 -8
View File
@@ -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
View File
@@ -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
}
+15 -4
View File
@@ -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
}
+2 -1
View File
@@ -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
View File
@@ -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
}
+8 -7
View File
@@ -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
View File
@@ -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)
})
}
+13 -15
View File
@@ -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 {
+20
View File
@@ -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
}
+13 -3
View File
@@ -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
}
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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)
}
}
}
+19 -108
View File
@@ -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