From 398955bccb7f20565c224a3064d331c19e422398 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Fri, 1 Aug 2025 19:33:12 +1000 Subject: [PATCH] console: add fallback for pre-TIOCGPTPEER kernels The pty driver has very consistent allocation rules for the major:minor numbers of /dev/pts/$n inodes, so it is possible to somewhat safely open /dev/pts/* paths if we validate that the inode is the one we expect. It is possible for an attacker to have over-mounted a pts peer from a different devpts instance, but to fix this would require more tracking of devpts instances than runc currently can do. This means runc should continue to work on very old kernels. Signed-off-by: Aleksa Sarai --- libcontainer/console_linux.go | 55 ++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/libcontainer/console_linux.go b/libcontainer/console_linux.go index 36db6d69b..418c27f25 100644 --- a/libcontainer/console_linux.go +++ b/libcontainer/console_linux.go @@ -1,6 +1,7 @@ package libcontainer import ( + "errors" "fmt" "os" "runtime" @@ -9,8 +10,60 @@ import ( "golang.org/x/sys/unix" "github.com/opencontainers/runc/internal/linux" + "github.com/opencontainers/runc/internal/pathrs" + "github.com/opencontainers/runc/internal/sys" ) +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 + ) + 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) { @@ -24,7 +77,7 @@ func safeAllocPty() (pty console.Console, peer *os.File, Err error) { } }() - peer, err = linux.GetPtyPeer(pty.Fd(), unsafePeerPath, unix.O_RDWR|unix.O_NOCTTY) + 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) }