summaryrefslogtreecommitdiffstats
path: root/src/runtime/runtime-gdb_unix_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/runtime/runtime-gdb_unix_test.go')
-rw-r--r--src/runtime/runtime-gdb_unix_test.go379
1 files changed, 379 insertions, 0 deletions
diff --git a/src/runtime/runtime-gdb_unix_test.go b/src/runtime/runtime-gdb_unix_test.go
new file mode 100644
index 0000000..8b602d1
--- /dev/null
+++ b/src/runtime/runtime-gdb_unix_test.go
@@ -0,0 +1,379 @@
+// 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")
+ }
+}