From 9b3550f70bf1612e2c474b3826b0347b21503401 Mon Sep 17 00:00:00 2001 From: Kevin Krakauer Date: Wed, 17 Oct 2018 10:50:24 -0700 Subject: runsc: Add --pid flag to runsc kill. --pid allows specific processes to be signalled rather than the container root process or all processes in the container. containerd needs to SIGKILL exec'd processes that timeout and check whether processes are still alive. PiperOrigin-RevId: 217547636 Change-Id: I2058ebb548b51c8eb748f5884fb88bad0b532e45 --- runsc/boot/loader.go | 16 ++++++++- runsc/cmd/kill.go | 17 ++++++++-- runsc/container/container.go | 32 ++++++++++++------ runsc/container/container_test.go | 58 ++++++++++++++++++++++++++++++++- runsc/container/multi_container_test.go | 12 +++---- runsc/container/test_app.go | 10 +++++- 6 files changed, 124 insertions(+), 21 deletions(-) (limited to 'runsc') diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 0a3f67774..fa169d090 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -756,8 +756,22 @@ 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 { - return fmt.Errorf("failed to signal container %q PID %d: no such PID", cid, pid) + ep, ok := l.processes[execID{cid: cid}] + if !ok { + return fmt.Errorf("no container with ID: %q", cid) + } + tg := ep.tg.PIDNamespace().ThreadGroupWithID(kernel.ThreadID(pid)) + if tg == nil { + return fmt.Errorf("failed to signal container %q PID %d: no such process", cid, pid) + } + if tg.Leader().ContainerID() != cid { + 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 { diff --git a/runsc/cmd/kill.go b/runsc/cmd/kill.go index dcb2988e3..7a98d10a2 100644 --- a/runsc/cmd/kill.go +++ b/runsc/cmd/kill.go @@ -31,6 +31,7 @@ import ( // Kill implements subcommands.Command for the "kill" command. type Kill struct { all bool + pid int } // Name implements subcommands.Command.Name. @@ -51,6 +52,7 @@ func (*Kill) Usage() string { // SetFlags implements subcommands.Command.SetFlags. func (k *Kill) SetFlags(f *flag.FlagSet) { f.BoolVar(&k.all, "all", false, "send the specified signal to all processes inside the container") + f.IntVar(&k.pid, "pid", 0, "send the specified signal to a specific process") } // Execute implements subcommands.Command.Execute. @@ -63,6 +65,10 @@ func (k *Kill) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) id := f.Arg(0) conf := args[0].(*boot.Config) + if k.pid != 0 && k.all { + Fatalf("it is invalid to specify both --all and --pid") + } + c, err := container.Load(conf.RootDir, id) if err != nil { Fatalf("error loading container: %v", err) @@ -80,8 +86,15 @@ func (k *Kill) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) if err != nil { Fatalf("%v", err) } - if err := c.Signal(sig, k.all); err != nil { - Fatalf("%v", err) + + if k.pid != 0 { + if err := c.SignalProcess(sig, int32(k.pid)); err != nil { + Fatalf("failed to signal pid %d: %v", k.pid, err) + } + } else { + if err := c.SignalContainer(sig, k.all); err != nil { + Fatalf("%v", err) + } } return subcommands.ExitSuccess } diff --git a/runsc/container/container.go b/runsc/container/container.go index 774cb6e07..0ec4d03c1 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -174,7 +174,7 @@ func Load(rootDir, id string) (*Container, error) { } else if c.Status == Running { // Container state should reflect the actual state of the application, so // we don't consider gofer process here. - if err := c.Signal(syscall.Signal(0), false); err != nil { + if err := c.SignalContainer(syscall.Signal(0), false); err != nil { c.changeStatus(Stopped) } } @@ -445,7 +445,7 @@ func (c *Container) SandboxPid() int { func (c *Container) Wait() (syscall.WaitStatus, error) { log.Debugf("Wait on container %q", c.ID) if !c.isSandboxRunning() { - return 0, fmt.Errorf("container is not running") + return 0, fmt.Errorf("sandbox is not running") } return c.Sandbox.Wait(c.ID) } @@ -455,7 +455,7 @@ func (c *Container) Wait() (syscall.WaitStatus, error) { func (c *Container) WaitRootPID(pid int32, clearStatus bool) (syscall.WaitStatus, error) { log.Debugf("Wait on PID %d in sandbox %q", pid, c.Sandbox.ID) if !c.isSandboxRunning() { - return 0, fmt.Errorf("container is not running") + return 0, fmt.Errorf("sandbox is not running") } return c.Sandbox.WaitPID(c.Sandbox.ID, pid, clearStatus) } @@ -465,16 +465,16 @@ func (c *Container) WaitRootPID(pid int32, clearStatus bool) (syscall.WaitStatus func (c *Container) WaitPID(pid int32, clearStatus bool) (syscall.WaitStatus, error) { log.Debugf("Wait on PID %d in container %q", pid, c.ID) if !c.isSandboxRunning() { - return 0, fmt.Errorf("container is not running") + return 0, fmt.Errorf("sandbox is not running") } return c.Sandbox.WaitPID(c.ID, pid, clearStatus) } -// Signal sends the signal to the container. If all is true and signal is -// SIGKILL, then waits for all processes to exit before returning. -// Signal returns an error if the container is already stopped. +// SignalContainer sends the signal to the container. If all is true and signal +// is SIGKILL, then waits for all processes to exit before returning. +// SignalContainer returns an error if the container is already stopped. // TODO: Distinguish different error types. -func (c *Container) Signal(sig syscall.Signal, all bool) error { +func (c *Container) SignalContainer(sig syscall.Signal, all bool) error { log.Debugf("Signal container %q: %v", c.ID, sig) // Signaling container in Stopped state is allowed. When all=false, // an error will be returned anyway; when all=true, this allows @@ -485,11 +485,23 @@ func (c *Container) Signal(sig syscall.Signal, all bool) error { return err } if !c.isSandboxRunning() { - return fmt.Errorf("container is not running") + return fmt.Errorf("sandbox is not running") } return c.Sandbox.SignalContainer(c.ID, sig, all) } +// SignalProcess sends sig to a specific process in the container. +func (c *Container) SignalProcess(sig syscall.Signal, pid int32) error { + log.Debugf("Signal process %d in container %q: %v", pid, c.ID, sig) + if err := c.requireStatus("signal a process inside", Running); err != nil { + return err + } + if !c.isSandboxRunning() { + return fmt.Errorf("sandbox is not running") + } + return c.Sandbox.SignalProcess(c.ID, int32(pid), sig, false) +} + // ForwardSignals forwards all signals received by the current process to the // container process inside the sandbox. It returns a function that will stop // forwarding signals. @@ -663,7 +675,7 @@ func (c *Container) waitForStopped() error { b := backoff.WithContext(backoff.NewConstantBackOff(100*time.Millisecond), ctx) op := func() error { if c.isSandboxRunning() { - if err := c.Signal(syscall.Signal(0), false); err == nil { + if err := c.SignalContainer(syscall.Signal(0), false); err == nil { return fmt.Errorf("container is still running") } } diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go index 94572667e..d9cd38c0a 100644 --- a/runsc/container/container_test.go +++ b/runsc/container/container_test.go @@ -354,7 +354,7 @@ func TestLifecycle(t *testing.T) { <-ch time.Sleep(100 * time.Millisecond) // Send the container a SIGTERM which will cause it to stop. - if err := c.Signal(syscall.SIGTERM, false); err != nil { + if err := c.SignalContainer(syscall.SIGTERM, false); err != nil { t.Fatalf("error sending signal %v to container: %v", syscall.SIGTERM, err) } // Wait for it to die. @@ -559,6 +559,62 @@ func TestExec(t *testing.T) { } } +// TestKillPid verifies that we can signal individual exec'd processes. +func TestKillPid(t *testing.T) { + for _, conf := range configs(overlay) { + t.Logf("Running test with conf: %+v", conf) + + app, err := testutil.FindFile("runsc/container/test_app") + if err != nil { + t.Fatal("error finding test_app:", err) + } + + const nProcs = 4 + spec := testutil.NewSpecWithArgs(app, "task-tree", "--depth", strconv.Itoa(nProcs-1), "--width=1", "--pause=true") + rootDir, bundleDir, err := testutil.SetupContainer(spec, conf) + if err != nil { + t.Fatalf("error setting up container: %v", err) + } + defer os.RemoveAll(rootDir) + defer os.RemoveAll(bundleDir) + + // Create and start the container. + cont, err := Create(testutil.UniqueContainerID(), spec, conf, bundleDir, "", "", "") + if err != nil { + t.Fatalf("error creating container: %v", err) + } + defer cont.Destroy() + if err := cont.Start(conf); err != nil { + t.Fatalf("error starting container: %v", err) + } + + // Verify that all processes are running. + if err := waitForProcessCount(cont, nProcs); err != nil { + t.Fatalf("timed out waiting for processes to start: %v", err) + } + + // Kill the child process with the largest PID. + procs, err := cont.Processes() + if err != nil { + t.Fatalf("failed to get process list: %v", err) + } + var pid int32 + for _, p := range procs { + if pid < int32(p.PID) { + pid = int32(p.PID) + } + } + if err := cont.SignalProcess(syscall.SIGKILL, pid); err != nil { + t.Fatalf("failed to signal process %d: %v", pid, err) + } + + // Verify that one process is gone. + if err := waitForProcessCount(cont, nProcs-1); err != nil { + t.Fatal(err) + } + } +} + // TestCheckpointRestore creates a container that continuously writes successive integers // to a file. To test checkpoint and restore functionality, the container is // checkpointed and the last number printed to the file is recorded. Then, it is restored in two diff --git a/runsc/container/multi_container_test.go b/runsc/container/multi_container_test.go index 77f8da8b0..1781a4602 100644 --- a/runsc/container/multi_container_test.go +++ b/runsc/container/multi_container_test.go @@ -335,7 +335,7 @@ func TestMultiContainerSignal(t *testing.T) { } // Kill process 2. - if err := containers[1].Signal(syscall.SIGKILL, false); err != nil { + if err := containers[1].SignalContainer(syscall.SIGKILL, false); err != nil { t.Errorf("failed to kill process 2: %v", err) } @@ -368,12 +368,12 @@ func TestMultiContainerSignal(t *testing.T) { // Now that process 2 is gone, ensure we get an error trying to // signal it again. - if err := containers[1].Signal(syscall.SIGKILL, false); err == nil { + if err := containers[1].SignalContainer(syscall.SIGKILL, false); err == nil { t.Errorf("container %q shouldn't exist, but we were able to signal it", containers[1].ID) } // Kill process 1. - if err := containers[0].Signal(syscall.SIGKILL, false); err != nil { + if err := containers[0].SignalContainer(syscall.SIGKILL, false); err != nil { t.Errorf("failed to kill process 1: %v", err) } @@ -395,7 +395,7 @@ func TestMultiContainerSignal(t *testing.T) { } // The sentry should be gone, so signaling should yield an error. - if err := containers[0].Signal(syscall.SIGKILL, false); err == nil { + if err := containers[0].SignalContainer(syscall.SIGKILL, false); err == nil { t.Errorf("sandbox %q shouldn't exist, but we were able to signal it", containers[0].Sandbox.ID) } } @@ -577,7 +577,7 @@ func TestMultiContainerKillAll(t *testing.T) { if tc.killContainer { // First kill the init process to make the container be stopped with // processes still running inside. - containers[1].Signal(syscall.SIGKILL, false) + containers[1].SignalContainer(syscall.SIGKILL, false) op := func() error { c, err := Load(conf.RootDir, ids[1]) if err != nil { @@ -598,7 +598,7 @@ func TestMultiContainerKillAll(t *testing.T) { t.Fatalf("failed to load child container %q: %v", c.ID, err) } // Kill'Em All - if err := c.Signal(syscall.SIGKILL, true); err != nil { + if err := c.SignalContainer(syscall.SIGKILL, true); err != nil { t.Fatalf("failed to send SIGKILL to container %q: %v", c.ID, err) } diff --git a/runsc/container/test_app.go b/runsc/container/test_app.go index 9e4b5326d..cc3b087e1 100644 --- a/runsc/container/test_app.go +++ b/runsc/container/test_app.go @@ -125,6 +125,7 @@ func server(listener net.Listener, out *os.File) { type taskTree struct { depth int width int + pause bool } // Name implements subcommands.Command. @@ -146,6 +147,7 @@ func (*taskTree) Usage() string { func (c *taskTree) SetFlags(f *flag.FlagSet) { f.IntVar(&c.depth, "depth", 1, "number of levels to create") f.IntVar(&c.width, "width", 1, "number of tasks at each level") + f.BoolVar(&c.pause, "pause", false, "whether the tasks should pause perpetually") } // Execute implements subcommands.Command. @@ -164,7 +166,8 @@ func (c *taskTree) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa cmd := exec.Command( "/proc/self/exe", c.Name(), "--depth", strconv.Itoa(c.depth-1), - "--width", strconv.Itoa(c.width)) + "--width", strconv.Itoa(c.width), + "--pause", strconv.FormatBool(c.pause)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -177,6 +180,11 @@ func (c *taskTree) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa for _, c := range cmds { c.Wait() } + + if c.pause { + select {} + } + return subcommands.ExitSuccess } -- cgit v1.2.3