File: console_linux.go

package info (click to toggle)
runc 1.3.3%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,000 kB
  • sloc: sh: 2,298; ansic: 1,125; makefile: 229
file content (164 lines) | stat: -rw-r--r-- 5,585 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package libcontainer

import (
	"errors"
	"fmt"
	"os"
	"runtime"

	"github.com/containerd/console"
	"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/utils"
)

// 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)
		}
		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
	})
}

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 := unix.Dup3(int(peerPty.Fd()), i, 0); err != nil {
			return err
		}
	}
	runtime.KeepAlive(peerPty)
	return nil
}