mirror of
https://github.com/opencontainers/runc.git
synced 2025-10-06 07:56:55 +08:00

The default terminal setting for a new pty on Linux (unix98) has +ONLCR, resulting in '\n' writes by a container process to be converted to '\r\n' reads by the managing process. This is quite unexpected, and causes multiple issues with things like bats testing. To fix it, make the terminal sane after opening it by setting -ONLCR. This patch might need to be rewritten after the console rewrite patchset is merged. Signed-off-by: Aleksa Sarai <asarai@suse.de>
172 lines
4.5 KiB
Go
172 lines
4.5 KiB
Go
package libcontainer
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"github.com/opencontainers/runc/libcontainer/label"
|
|
)
|
|
|
|
// NewConsole returns an initalized console that can be used within a container by copying bytes
|
|
// from the master side to the slave that is attached as the tty for the container's init process.
|
|
func NewConsole(uid, gid int) (Console, error) {
|
|
master, err := os.OpenFile("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := saneTerminal(master); err != nil {
|
|
return nil, err
|
|
}
|
|
console, err := ptsname(master)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := unlockpt(master); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.Chmod(console, 0600); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.Chown(console, uid, gid); err != nil {
|
|
return nil, err
|
|
}
|
|
return &linuxConsole{
|
|
slavePath: console,
|
|
master: master,
|
|
}, nil
|
|
}
|
|
|
|
// newConsoleFromPath is an internal function returning an initialized console for use inside
|
|
// a container's MNT namespace.
|
|
func newConsoleFromPath(slavePath string) *linuxConsole {
|
|
return &linuxConsole{
|
|
slavePath: slavePath,
|
|
}
|
|
}
|
|
|
|
// linuxConsole is a linux pseudo TTY for use within a container.
|
|
type linuxConsole struct {
|
|
master *os.File
|
|
slavePath string
|
|
}
|
|
|
|
func (c *linuxConsole) Fd() uintptr {
|
|
return c.master.Fd()
|
|
}
|
|
|
|
func (c *linuxConsole) Path() string {
|
|
return c.slavePath
|
|
}
|
|
|
|
func (c *linuxConsole) Read(b []byte) (int, error) {
|
|
return c.master.Read(b)
|
|
}
|
|
|
|
func (c *linuxConsole) Write(b []byte) (int, error) {
|
|
return c.master.Write(b)
|
|
}
|
|
|
|
func (c *linuxConsole) Close() error {
|
|
if m := c.master; m != nil {
|
|
return m.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mount initializes the console inside the rootfs mounting with the specified mount label
|
|
// and applying the correct ownership of the console.
|
|
func (c *linuxConsole) mount(rootfs, mountLabel string) error {
|
|
oldMask := syscall.Umask(0000)
|
|
defer syscall.Umask(oldMask)
|
|
if err := label.SetFileLabel(c.slavePath, mountLabel); err != nil {
|
|
return err
|
|
}
|
|
dest := filepath.Join(rootfs, "/dev/console")
|
|
f, err := os.Create(dest)
|
|
if err != nil && !os.IsExist(err) {
|
|
return err
|
|
}
|
|
if f != nil {
|
|
f.Close()
|
|
}
|
|
return syscall.Mount(c.slavePath, dest, "bind", syscall.MS_BIND, "")
|
|
}
|
|
|
|
// dupStdio opens the slavePath for the console and dups the fds to the current
|
|
// processes stdio, fd 0,1,2.
|
|
func (c *linuxConsole) dupStdio() error {
|
|
slave, err := c.open(syscall.O_RDWR)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fd := int(slave.Fd())
|
|
for _, i := range []int{0, 1, 2} {
|
|
if err := syscall.Dup3(fd, i, 0); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// open is a clone of os.OpenFile without the O_CLOEXEC used to open the pty slave.
|
|
func (c *linuxConsole) open(flag int) (*os.File, error) {
|
|
r, e := syscall.Open(c.slavePath, flag, 0)
|
|
if e != nil {
|
|
return nil, &os.PathError{
|
|
Op: "open",
|
|
Path: c.slavePath,
|
|
Err: e,
|
|
}
|
|
}
|
|
return os.NewFile(uintptr(r), c.slavePath), nil
|
|
}
|
|
|
|
func ioctl(fd uintptr, flag, data uintptr) error {
|
|
if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, flag, data); err != 0 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
|
|
// unlockpt should be called before opening the slave side of a pty.
|
|
func unlockpt(f *os.File) error {
|
|
var u int32
|
|
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
|
|
}
|
|
|
|
// ptsname retrieves the name of the first available pts for the given master.
|
|
func ptsname(f *os.File) (string, error) {
|
|
var n int32
|
|
if err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("/dev/pts/%d", n), nil
|
|
}
|
|
|
|
// saneTerminal sets the necessary tty_ioctl(4)s to ensure that a pty pair
|
|
// created by us acts normally. In particular, a not-very-well-known default of
|
|
// Linux unix98 ptys is that they have +onlcr by default. While this isn't a
|
|
// problem for terminal emulators, because we relay data from the terminal we
|
|
// also relay that funky line discipline.
|
|
func saneTerminal(terminal *os.File) error {
|
|
// Go doesn't have a wrapper for any of the termios ioctls.
|
|
var termios syscall.Termios
|
|
|
|
if err := ioctl(terminal.Fd(), syscall.TCGETS, uintptr(unsafe.Pointer(&termios))); err != nil {
|
|
return fmt.Errorf("ioctl(tty, tcgets): %s", err.Error())
|
|
}
|
|
|
|
// Set -onlcr so we don't have to deal with \r.
|
|
termios.Oflag &^= syscall.ONLCR
|
|
|
|
if err := ioctl(terminal.Fd(), syscall.TCSETS, uintptr(unsafe.Pointer(&termios))); err != nil {
|
|
return fmt.Errorf("ioctl(tty, tcsets): %s", err.Error())
|
|
}
|
|
|
|
return nil
|
|
}
|