Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions libcontainer/init_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"net"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
Expand Down Expand Up @@ -41,6 +42,25 @@ type pid struct {
PidFirstChild int `json:"stage1_pid"`
}

func setupPreExecSignalExit() {
if unix.Getpid() != 1 {
return
}

// The Go runtime assumes the kernel will finish terminating _SigKill
// signals by calling dieFromSignal:
// https://github.com/golang/go/blob/c60392da8b6f18b2aa92db5d22c4963ec25ae0ad/src/runtime/signal_unix.go#L993
// But Linux suppresses the default action for PID 1. While this helper is
// still the container's PID 1, translate the affected signals to 128+signo
// instead of leaking Go's fallback exit status 2.
signals := make(chan os.Signal, 1)
signal.Notify(signals, unix.SIGTERM, unix.SIGINT, unix.SIGHUP)
go func() {
sig := <-signals
Comment thread
lifubang marked this conversation as resolved.
os.Exit(128 + int(sig.(unix.Signal)))
}()
}

// network is an internal struct used to setup container networks.
type network struct {
configs.Network
Expand Down
167 changes: 167 additions & 0 deletions libcontainer/integration/preexec_signal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package integration

import (
"errors"
"os"
"path/filepath"
"syscall"
"testing"
"time"

"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/configs"

"golang.org/x/sys/unix"
)

// Test to simulate problem we saw in https://github.com/kubernetes/kubernetes/issues/135713
func TestPreExecSignalMapping(t *testing.T) {
Comment thread
dims marked this conversation as resolved.
if testing.Short() {
return
}

testCases := []struct {
name string
sig unix.Signal
}{
{name: "SIGTERM", sig: unix.SIGTERM},
{name: "SIGINT", sig: unix.SIGINT},
{name: "SIGHUP", sig: unix.SIGHUP},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
container, process, hookRelease := newPreExecSignalProcess(t)
defer destroyContainer(container)

ok(t, container.Signal(tc.sig))
if got, want := waitForExitCodeWhileHookBlocked(t, process, hookRelease), 128+int(tc.sig); got != want {
t.Fatalf("expected %s to exit %d, got %d", tc.name, want, got)
}
})
}
}

func newPreExecSignalProcess(t testing.TB) (*libcontainer.Container, *libcontainer.Process, string) {
t.Helper()

config := newTemplateConfig(t, nil)
hookReady := filepath.Join(config.Rootfs, "startContainer-hook-ready")
hookRelease := filepath.Join(config.Rootfs, "startContainer-hook-release")
ok(t, unix.Mkfifo(hookRelease, 0o600))
config.Hooks = configs.Hooks{
configs.StartContainer: configs.HookList{
configs.NewCommandHook(&configs.Command{
Path: "/bin/sh",
Args: []string{"/bin/sh", "-c", "touch /startContainer-hook-ready && cat /startContainer-hook-release >/dev/null"},
}),
},
}

container, err := newContainer(t, config)
ok(t, err)

process := &libcontainer.Process{
Cwd: "/",
Args: []string{"false"},
Env: standardEnvironment,
Init: true,
}

ok(t, container.Run(process))
waitForFile(t, hookReady)
return container, process, hookRelease
}

func waitForFile(t testing.TB, path string) {
t.Helper()

deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if _, err := os.Stat(path); err == nil {
return
} else if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("stat %s: %v", path, err)
}
time.Sleep(10 * time.Millisecond)
}

t.Fatalf("timed out waiting for %s", path)
}

func processExitCode(process *libcontainer.Process) (int, error) {
ps, err := process.Wait()
if err != nil && ps == nil {
return 0, err
}
if ps == nil {
return 0, errors.New("wait returned nil process state")
}

status, ok := ps.Sys().(syscall.WaitStatus)
if !ok {
return 0, errors.New("unexpected wait status type")
}
if !status.Exited() {
return 0, errors.New("process did not exit")
}
return status.ExitStatus(), nil
}

func waitForExitCodeWhileHookBlocked(t testing.TB, process *libcontainer.Process, hookRelease string) int {
t.Helper()

type waitResult struct {
code int
err error
}

results := make(chan waitResult, 1)
go func() {
code, err := processExitCode(process)
results <- waitResult{code: code, err: err}
}()

select {
case result := <-results:
releaseHook(t, hookRelease)
if result.err != nil {
t.Fatalf("wait: %v", result.err)
}
return result.code
case <-time.After(500 * time.Millisecond):
releaseHook(t, hookRelease)
t.Fatal("process did not exit while startContainer hook was still blocking")
return 0
}
}

func releaseHook(t testing.TB, path string) {
t.Helper()

fd, err := unix.Open(path, unix.O_WRONLY|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
if errors.Is(err, unix.ENXIO) || errors.Is(err, os.ErrNotExist) {
return
}
ok(t, err)
f := os.NewFile(uintptr(fd), path)
if _, err := f.Write([]byte("ok\n")); err != nil {
_ = f.Close()
t.Fatalf("write %s: %v", path, err)
}
ok(t, f.Close())

deadline := time.Now().Add(500 * time.Millisecond)
for time.Now().Before(deadline) {
fd, err := unix.Open(path, unix.O_WRONLY|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
if errors.Is(err, unix.ENXIO) || errors.Is(err, os.ErrNotExist) {
return
}
if err == nil {
_ = unix.Close(fd)
}
time.Sleep(10 * time.Millisecond)
}

t.Fatalf("timed out waiting for startContainer hook to exit")
}
5 changes: 5 additions & 0 deletions libcontainer/standard_init_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ func (l *linuxStandardInit) Init() error {
return fmt.Errorf("reopen exec fifo: %w", err)
}
defer fifoFile.Close()

// Translate termination signals to conventional shell-style exit codes
// while PID 1 is still the Go-based runc init helper.
setupPreExecSignalExit()

if _, err := fifoFile.Write([]byte("0")); err != nil {
return &os.PathError{Op: "write exec fifo", Path: fifoFile.Name(), Err: err}
}
Expand Down
Loading