Files
runc/internal/linux/linux.go
T
Aleksa Sarai 531ef794e4 console: use TIOCGPTPEER when allocating peer PTY
When opening the peer end of a pty, the old kernel API required us to
open /dev/pts/$num inside the container (at least since we fixed console
handling many years ago in commit 244c9fc426 ("*: console rewrite")).

The problem is that in a hostile container it is possible for
/dev/pts/$num to be an attacker-controlled symlink that runc can be
tricked into resolving when doing bind-mounts. This allows the attacker
to (among other things) persist /proc/... entries that are later masked
by runc, allowing an attacker to escape through the kernel.core_pattern
sysctl (/proc/sys/kernel/core_pattern). This is the original issue
reported by Lei Wang and Li Fu Bang in CVE-2025-52565.

However, it should be noted that this is not entirely a newly-discovered
problem. Way back in Linux 4.13 (2017), I added the TIOCGPTPEER ioctl,
which allows us to get a pty peer without touching the /dev/pts inside
the container. The original threat model was around an attacker
replacing /dev/pts/$n or /dev/pts/ptmx with some malicious inode (a DoS
inode, or possibly a PTY they wanted a confused deputy to operate on).
Unfortunately, there was no practical way for runc to cache a safe
O_PATH handle to /dev/pts/ptmx (unlike other runtimes like LXC, which
switched to TIOCGPTPEER way back in 2017). Since it wasn't clear how we
could protect against the main attack TIOCGPTPEER was meant to protect
against, we never switched to it (even though I implemented it
specifically to harden container runtimes).

Unfortunately, It turns out that mount *sources* are a threat we didn't
fully consider. Since TIOCGPTPEER already solves this problem entirely
for us in a race free way, we should just use that. In a later patch, we
will add some hardening for /dev/pts/$num opening to maintain support
for very old kernels (Linux 4.13 is very old at this point, but RHEL 7
is still kicking and is stuck on Linux 3.10).

Fixes: GHSA-qw9x-cqr3-wc7r CVE-2025-52565
Reported-by: Lei Wang <ssst0n3@gmail.com> (CVE-2025-52565)
Reported-by: lfbzhm <lifubang@acmcoder.com> (CVE-2025-52565)
Reported-by: Aleksa Sarai <cyphar@cyphar.com> (TIOCGPTPEER)
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
2025-11-01 21:24:03 +11:00

108 lines
2.8 KiB
Go

package linux
import (
"os"
"unsafe"
"golang.org/x/sys/unix"
)
// Dup3 wraps [unix.Dup3].
func Dup3(oldfd, newfd, flags int) error {
err := retryOnEINTR(func() error {
return unix.Dup3(oldfd, newfd, flags)
})
return os.NewSyscallError("dup3", err)
}
// Exec wraps [unix.Exec].
func Exec(cmd string, args []string, env []string) error {
err := retryOnEINTR(func() error {
return unix.Exec(cmd, args, env)
})
if err != nil {
return &os.PathError{Op: "exec", Path: cmd, Err: err}
}
return nil
}
// Getwd wraps [unix.Getwd].
func Getwd() (wd string, err error) {
wd, err = retryOnEINTR2(unix.Getwd)
return wd, os.NewSyscallError("getwd", err)
}
// Open wraps [unix.Open].
func Open(path string, mode int, perm uint32) (fd int, err error) {
fd, err = retryOnEINTR2(func() (int, error) {
return unix.Open(path, mode, perm)
})
if err != nil {
return -1, &os.PathError{Op: "open", Path: path, Err: err}
}
return fd, nil
}
// Openat wraps [unix.Openat].
func Openat(dirfd int, path string, mode int, perm uint32) (fd int, err error) {
fd, err = retryOnEINTR2(func() (int, error) {
return unix.Openat(dirfd, path, mode, perm)
})
if err != nil {
return -1, &os.PathError{Op: "openat", Path: path, Err: err}
}
return fd, nil
}
// Recvfrom wraps [unix.Recvfrom].
func Recvfrom(fd int, p []byte, flags int) (n int, from unix.Sockaddr, err error) {
err = retryOnEINTR(func() error {
n, from, err = unix.Recvfrom(fd, p, flags)
return err
})
if err != nil {
return 0, nil, os.NewSyscallError("recvfrom", err)
}
return n, from, err
}
// Sendmsg wraps [unix.Sendmsg].
func Sendmsg(fd int, p, oob []byte, to unix.Sockaddr, flags int) error {
err := retryOnEINTR(func() error {
return unix.Sendmsg(fd, p, oob, to, flags)
})
return os.NewSyscallError("sendmsg", err)
}
// SetMempolicy wraps set_mempolicy.
func SetMempolicy(mode uint, mask *unix.CPUSet) error {
err := retryOnEINTR(func() error {
_, _, errno := unix.Syscall(unix.SYS_SET_MEMPOLICY, uintptr(mode), uintptr(unsafe.Pointer(mask)), unsafe.Sizeof(*mask)*8)
if errno != 0 {
return errno
}
return nil
})
return os.NewSyscallError("set_mempolicy", err)
}
// 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
}