summaryrefslogtreecommitdiffhomepage
path: root/runsc/boot
diff options
context:
space:
mode:
authorNicolas Lacasse <nlacasse@google.com>2018-10-17 12:27:58 -0700
committerShentubot <shentubot@google.com>2018-10-17 12:29:05 -0700
commit4e6f0892c96c374b1abcf5c39b75ba52d98c97f8 (patch)
treecb21538ad26a50ff61086d55c1bef36d5026e4c0 /runsc/boot
parent578fe5a50dcf8e104b6bce3802987b0f8c069ade (diff)
runsc: Support job control signals for the root container.
Now containers run with "docker run -it" support control characters like ^C and ^Z. This required refactoring our signal handling a bit. Signals delivered to the "runsc boot" process are turned into loader.Signal calls with the appropriate delivery mode. Previously they were always sent directly to PID 1. PiperOrigin-RevId: 217566770 Change-Id: I5b7220d9a0f2b591a56335479454a200c6de8732
Diffstat (limited to 'runsc/boot')
-rw-r--r--runsc/boot/controller.go58
-rw-r--r--runsc/boot/fds.go45
-rw-r--r--runsc/boot/loader.go221
3 files changed, 182 insertions, 142 deletions
diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go
index eaeb9e2d8..bee82f344 100644
--- a/runsc/boot/controller.go
+++ b/runsc/boot/controller.go
@@ -425,6 +425,26 @@ func (cm *containerManager) WaitPID(args *WaitPIDArgs, waitStatus *uint32) error
return cm.l.waitPID(kernel.ThreadID(args.PID), args.CID, args.ClearStatus, waitStatus)
}
+// SignalDeliveryMode enumerates different signal delivery modes.
+type SignalDeliveryMode int
+
+const (
+ // DeliverToProcess delivers the signal to the container process with
+ // the specified PID. If PID is 0, then the container init process is
+ // signaled.
+ DeliverToProcess SignalDeliveryMode = iota
+
+ // DeliverToAllProcesses delivers the signal to all processes in the
+ // container. PID must be 0.
+ DeliverToAllProcesses
+
+ // DeliverToForegroundProcessGroup delivers the signal to the
+ // foreground process group in the same TTY session as the specified
+ // process. If PID is 0, then the signal is delivered to the foreground
+ // process group for the TTY for the init process.
+ DeliverToForegroundProcessGroup
+)
+
// SignalArgs are arguments to the Signal method.
type SignalArgs struct {
// CID is the container ID.
@@ -433,36 +453,20 @@ type SignalArgs struct {
// Signo is the signal to send to the process.
Signo int32
- // All is set when signal should be sent to all processes in the container.
- // When false, the signal is sent to the root container process only.
- All bool
-}
-
-// Signal sends a signal to the root process of the container.
-func (cm *containerManager) Signal(args *SignalArgs, _ *struct{}) error {
- log.Debugf("containerManager.Signal %q %d, all: %t", args.CID, args.Signo, args.All)
- return cm.l.signalContainer(args.CID, args.Signo, args.All)
-}
-
-// SignalProcessArgs are arguments to the Signal method.
-type SignalProcessArgs struct {
- // CID is the container ID.
- CID string
-
// PID is the process ID in the given container that will be signaled.
+ // If 0, the root container will be signalled.
PID int32
- // Signo is the signal to send to the process.
- Signo int32
-
- // SendToForegroundProcess indicates that the signal should be sent to
- // the foreground process group in the session that PID belongs to.
- // This is only valid if the process is attached to a host TTY.
- SendToForegroundProcess bool
+ // Mode is the signal delivery mode.
+ Mode SignalDeliveryMode
}
-// SignalProcess sends a signal to a particular process in the container.
-func (cm *containerManager) SignalProcess(args *SignalProcessArgs, _ *struct{}) error {
- log.Debugf("containerManager.Signal: %+v", args)
- return cm.l.signalProcess(args.CID, args.PID, args.Signo, args.SendToForegroundProcess)
+// Signal sends a signal to one or more processes in a container. If args.PID
+// is 0, then the container init process is used. Depending on the
+// args.SignalDeliveryMode option, the signal may be sent directly to the
+// indicated process, to all processes in the container, or to the foreground
+// process group.
+func (cm *containerManager) Signal(args *SignalArgs, _ *struct{}) error {
+ log.Debugf("containerManager.Signal %+v", args)
+ return cm.l.signal(args.CID, args.PID, args.Signo, args.Mode)
}
diff --git a/runsc/boot/fds.go b/runsc/boot/fds.go
index a5a6ba8af..9416e3a5c 100644
--- a/runsc/boot/fds.go
+++ b/runsc/boot/fds.go
@@ -35,6 +35,7 @@ func createFDMap(ctx context.Context, k *kernel.Kernel, l *limits.LimitSet, cons
fdm := k.NewFDMap()
defer fdm.DecRef()
+ mounter := fs.FileOwnerFromContext(ctx)
// Maps sandbox FD to host FD.
fdMap := map[int]int{
@@ -42,16 +43,44 @@ func createFDMap(ctx context.Context, k *kernel.Kernel, l *limits.LimitSet, cons
1: stdioFDs[1],
2: stdioFDs[2],
}
- mounter := fs.FileOwnerFromContext(ctx)
- for sfd, hfd := range fdMap {
- file, err := host.ImportFile(ctx, hfd, mounter, console /* isTTY */)
- if err != nil {
- return nil, fmt.Errorf("failed to import fd %d: %v", hfd, err)
+ var ttyFile *fs.File
+ for appFD, hostFD := range fdMap {
+ var appFile *fs.File
+
+ if console && appFD < 3 {
+ // Import the file as a host TTY file.
+ if ttyFile == nil {
+ var err error
+ appFile, err = host.ImportFile(ctx, hostFD, mounter, true /* isTTY */)
+ if err != nil {
+ return nil, err
+ }
+ defer appFile.DecRef()
+
+ // Remember this in the TTY file, as we will
+ // use it for the other stdio FDs.
+ ttyFile = appFile
+ } else {
+ // Re-use the existing TTY file, as all three
+ // stdio FDs must point to the same fs.File in
+ // order to share TTY state, specifically the
+ // foreground process group id.
+ appFile = ttyFile
+ }
+ } else {
+ // Import the file as a regular host file.
+ var err error
+ appFile, err = host.ImportFile(ctx, hostFD, mounter, false /* isTTY */)
+ if err != nil {
+ return nil, err
+ }
+ defer appFile.DecRef()
}
- defer file.DecRef()
- if err := fdm.NewFDAt(kdefs.FD(sfd), file, kernel.FDFlags{}, l); err != nil {
- return nil, fmt.Errorf("failed to add imported fd %d to FDMap: %v", hfd, err)
+
+ // Add the file to the FD map.
+ if err := fdm.NewFDAt(kdefs.FD(appFD), appFile, kernel.FDFlags{}, l); err != nil {
+ return nil, err
}
}
diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go
index fa169d090..c79b95bde 100644
--- a/runsc/boot/loader.go
+++ b/runsc/boot/loader.go
@@ -19,7 +19,6 @@ import (
"fmt"
mrand "math/rand"
"os"
- "os/signal"
"runtime"
"sync"
"sync/atomic"
@@ -110,7 +109,7 @@ type Loader struct {
// mu guards processes.
mu sync.Mutex
- // processes maps containers root process and invocation of exec. Root
+ // processes maps containers init process and invocation of exec. Root
// processes are keyed with container ID and pid=0, while exec invocations
// have the corresponding pid set.
//
@@ -291,28 +290,9 @@ func New(args Args) (*Loader, error) {
return nil, fmt.Errorf("error creating control server: %v", err)
}
- // We don't care about child signals; some platforms can generate a
- // tremendous number of useless ones (I'm looking at you, ptrace).
- if err := sighandling.IgnoreChildStop(); err != nil {
- return nil, fmt.Errorf("failed to ignore child stop signals: %v", err)
- }
- // Ensure that signals received are forwarded to the emulated kernel.
- ps := syscall.Signal(args.Conf.PanicSignal)
- startSignalForwarding := sighandling.PrepareForwarding(k, ps)
- if args.Conf.PanicSignal != -1 {
- // Panics if the sentry receives 'Config.PanicSignal'.
- panicChan := make(chan os.Signal, 1)
- signal.Notify(panicChan, ps)
- go func() { // S/R-SAFE: causes sentry panic.
- <-panicChan
- panic("Signal-induced panic")
- }()
- log.Infof("Panic signal set to %v(%d)", ps, args.Conf.PanicSignal)
- }
-
procArgs, err := newProcess(args.ID, args.Spec, creds, k)
if err != nil {
- return nil, fmt.Errorf("failed to create root process: %v", err)
+ return nil, fmt.Errorf("failed to create init process for root container: %v", err)
}
if err := initCompatLogs(args.UserLogFD); err != nil {
@@ -320,19 +300,47 @@ func New(args Args) (*Loader, error) {
}
l := &Loader{
- k: k,
- ctrl: ctrl,
- conf: args.Conf,
- console: args.Console,
- watchdog: watchdog,
- spec: args.Spec,
- goferFDs: args.GoferFDs,
- stdioFDs: args.StdioFDs,
- startSignalForwarding: startSignalForwarding,
- rootProcArgs: procArgs,
- sandboxID: args.ID,
- processes: make(map[execID]*execProcess),
+ k: k,
+ ctrl: ctrl,
+ conf: args.Conf,
+ console: args.Console,
+ watchdog: watchdog,
+ spec: args.Spec,
+ goferFDs: args.GoferFDs,
+ stdioFDs: args.StdioFDs,
+ rootProcArgs: procArgs,
+ sandboxID: args.ID,
+ processes: make(map[execID]*execProcess),
}
+
+ // We don't care about child signals; some platforms can generate a
+ // tremendous number of useless ones (I'm looking at you, ptrace).
+ if err := sighandling.IgnoreChildStop(); err != nil {
+ return nil, fmt.Errorf("failed to ignore child stop signals: %v", err)
+ }
+
+ // Handle signals by forwarding them to the root container process
+ // (except for panic signal, which should cause a panic).
+ l.startSignalForwarding = sighandling.PrepareHandler(func(sig linux.Signal) {
+ // Panic signal should cause a panic.
+ if args.Conf.PanicSignal != -1 && sig == linux.Signal(args.Conf.PanicSignal) {
+ panic("Signal-induced panic")
+ }
+
+ // Otherwise forward to root container.
+ deliveryMode := DeliverToProcess
+ if args.Console {
+ // Since we are running with a console, we should
+ // forward the signal to the foreground process group
+ // so that job control signals like ^C can be handled
+ // properly.
+ deliveryMode = DeliverToForegroundProcessGroup
+ }
+ if err := l.signal(args.ID, 0, int32(sig), deliveryMode); err != nil {
+ log.Warningf("error sending signal %v to container %q: %v", sig, args.ID, err)
+ }
+ })
+
ctrl.manager.l = l
return l, nil
}
@@ -467,9 +475,15 @@ func (l *Loader) run() error {
l.rootProcArgs.FDMap.DecRef()
}
- l.mu.Lock()
eid := execID{cid: l.sandboxID}
- l.processes[eid] = &execProcess{tg: l.k.GlobalInit()}
+ ep := execProcess{tg: l.k.GlobalInit()}
+ if l.console {
+ ttyFile := l.rootProcArgs.FDMap.GetFile(0)
+ defer ttyFile.DecRef()
+ ep.tty = ttyFile.FileOperations.(*host.TTYFileOperations)
+ }
+ l.mu.Lock()
+ l.processes[eid] = &ep
l.mu.Unlock()
// Start signal forwarding only after an init process is created.
@@ -572,7 +586,7 @@ func (l *Loader) startContainer(k *kernel.Kernel, spec *specs.Spec, conf *Config
// filesystem.
func (l *Loader) destroyContainer(cid string) error {
// First kill and wait for all processes in the container.
- if err := l.signalContainer(cid, int32(linux.SIGKILL), true /*all*/); err != nil {
+ if err := l.signal(cid, 0, int32(linux.SIGKILL), DeliverToAllProcesses); err != nil {
return fmt.Errorf("failed to SIGKILL all container processes: %v", err)
}
@@ -634,7 +648,7 @@ func (l *Loader) executeAsync(args *control.ExecArgs) (kernel.ThreadID, error) {
return tgid, nil
}
-// waitContainer waits for the root process of a container to exit.
+// waitContainer waits for the init process of a container to exit.
func (l *Loader) waitContainer(cid string, waitStatus *uint32) error {
// Don't defer unlock, as doing so would make it impossible for
// multiple clients to wait on the same container.
@@ -740,11 +754,12 @@ func newEmptyNetworkStack(conf *Config, clock tcpip.Clock) (inet.Stack, error) {
}
}
-// signalProcess sends a signal to the process with the given PID. If
-// sendToFGProcess is true, then the signal will be sent to the foreground
-// process group in the same session that PID belongs to.
-func (l *Loader) signalProcess(cid string, pid, signo int32, sendToFGProcess bool) error {
- if pid <= 0 {
+// signal sends a signal to one or more processes in a container. If PID is 0,
+// then the container init process is used. Depending on the SignalDeliveryMode
+// option, the signal may be sent directly to the indicated process, to all
+// processes in the container, or to the foreground process group.
+func (l *Loader) signal(cid string, pid, signo int32, mode SignalDeliveryMode) error {
+ if pid < 0 {
return fmt.Errorf("failed to signal container %q PID %d: PID must be positive", cid, pid)
}
@@ -756,10 +771,16 @@ func (l *Loader) signalProcess(cid string, pid, signo int32, sendToFGProcess boo
ep, ok := l.processes[eid]
l.mu.Unlock()
- // The caller may be signaling a process not started directly via exec.
- // In this case, find the process in the container's PID namespace and
- // signal it.
- if !ok {
+ switch mode {
+ case DeliverToProcess:
+ if ok {
+ // Send signal directly to the identified process.
+ return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
+ }
+
+ // The caller may be signaling a process not started directly via exec.
+ // In this case, find the process in the container's PID namespace and
+ // signal it.
ep, ok := l.processes[execID{cid: cid}]
if !ok {
return fmt.Errorf("no container with ID: %q", cid)
@@ -772,74 +793,60 @@ func (l *Loader) signalProcess(cid string, pid, signo int32, sendToFGProcess boo
return fmt.Errorf("process %d is part of a different container: %q", pid, tg.Leader().ContainerID())
}
return tg.SendSignal(&arch.SignalInfo{Signo: signo})
- }
-
- if !sendToFGProcess {
- // Send signal directly to exec process.
- return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
- }
- // Lookup foreground process group from the TTY for the given process,
- // and send the signal to it.
- if ep.tty == nil {
- return fmt.Errorf("failed to signal foreground process group in container %q PID %d: no TTY attached", cid, pid)
- }
- pg := ep.tty.ForegroundProcessGroup()
- if pg == nil {
- // No foreground process group has been set. Signal the
- // original thread group.
- log.Warningf("No foreground process group for container %q and PID %d. Sending signal directly to PID %d.", cid, pid, pid)
- return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
- }
+ case DeliverToForegroundProcessGroup:
+ if !ok {
+ return fmt.Errorf("failed to signal foreground process group for container %q PID %d: no such PID", cid, pid)
+ }
- // Send the signal to all processes in the process group.
- var lastErr error
- for _, tg := range l.k.TaskSet().Root.ThreadGroups() {
- if tg.ProcessGroup() != pg {
- continue
+ // Lookup foreground process group from the TTY for the given process,
+ // and send the signal to it.
+ if ep.tty == nil {
+ return fmt.Errorf("failed to signal foreground process group in container %q PID %d: no TTY attached", cid, pid)
}
- if err := tg.SendSignal(&arch.SignalInfo{Signo: signo}); err != nil {
- lastErr = err
+ pg := ep.tty.ForegroundProcessGroup()
+ if pg == nil {
+ // No foreground process group has been set. Signal the
+ // original thread group.
+ log.Warningf("No foreground process group for container %q and PID %d. Sending signal directly to PID %d.", cid, pid, pid)
+ return ep.tg.SendSignal(&arch.SignalInfo{Signo: signo})
+ }
+ // Send the signal to all processes in the process group.
+ var lastErr error
+ for _, tg := range l.k.TaskSet().Root.ThreadGroups() {
+ if tg.ProcessGroup() != pg {
+ continue
+ }
+ if err := tg.SendSignal(&arch.SignalInfo{Signo: signo}); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+ case DeliverToAllProcesses:
+ if !ok {
+ return fmt.Errorf("failed to signal all processes in container %q PID %d: no such PID", cid, pid)
}
- }
- return lastErr
-}
-
-// signalContainer sends a signal to the root container process, or to all
-// processes in the container if all is true.
-func (l *Loader) signalContainer(cid string, signo int32, all bool) error {
- si := arch.SignalInfo{Signo: signo}
-
- l.mu.Lock()
- defer l.mu.Unlock()
-
- eid := execID{cid: cid}
- ep, ok := l.processes[eid]
- if !ok {
- return fmt.Errorf("failed to signal container %q: no such container", cid)
- }
-
- if !all {
- return ep.tg.SendSignal(&si)
- }
- // Pause the kernel to prevent new processes from being created while
- // the signal is delivered. This prevents process leaks when SIGKILL is
- // sent to the entire container.
- l.k.Pause()
- if err := l.k.SendContainerSignal(cid, &si); err != nil {
+ // Pause the kernel to prevent new processes from being created while
+ // the signal is delivered. This prevents process leaks when SIGKILL is
+ // sent to the entire container.
+ l.k.Pause()
+ if err := l.k.SendContainerSignal(cid, &arch.SignalInfo{Signo: signo}); err != nil {
+ l.k.Unpause()
+ return err
+ }
l.k.Unpause()
- return err
- }
- l.k.Unpause()
- // If killing all processes, wait for them to exit.
- if all && linux.Signal(signo) == linux.SIGKILL {
- for _, t := range l.k.TaskSet().Root.Tasks() {
- if t.ContainerID() == cid {
- t.ThreadGroup().WaitExited()
+ // If SIGKILLing all processes, wait for them to exit.
+ if linux.Signal(signo) == linux.SIGKILL {
+ for _, t := range l.k.TaskSet().Root.Tasks() {
+ if t.ContainerID() == cid {
+ t.ThreadGroup().WaitExited()
+ }
}
}
+ return nil
+ default:
+ panic(fmt.Sprintf("unknown signal signal delivery mode %v", mode))
}
- return nil
}