mirror of
https://github.com/opencontainers/runc.git
synced 2025-12-24 11:50:58 +08:00
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 <cyphar@cyphar.com>
This commit is contained in:
@@ -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 <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) {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user