Add separate console socket

Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
Michael Crosby 2017-03-02 12:53:06 -08:00
parent 697cb97cb7
commit 00a0ecf554
14 changed files with 178 additions and 215 deletions

View File

@ -7,6 +7,12 @@ import (
"unsafe"
)
func ConsoleFromFile(f *os.File) Console {
return &linuxConsole{
master: f,
}
}
// newConsole returns an initialized 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() (Console, error) {

View File

@ -335,7 +335,7 @@ func (c *linuxContainer) deleteExecFifo() {
}
func (c *linuxContainer) newParentProcess(p *Process, doInit bool) (parentProcess, error) {
parentPipe, childPipe, err := newPipe()
parentPipe, childPipe, err := utils.NewSockPair("init")
if err != nil {
return nil, newSystemErrorWithCause(err, "creating new init pipe")
}
@ -370,9 +370,17 @@ func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.ExtraFiles = append(p.ExtraFiles, childPipe)
cmd.ExtraFiles = append(cmd.ExtraFiles, p.ExtraFiles...)
if p.ConsoleSocket != nil {
cmd.ExtraFiles = append(cmd.ExtraFiles, p.ConsoleSocket)
cmd.Env = append(cmd.Env,
fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1))
fmt.Sprintf("_LIBCONTAINER_CONSOLE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
)
}
cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe)
cmd.Env = append(cmd.Env,
fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1),
)
// NOTE: when running a container with no PID namespace and the parent process spawning the container is
// PID1 the pdeathsig is being delivered to the container's init process by the kernel for some reason
// even with the parent still running.
@ -395,7 +403,6 @@ func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, c
if err != nil {
return nil, err
}
p.consoleChan = make(chan *os.File, 1)
return &initProcess{
cmd: cmd,
childPipe: childPipe,
@ -422,8 +429,6 @@ func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, parentPipe,
if err != nil {
return nil, err
}
// TODO: set on container for process management
p.consoleChan = make(chan *os.File, 1)
return &setnsProcess{
cmd: cmd,
cgroupPaths: c.cgroupManager.GetPaths(),
@ -463,28 +468,10 @@ func (c *linuxContainer) newInitConfig(process *Process) *initConfig {
if len(process.Rlimits) > 0 {
cfg.Rlimits = process.Rlimits
}
/*
* TODO: This should not be automatically computed. We should implement
* this as a field in libcontainer.Process, and then we only dup the
* new console over the file descriptors which were not explicitly
* set with process.Std{in,out,err}. The reason I've left this as-is
* is because the GetConsole() interface is new, there's no need to
* polish this interface right now.
*/
if process.Stdin == nil && process.Stdout == nil && process.Stderr == nil {
cfg.CreateConsole = true
}
cfg.CreateConsole = process.ConsoleSocket != nil
return cfg
}
func newPipe() (parent *os.File, child *os.File, err error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
return os.NewFile(uintptr(fds[1]), "parent"), os.NewFile(uintptr(fds[0]), "child"), nil
}
func (c *linuxContainer) Destroy() error {
c.m.Lock()
defer c.m.Unlock()

View File

@ -222,8 +222,10 @@ func (l *LinuxFactory) Type() string {
func (l *LinuxFactory) StartInitialization() (err error) {
var (
pipefd, rootfd int
consoleSocket *os.File
envInitPipe = os.Getenv("_LIBCONTAINER_INITPIPE")
envStateDir = os.Getenv("_LIBCONTAINER_STATEDIR")
envConsole = os.Getenv("_LIBCONTAINER_CONSOLE")
)
// Get the INITPIPE.
@ -241,12 +243,20 @@ func (l *LinuxFactory) StartInitialization() (err error) {
// Only init processes have STATEDIR.
rootfd = -1
if it == initStandard {
rootfd, err = strconv.Atoi(envStateDir)
if err != nil {
if rootfd, err = strconv.Atoi(envStateDir); err != nil {
return fmt.Errorf("unable to convert _LIBCONTAINER_STATEDIR=%s to int: %s", envStateDir, err)
}
}
if envConsole != "" {
console, err := strconv.Atoi(envConsole)
if err != nil {
return fmt.Errorf("unable to convert _LIBCONTAINER_CONSOLE=%s to int: %s", envConsole, err)
}
consoleSocket = os.NewFile(uintptr(console), "console-socket")
defer consoleSocket.Close()
}
// clear the current process's environment to clean any libcontainer
// specific env vars.
os.Clearenv()
@ -269,7 +279,7 @@ func (l *LinuxFactory) StartInitialization() (err error) {
}
}()
i, err := newContainerInit(it, pipe, rootfd)
i, err := newContainerInit(it, pipe, consoleSocket, rootfd)
if err != nil {
return err
}

View File

@ -66,7 +66,7 @@ type initer interface {
Init() error
}
func newContainerInit(t initType, pipe *os.File, stateDirFD int) (initer, error) {
func newContainerInit(t initType, pipe *os.File, consoleSocket *os.File, stateDirFD int) (initer, error) {
var config *initConfig
if err := json.NewDecoder(pipe).Decode(&config); err != nil {
return nil, err
@ -78,11 +78,13 @@ func newContainerInit(t initType, pipe *os.File, stateDirFD int) (initer, error)
case initSetns:
return &linuxSetnsInit{
pipe: pipe,
consoleSocket: consoleSocket,
config: config,
}, nil
case initStandard:
return &linuxStandardInit{
pipe: pipe,
consoleSocket: consoleSocket,
parentPid: syscall.Getppid(),
config: config,
stateDirFD: stateDirFD,
@ -155,7 +157,8 @@ func finalizeNamespace(config *initConfig) error {
// consoles are scoped to a container properly (see runc#814 and the many
// issues related to that). This has to be run *after* we've pivoted to the new
// rootfs (and the users' configuration is entirely set up).
func setupConsole(pipe *os.File, config *initConfig, mount bool) error {
func setupConsole(socket *os.File, config *initConfig, mount bool) error {
defer socket.Close()
// At this point, /dev/ptmx points to something that we would expect. We
// used to change the owner of the slave path, but since the /dev/pts mount
// can have gid=X set (at the users' option). So touching the owner of the
@ -174,37 +177,16 @@ func setupConsole(pipe *os.File, config *initConfig, mount bool) error {
if !ok {
return fmt.Errorf("failed to cast console to *linuxConsole")
}
// Mount the console inside our rootfs.
if mount {
if err := linuxConsole.mount(); err != nil {
return err
}
}
if err := writeSync(pipe, procConsole); err != nil {
return err
}
// We need to have a two-way synchronisation here. Though it might seem
// pointless, it's important to make sure that the sendmsg(2) payload
// doesn't get swallowed by an out-of-place read(2) [which happens if the
// syscalls get reordered so that sendmsg(2) is before the other side's
// read(2) of procConsole].
if err := readSync(pipe, procConsoleReq); err != nil {
return err
}
// While we can access console.master, using the API is a good idea.
if err := utils.SendFd(pipe, linuxConsole.File()); err != nil {
if err := utils.SendFd(socket, linuxConsole.File()); err != nil {
return err
}
// Make sure the other side received the fd.
if err := readSync(pipe, procConsoleAck); err != nil {
return err
}
// Now, dup over all the things.
return linuxConsole.dupStdio()
}

View File

@ -13,6 +13,7 @@ import (
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/utils"
)
func TestExecIn(t *testing.T) {
@ -279,9 +280,36 @@ func TestExecInTTY(t *testing.T) {
Args: []string{"ps"},
Env: standardEnvironment,
}
parent, child, err := utils.NewSockPair("console")
if err != nil {
ok(t, err)
}
defer parent.Close()
defer child.Close()
ps.ConsoleSocket = child
type cdata struct {
c libcontainer.Console
err error
}
dc := make(chan *cdata, 1)
go func() {
f, err := utils.RecvFd(parent)
if err != nil {
dc <- &cdata{
err: err,
}
}
dc <- &cdata{
c: libcontainer.ConsoleFromFile(f),
}
}()
err = container.Run(ps)
ok(t, err)
console, err := ps.GetConsole()
data := <-dc
if data.err != nil {
ok(t, data.err)
}
console := data.c
copy := make(chan struct{})
go func() {
io.Copy(&stdout, console)

View File

@ -47,10 +47,6 @@ type Process struct {
// ExtraFiles specifies additional open files to be inherited by the container
ExtraFiles []*os.File
// consoleChan provides the masterfd console.
// TODO: Make this persistent in Process.
consoleChan chan *os.File
// Capabilities specify the capabilities to keep when executing the process inside the container
// All capabilities not specified will be dropped from the processes capability mask
Capabilities *configs.Capabilities
@ -69,6 +65,9 @@ type Process struct {
// If Rlimits are not set, the container will inherit rlimits from the parent process
Rlimits []configs.Rlimit
// ConsoleSocket provides the masterfd console.
ConsoleSocket *os.File
ops processOperations
}
@ -105,15 +104,3 @@ type IO struct {
Stdout io.ReadCloser
Stderr io.ReadCloser
}
func (p *Process) GetConsole() (Console, error) {
consoleFd, ok := <-p.consoleChan
if !ok {
return nil, fmt.Errorf("failed to get console from process")
}
// TODO: Fix this so that it used the console API.
return &linuxConsole{
master: consoleFd,
}, nil
}

View File

@ -100,25 +100,6 @@ func (p *setnsProcess) start() (err error) {
ierr := parseSync(p.parentPipe, func(sync *syncT) error {
switch sync.Type {
case procConsole:
if err := writeSync(p.parentPipe, procConsoleReq); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'request fd'")
}
masterFile, err := utils.RecvFd(p.parentPipe)
if err != nil {
return newSystemErrorWithCause(err, "getting master pty from child pipe")
}
if p.process.consoleChan == nil {
// TODO: Don't panic here, do something more sane.
panic("consoleChan is nil")
}
p.process.consoleChan <- masterFile
if err := writeSync(p.parentPipe, procConsoleAck); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'ack fd'")
}
case procReady:
// This shouldn't happen.
panic("unexpected procReady in setns")
@ -128,7 +109,6 @@ func (p *setnsProcess) start() (err error) {
default:
return newSystemError(fmt.Errorf("invalid JSON payload from child"))
}
return nil
})
if err := syscall.Shutdown(int(p.parentPipe.Fd()), syscall.SHUT_WR); err != nil {
@ -264,7 +244,7 @@ func (p *initProcess) start() error {
return newSystemErrorWithCause(err, "starting init process command")
}
if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil {
return err
return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
}
if err := p.execSetns(); err != nil {
return newSystemErrorWithCause(err, "running exec setns process for init")
@ -301,25 +281,6 @@ func (p *initProcess) start() error {
ierr := parseSync(p.parentPipe, func(sync *syncT) error {
switch sync.Type {
case procConsole:
if err := writeSync(p.parentPipe, procConsoleReq); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'request fd'")
}
masterFile, err := utils.RecvFd(p.parentPipe)
if err != nil {
return newSystemErrorWithCause(err, "getting master pty from child pipe")
}
if p.process.consoleChan == nil {
// TODO: Don't panic here, do something more sane.
panic("consoleChan is nil")
}
p.process.consoleChan <- masterFile
if err := writeSync(p.parentPipe, procConsoleAck); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'ack fd'")
}
case procReady:
if err := p.manager.Set(p.config.Config); err != nil {
return newSystemErrorWithCause(err, "setting cgroup config for ready process")

View File

@ -17,6 +17,7 @@ import (
// inside an existing container.
type linuxSetnsInit struct {
pipe *os.File
consoleSocket *os.File
config *initConfig
}
@ -32,7 +33,7 @@ func (l *linuxSetnsInit) Init() error {
}
}
if l.config.CreateConsole {
if err := setupConsole(l.pipe, l.config, false); err != nil {
if err := setupConsole(l.consoleSocket, l.config, false); err != nil {
return err
}
if err := system.Setctty(); err != nil {

View File

@ -18,6 +18,7 @@ import (
type linuxStandardInit struct {
pipe *os.File
consoleSocket *os.File
parentPid int
stateDirFD int
config *initConfig
@ -78,7 +79,7 @@ func (l *linuxStandardInit) Init() error {
// but *after* we've given the user the chance to set up all of the mounts
// they wanted.
if l.config.CreateConsole {
if err := setupConsole(l.pipe, l.config, true); err != nil {
if err := setupConsole(l.consoleSocket, l.config, true); err != nil {
return err
}
if err := system.Setctty(); err != nil {

View File

@ -32,9 +32,6 @@ const (
procRun syncType = "procRun"
procHooks syncType = "procHooks"
procResume syncType = "procResume"
procConsole syncType = "procConsole"
procConsoleReq syncType = "procConsoleReq"
procConsoleAck syncType = "procConsoleAck"
)
type syncT struct {

View File

@ -4,6 +4,7 @@ package utils
import (
"io/ioutil"
"os"
"strconv"
"syscall"
)
@ -31,3 +32,12 @@ func CloseExecFrom(minFd int) error {
}
return nil
}
// NewSockPair returns a new unix socket pair
func NewSockPair(name string) (parent *os.File, child *os.File, err error) {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
return os.NewFile(uintptr(fds[1]), name+"-p"), os.NewFile(uintptr(fds[0]), name+"-c"), nil
}

View File

@ -161,7 +161,7 @@ func restoreContainer(context *cli.Context, spec *specs.Spec, config *configs.Co
defer destroy(container)
}
process := &libcontainer.Process{}
tty, err := setupIO(process, rootuid, rootgid, false, detach)
tty, err := setupIO(process, rootuid, rootgid, false, detach, "")
if err != nil {
return -1, err
}

22
tty.go
View File

@ -19,6 +19,7 @@ type tty struct {
closers []io.Closer
postStart []io.Closer
wg sync.WaitGroup
consoleC chan error
}
func (t *tty) copyIO(w io.Writer, r io.ReadCloser) {
@ -68,37 +69,30 @@ func inheritStdio(process *libcontainer.Process) error {
return nil
}
func (t *tty) recvtty(process *libcontainer.Process, detach bool) error {
console, err := process.GetConsole()
func (t *tty) recvtty(process *libcontainer.Process, socket *os.File) error {
f, err := utils.RecvFd(socket)
if err != nil {
return err
}
if !detach {
console := libcontainer.ConsoleFromFile(f)
go io.Copy(console, os.Stdin)
t.wg.Add(1)
go t.copyIO(os.Stdout, console)
state, err := term.SetRawTerminal(os.Stdin.Fd())
if err != nil {
return fmt.Errorf("failed to set the terminal from the stdin: %v", err)
}
t.state = state
}
t.console = console
t.closers = []io.Closer{console}
return nil
}
func (t *tty) sendtty(socket *os.File, ti *libcontainer.TerminalInfo) error {
if t.console == nil {
return fmt.Errorf("tty.console not set")
func (t *tty) waitConsole() error {
if t.consoleC != nil {
return <-t.consoleC
}
// Create a fake file to contain the terminal info.
console := os.NewFile(t.console.File().Fd(), ti.String())
return utils.SendFd(socket, console)
return nil
}
// ClosePostStart closes any fds that are provided to the container and dup2'd

View File

@ -17,6 +17,7 @@ import (
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/specconv"
"github.com/opencontainers/runc/libcontainer/utils"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/urfave/cli"
)
@ -110,13 +111,45 @@ func destroy(container libcontainer.Container) {
}
// setupIO modifies the given process config according to the options.
func setupIO(process *libcontainer.Process, rootuid, rootgid int, createTTY, detach bool) (*tty, error) {
// This is entirely handled by recvtty.
func setupIO(process *libcontainer.Process, rootuid, rootgid int, createTTY, detach bool, sockpath string) (*tty, error) {
if createTTY {
process.Stdin = nil
process.Stdout = nil
process.Stderr = nil
return &tty{}, nil
t := &tty{}
if !detach {
parent, child, err := utils.NewSockPair("console")
if err != nil {
return nil, err
}
process.ConsoleSocket = child
t.postStart = append(t.postStart, parent, child)
t.consoleC = make(chan error, 1)
go func() {
if err := t.recvtty(process, parent); err != nil {
t.consoleC <- err
}
t.consoleC <- nil
}()
} else {
// the caller of runc will handle receiving the console master
conn, err := net.Dial("unix", sockpath)
if err != nil {
return nil, err
}
uc, ok := conn.(*net.UnixConn)
if !ok {
return nil, fmt.Errorf("casting to UnixConn failed")
}
t.postStart = append(t.postStart, uc)
socket, err := uc.File()
if err != nil {
return nil, err
}
t.postStart = append(t.postStart, socket)
process.ConsoleSocket = socket
}
return t, nil
}
// when runc will detach the caller provides the stdio to runc via runc's 0,1,2
// and the container's process inherits runc's stdio.
@ -190,48 +223,37 @@ func (r *runner) terminalinfo() *libcontainer.TerminalInfo {
}
func (r *runner) run(config *specs.Process) (int, error) {
if err := r.checkTerminal(config); err != nil {
r.destroy()
return -1, err
}
process, err := newProcess(*config)
if err != nil {
r.destroy()
return -1, err
}
if len(r.listenFDs) > 0 {
process.Env = append(process.Env, fmt.Sprintf("LISTEN_FDS=%d", len(r.listenFDs)), "LISTEN_PID=1")
process.ExtraFiles = append(process.ExtraFiles, r.listenFDs...)
}
baseFd := 3 + len(process.ExtraFiles)
for i := baseFd; i < baseFd+r.preserveFDs; i++ {
process.ExtraFiles = append(process.ExtraFiles, os.NewFile(uintptr(i), "PreserveFD:"+strconv.Itoa(i)))
}
rootuid, err := r.container.Config().HostUID()
if err != nil {
r.destroy()
return -1, err
}
rootgid, err := r.container.Config().HostGID()
if err != nil {
r.destroy()
return -1, err
}
detach := r.detach || r.create
// Check command-line for sanity.
if detach && config.Terminal && r.consoleSocket == "" {
r.destroy()
return -1, fmt.Errorf("cannot allocate tty if runc will detach without setting console socket")
}
// XXX: Should we change this?
if (!detach || !config.Terminal) && r.consoleSocket != "" {
r.destroy()
return -1, fmt.Errorf("cannot use console socket if runc will not detach or allocate tty")
}
startFn := r.container.Start
var (
detach = r.detach || r.create
startFn = r.container.Start
)
if !r.create {
startFn = r.container.Run
}
@ -239,7 +261,7 @@ func (r *runner) run(config *specs.Process) (int, error) {
// with detaching containers, and then we get a tty after the container has
// started.
handler := newSignalHandler(r.enableSubreaper, r.notifySocket)
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach)
tty, err := setupIO(process, rootuid, rootgid, config.Terminal, detach, r.consoleSocket)
if err != nil {
r.destroy()
return -1, err
@ -249,46 +271,11 @@ func (r *runner) run(config *specs.Process) (int, error) {
r.destroy()
return -1, err
}
if config.Terminal {
if err = tty.recvtty(process, r.detach || r.create); err != nil {
if err := tty.waitConsole(); err != nil {
r.terminate(process)
r.destroy()
return -1, err
}
}
if config.Terminal && detach {
conn, err := net.Dial("unix", r.consoleSocket)
if err != nil {
r.terminate(process)
r.destroy()
return -1, err
}
defer conn.Close()
unixconn, ok := conn.(*net.UnixConn)
if !ok {
r.terminate(process)
r.destroy()
return -1, fmt.Errorf("casting to UnixConn failed")
}
socket, err := unixconn.File()
if err != nil {
r.terminate(process)
r.destroy()
return -1, err
}
defer socket.Close()
err = tty.sendtty(socket, r.terminalinfo())
if err != nil {
r.terminate(process)
r.destroy()
return -1, err
}
}
if err = tty.ClosePostStart(); err != nil {
r.terminate(process)
r.destroy()
@ -323,6 +310,18 @@ func (r *runner) terminate(p *libcontainer.Process) {
_, _ = p.Wait()
}
func (r *runner) checkTerminal(config *specs.Process) error {
detach := r.detach || r.create
// Check command-line for sanity.
if detach && config.Terminal && r.consoleSocket == "" {
return fmt.Errorf("cannot allocate tty if runc will detach without setting console socket")
}
if (!detach || !config.Terminal) && r.consoleSocket != "" {
return fmt.Errorf("cannot use console socket if runc will not detach or allocate tty")
}
return nil
}
func validateProcessSpec(spec *specs.Process) error {
if spec.Cwd == "" {
return fmt.Errorf("Cwd property must not be empty")