| // Copyright 2026 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package runtime_test |
| |
| import ( |
| "bufio" |
| "bytes" |
| "errors" |
| "internal/testenv" |
| "io" |
| "os" |
| "os/exec" |
| "strings" |
| "syscall" |
| "testing" |
| ) |
| |
| // TestSignalPid1 verifies that a Go program running as PID 1 with no |
| // SIGTERM handler provides a sane exit code upon receiving SIGTERM. |
| // |
| // The test is Linux-specific because it uses CLONE_NEWPID to run as PID 1. |
| func TestSignalPid1(t *testing.T) { |
| t.Parallel() |
| |
| exe, err := buildTestProg(t, "testprog") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| cmd := testenv.Command(t, exe, "SignalPid1") |
| cmd.SysProcAttr = &syscall.SysProcAttr{ |
| Cloneflags: syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, |
| UidMappings: []syscall.SysProcIDMap{ |
| {ContainerID: 0, HostID: os.Getuid(), Size: 1}, |
| }, |
| GidMappings: []syscall.SysProcIDMap{ |
| {ContainerID: 0, HostID: os.Getgid(), Size: 1}, |
| }, |
| } |
| stdout, err := cmd.StdoutPipe() |
| if err != nil { |
| t.Fatal(err) |
| } |
| var stderr bytes.Buffer |
| cmd.Stderr = &stderr |
| if err := cmd.Start(); err != nil { |
| t.Skipf("cannot create PID namespace (may require unprivileged user namespaces): %v", err) |
| } |
| |
| waited := false |
| defer func() { |
| if !waited { |
| cmd.Process.Kill() |
| cmd.Wait() |
| } |
| }() |
| |
| // Wait for child to signal readiness. |
| r := bufio.NewReader(stdout) |
| line, err := r.ReadString('\n') |
| if err != nil { |
| t.Fatalf("reading from child: %v", err) |
| } |
| if strings.TrimRight(line, "\n") != "ready" { |
| t.Fatalf("unexpected output from child: %q", line) |
| } |
| go io.Copy(io.Discard, r) // Drain any further output. |
| |
| const ( |
| signal = syscall.SIGTERM |
| expExitCode = int(128 + signal) |
| ) |
| // Send signal from outside the child PID namespace. |
| if err := cmd.Process.Signal(signal); err != nil { |
| t.Fatalf("sending signal %d (%q): %v", signal, signal, err) |
| } |
| |
| err = cmd.Wait() |
| waited = true |
| t.Logf("child: %v", err) |
| if s := stderr.String(); s != "" { |
| t.Fatalf("child stderr: %s", s) |
| } |
| if exitErr, ok := errors.AsType[*exec.ExitError](err); ok { |
| if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { |
| if ec := status.ExitStatus(); ec == expExitCode { |
| return // PASS. |
| } |
| } |
| } |
| |
| t.Errorf("Want child exited with %d, got: %+v", expExitCode, err) |
| } |