| // Copyright 2023 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. |
| |
| //go:build unix |
| |
| package runtime_test |
| |
| import ( |
| "bytes" |
| "fmt" |
| "internal/testenv" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "syscall" |
| "testing" |
| ) |
| |
| func canGenerateCore(t *testing.T) bool { |
| // Ensure there is enough RLIMIT_CORE available to generate a full core. |
| var lim syscall.Rlimit |
| err := syscall.Getrlimit(syscall.RLIMIT_CORE, &lim) |
| if err != nil { |
| t.Fatalf("error getting rlimit: %v", err) |
| } |
| // Minimum RLIMIT_CORE max to allow. This is a conservative estimate. |
| // Most systems allow infinity. |
| const minRlimitCore = 100 << 20 // 100 MB |
| if lim.Max < minRlimitCore { |
| t.Skipf("RLIMIT_CORE max too low: %#+v", lim) |
| } |
| |
| // Make sure core pattern will send core to the current directory. |
| b, err := os.ReadFile("/proc/sys/kernel/core_pattern") |
| if err != nil { |
| t.Fatalf("error reading core_pattern: %v", err) |
| } |
| if string(b) != "core\n" { |
| t.Skipf("Unexpected core pattern %q", string(b)) |
| } |
| |
| coreUsesPID := false |
| b, err = os.ReadFile("/proc/sys/kernel/core_uses_pid") |
| if err == nil { |
| switch string(bytes.TrimSpace(b)) { |
| case "0": |
| case "1": |
| coreUsesPID = true |
| default: |
| t.Skipf("unexpected core_uses_pid value %q", string(b)) |
| } |
| } |
| return coreUsesPID |
| } |
| |
| const coreSignalSource = ` |
| package main |
| |
| import ( |
| "flag" |
| "fmt" |
| "os" |
| "runtime/debug" |
| "syscall" |
| ) |
| |
| var pipeFD = flag.Int("pipe-fd", -1, "FD of write end of control pipe") |
| |
| func enableCore() { |
| debug.SetTraceback("crash") |
| |
| var lim syscall.Rlimit |
| err := syscall.Getrlimit(syscall.RLIMIT_CORE, &lim) |
| if err != nil { |
| panic(fmt.Sprintf("error getting rlimit: %v", err)) |
| } |
| lim.Cur = lim.Max |
| fmt.Fprintf(os.Stderr, "Setting RLIMIT_CORE = %+#v\n", lim) |
| err = syscall.Setrlimit(syscall.RLIMIT_CORE, &lim) |
| if err != nil { |
| panic(fmt.Sprintf("error setting rlimit: %v", err)) |
| } |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| enableCore() |
| |
| // Ready to go. Notify parent. |
| if err := syscall.Close(*pipeFD); err != nil { |
| panic(fmt.Sprintf("error closing control pipe fd %d: %v", *pipeFD, err)) |
| } |
| |
| for {} |
| } |
| ` |
| |
| // TestGdbCoreSignalBacktrace tests that gdb can unwind the stack correctly |
| // through a signal handler in a core file |
| func TestGdbCoreSignalBacktrace(t *testing.T) { |
| if runtime.GOOS != "linux" { |
| // N.B. This test isn't fundamentally Linux-only, but it needs |
| // to know how to enable/find core files on each OS. |
| t.Skip("Test only supported on Linux") |
| } |
| if runtime.GOARCH != "386" && runtime.GOARCH != "amd64" { |
| // TODO(go.dev/issue/25218): Other architectures use sigreturn |
| // via VDSO, which we somehow don't handle correctly. |
| t.Skip("Backtrace through signal handler only works on 386 and amd64") |
| } |
| |
| checkGdbEnvironment(t) |
| t.Parallel() |
| checkGdbVersion(t) |
| |
| coreUsesPID := canGenerateCore(t) |
| |
| // Build the source code. |
| dir := t.TempDir() |
| src := filepath.Join(dir, "main.go") |
| err := os.WriteFile(src, []byte(coreSignalSource), 0644) |
| if err != nil { |
| t.Fatalf("failed to create file: %v", err) |
| } |
| cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go") |
| cmd.Dir = dir |
| out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() |
| if err != nil { |
| t.Fatalf("building source %v\n%s", err, out) |
| } |
| |
| r, w, err := os.Pipe() |
| if err != nil { |
| t.Fatalf("error creating control pipe: %v", err) |
| } |
| defer r.Close() |
| |
| // Start the test binary. |
| cmd = testenv.Command(t, "./a.exe", "-pipe-fd=3") |
| cmd.Dir = dir |
| cmd.ExtraFiles = []*os.File{w} |
| var output bytes.Buffer |
| cmd.Stdout = &output // for test logging |
| cmd.Stderr = &output |
| |
| if err := cmd.Start(); err != nil { |
| t.Fatalf("error starting test binary: %v", err) |
| } |
| w.Close() |
| |
| pid := cmd.Process.Pid |
| |
| // Wait for child to be ready. |
| var buf [1]byte |
| if _, err := r.Read(buf[:]); err != io.EOF { |
| t.Fatalf("control pipe read get err %v want io.EOF", err) |
| } |
| |
| // 💥 |
| if err := cmd.Process.Signal(os.Signal(syscall.SIGABRT)); err != nil { |
| t.Fatalf("erroring signaling child: %v", err) |
| } |
| |
| err = cmd.Wait() |
| t.Logf("child output:\n%s", output.String()) |
| if err == nil { |
| t.Fatalf("Wait succeeded, want SIGABRT") |
| } |
| ee, ok := err.(*exec.ExitError) |
| if !ok { |
| t.Fatalf("Wait err got %T %v, want exec.ExitError", ee, ee) |
| } |
| ws, ok := ee.Sys().(syscall.WaitStatus) |
| if !ok { |
| t.Fatalf("Sys got %T %v, want syscall.WaitStatus", ee.Sys(), ee.Sys()) |
| } |
| if ws.Signal() != syscall.SIGABRT { |
| t.Fatalf("Signal got %d want SIGABRT", ws.Signal()) |
| } |
| if !ws.CoreDump() { |
| t.Fatalf("CoreDump got %v want true", ws.CoreDump()) |
| } |
| |
| coreFile := "core" |
| if coreUsesPID { |
| coreFile += fmt.Sprintf(".%d", pid) |
| } |
| |
| // Execute gdb commands. |
| args := []string{"-nx", "-batch", |
| "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), |
| "-ex", "backtrace", |
| filepath.Join(dir, "a.exe"), |
| filepath.Join(dir, coreFile), |
| } |
| cmd = testenv.Command(t, "gdb", args...) |
| |
| got, err := cmd.CombinedOutput() |
| t.Logf("gdb output:\n%s", got) |
| if err != nil { |
| t.Fatalf("gdb exited with error: %v", err) |
| } |
| |
| // We don't know which thread the fatal signal will land on, but we can still check for basics: |
| // |
| // 1. A frame in the signal handler: runtime.sigtramp |
| // 2. GDB detection of the signal handler: <signal handler called> |
| // 3. A frame before the signal handler: this could be foo, or somewhere in the scheduler |
| |
| re := regexp.MustCompile(`#.* runtime\.sigtramp `) |
| if found := re.Find(got) != nil; !found { |
| t.Fatalf("could not find sigtramp in backtrace") |
| } |
| |
| re = regexp.MustCompile("#.* <signal handler called>") |
| loc := re.FindIndex(got) |
| if loc == nil { |
| t.Fatalf("could not find signal handler marker in backtrace") |
| } |
| rest := got[loc[1]:] |
| |
| // Look for any frames after the signal handler. We want to see |
| // symbolized frames, not garbage unknown frames. |
| // |
| // Since the signal might not be delivered to the main thread we can't |
| // look for main.main. Every thread should have a runtime frame though. |
| re = regexp.MustCompile(`#.* runtime\.`) |
| if found := re.Find(rest) != nil; !found { |
| t.Fatalf("could not find runtime symbol in backtrace after signal handler:\n%s", rest) |
| } |
| } |
| |
| const coreCrashThreadSource = ` |
| package main |
| |
| /* |
| #cgo CFLAGS: -g -O0 |
| #include <stdio.h> |
| #include <stddef.h> |
| void trigger_crash() |
| { |
| int* ptr = NULL; |
| *ptr = 1024; |
| } |
| */ |
| import "C" |
| import ( |
| "flag" |
| "fmt" |
| "os" |
| "runtime/debug" |
| "syscall" |
| ) |
| |
| func enableCore() { |
| debug.SetTraceback("crash") |
| |
| var lim syscall.Rlimit |
| err := syscall.Getrlimit(syscall.RLIMIT_CORE, &lim) |
| if err != nil { |
| panic(fmt.Sprintf("error getting rlimit: %v", err)) |
| } |
| lim.Cur = lim.Max |
| fmt.Fprintf(os.Stderr, "Setting RLIMIT_CORE = %+#v\n", lim) |
| err = syscall.Setrlimit(syscall.RLIMIT_CORE, &lim) |
| if err != nil { |
| panic(fmt.Sprintf("error setting rlimit: %v", err)) |
| } |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| enableCore() |
| |
| C.trigger_crash() |
| } |
| ` |
| |
| // TestGdbCoreCrashThreadBacktrace tests that runtime could let the fault thread to crash process |
| // and make fault thread as number one thread while gdb in a core file |
| func TestGdbCoreCrashThreadBacktrace(t *testing.T) { |
| if runtime.GOOS != "linux" { |
| // N.B. This test isn't fundamentally Linux-only, but it needs |
| // to know how to enable/find core files on each OS. |
| t.Skip("Test only supported on Linux") |
| } |
| if runtime.GOARCH != "386" && runtime.GOARCH != "amd64" { |
| // TODO(go.dev/issue/25218): Other architectures use sigreturn |
| // via VDSO, which we somehow don't handle correctly. |
| t.Skip("Backtrace through signal handler only works on 386 and amd64") |
| } |
| |
| testenv.SkipFlaky(t, 65138) |
| |
| testenv.MustHaveCGO(t) |
| checkGdbEnvironment(t) |
| t.Parallel() |
| checkGdbVersion(t) |
| |
| coreUsesPID := canGenerateCore(t) |
| |
| // Build the source code. |
| dir := t.TempDir() |
| src := filepath.Join(dir, "main.go") |
| err := os.WriteFile(src, []byte(coreCrashThreadSource), 0644) |
| if err != nil { |
| t.Fatalf("failed to create file: %v", err) |
| } |
| cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go") |
| cmd.Dir = dir |
| out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() |
| if err != nil { |
| t.Fatalf("building source %v\n%s", err, out) |
| } |
| |
| // Start the test binary. |
| cmd = testenv.Command(t, "./a.exe") |
| cmd.Dir = dir |
| var output bytes.Buffer |
| cmd.Stdout = &output // for test logging |
| cmd.Stderr = &output |
| |
| if err := cmd.Start(); err != nil { |
| t.Fatalf("error starting test binary: %v", err) |
| } |
| |
| pid := cmd.Process.Pid |
| |
| err = cmd.Wait() |
| t.Logf("child output:\n%s", output.String()) |
| if err == nil { |
| t.Fatalf("Wait succeeded, want SIGABRT") |
| } |
| ee, ok := err.(*exec.ExitError) |
| if !ok { |
| t.Fatalf("Wait err got %T %v, want exec.ExitError", ee, ee) |
| } |
| ws, ok := ee.Sys().(syscall.WaitStatus) |
| if !ok { |
| t.Fatalf("Sys got %T %v, want syscall.WaitStatus", ee.Sys(), ee.Sys()) |
| } |
| if ws.Signal() != syscall.SIGABRT { |
| t.Fatalf("Signal got %d want SIGABRT", ws.Signal()) |
| } |
| if !ws.CoreDump() { |
| t.Fatalf("CoreDump got %v want true", ws.CoreDump()) |
| } |
| |
| coreFile := "core" |
| if coreUsesPID { |
| coreFile += fmt.Sprintf(".%d", pid) |
| } |
| |
| // Execute gdb commands. |
| args := []string{"-nx", "-batch", |
| "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), |
| "-ex", "backtrace", |
| filepath.Join(dir, "a.exe"), |
| filepath.Join(dir, coreFile), |
| } |
| cmd = testenv.Command(t, "gdb", args...) |
| |
| got, err := cmd.CombinedOutput() |
| t.Logf("gdb output:\n%s", got) |
| if err != nil { |
| t.Fatalf("gdb exited with error: %v", err) |
| } |
| |
| re := regexp.MustCompile(`#.* trigger_crash`) |
| if found := re.Find(got) != nil; !found { |
| t.Fatalf("could not find trigger_crash in backtrace") |
| } |
| } |