package libcontainer import ( "errors" "os" "os/exec" "runtime" "testing" "time" "golang.org/x/sys/unix" ) const zombieLeaderEnv = "RUNC_TEST_ZOMBIE_LEADER" // init turns this process into the helper for TestWaitForInitWriteZombieLeader // when asked to: it kills its own thread group leader while the rest of its // threads (and thus all of its file descriptors) stay alive, the way seccomp's // SCMP_ACT_KILL does to "runc init". // // This has to happen from init, because that is the only place where the Go // runtime guarantees we are running on the main thread (i.e. on the thread // group leader). func init() { if os.Getenv(zombieLeaderEnv) != "1" { return } // Keep a non-leader thread around forever. It has to block in a syscall // rather than on a channel, or the Go runtime declares a deadlock and // terminates the whole process. started := make(chan struct{}) go func() { runtime.LockOSThread() close(started) for { _ = unix.Pause() } }() <-started if unix.Gettid() != unix.Getpid() { panic("zombie leader helper is not running on the thread group leader") } // Terminate this thread, and only this thread. _, _, _ = unix.RawSyscall(unix.SYS_EXIT, 0, 0, 0) } func TestWaitForInitWriteZombieLeader(t *testing.T) { r, w, err := os.Pipe() if err != nil { t.Fatal(err) } defer r.Close() cmd := exec.Command(os.Args[0], "-test.run=^$") cmd.Env = append(os.Environ(), zombieLeaderEnv+"=1") cmd.ExtraFiles = []*os.File{w} if err := cmd.Start(); err != nil { t.Fatal(err) } // Only the helper is supposed to hold the writing end now. w.Close() defer func() { // SIGKILL is group-wide, so it does what the helper's own // exit(2) could not. _ = cmd.Process.Kill() _ = cmd.Wait() }() done := make(chan error, 1) go func() { done <- waitForInitWrite(int(r.Fd()), cmd.Process.Pid) }() select { case err := <-done: if !errors.Is(err, errInitDead) { t.Fatalf("waitForInitWrite: want %v, got %v", errInitDead, err) } case <-time.After(30 * time.Second): t.Fatal("waitForInitWrite did not notice the dead init") } } func TestWaitForInitWriteReadable(t *testing.T) { r, w, err := os.Pipe() if err != nil { t.Fatal(err) } defer r.Close() defer w.Close() if _, err := w.Write([]byte("0")); err != nil { t.Fatal(err) } // A readable fd has to win over the liveness check. checkInitDead = func(int) bool { t.Error("liveness checked while the fd is readable") return true } defer func() { checkInitDead = initDead }() if err := waitForInitWrite(int(r.Fd()), 1); err != nil { t.Fatalf("waitForInitWrite: %v", err) } } // Init may write something and die right after poll(2) has timed out, but // before its death is noticed. What it wrote must not be lost. func TestWaitForInitWriteThenDie(t *testing.T) { r, w, err := os.Pipe() if err != nil { t.Fatal(err) } defer r.Close() defer w.Close() checkInitDead = func(int) bool { if _, err := w.Write([]byte("0")); err != nil { t.Error(err) } return true } defer func() { checkInitDead = initDead }() if err := waitForInitWrite(int(r.Fd()), 1); err != nil { t.Fatalf("waitForInitWrite: %v", err) } }