blob: f9cc64803e8a91e5a0e9e0e9d21bbfe287669dc9 [file] [log] [blame]
// 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"
"internal/testenv"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"syscall"
"testing"
)
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)
// 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))
}
dir := t.TempDir()
// Build the source code.
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()
// 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())
}
// 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, "core"),
}
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)
}
}