1// Copyright 2023 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5//go:build unix
6
7package runtime_test
8
9import (
10	"bytes"
11	"fmt"
12	"internal/testenv"
13	"io"
14	"os"
15	"os/exec"
16	"path/filepath"
17	"regexp"
18	"runtime"
19	"syscall"
20	"testing"
21)
22
23func canGenerateCore(t *testing.T) bool {
24	// Ensure there is enough RLIMIT_CORE available to generate a full core.
25	var lim syscall.Rlimit
26	err := syscall.Getrlimit(syscall.RLIMIT_CORE, &lim)
27	if err != nil {
28		t.Fatalf("error getting rlimit: %v", err)
29	}
30	// Minimum RLIMIT_CORE max to allow. This is a conservative estimate.
31	// Most systems allow infinity.
32	const minRlimitCore = 100 << 20 // 100 MB
33	if lim.Max < minRlimitCore {
34		t.Skipf("RLIMIT_CORE max too low: %#+v", lim)
35	}
36
37	// Make sure core pattern will send core to the current directory.
38	b, err := os.ReadFile("/proc/sys/kernel/core_pattern")
39	if err != nil {
40		t.Fatalf("error reading core_pattern: %v", err)
41	}
42	if string(b) != "core\n" {
43		t.Skipf("Unexpected core pattern %q", string(b))
44	}
45
46	coreUsesPID := false
47	b, err = os.ReadFile("/proc/sys/kernel/core_uses_pid")
48	if err == nil {
49		switch string(bytes.TrimSpace(b)) {
50		case "0":
51		case "1":
52			coreUsesPID = true
53		default:
54			t.Skipf("unexpected core_uses_pid value %q", string(b))
55		}
56	}
57	return coreUsesPID
58}
59
60const coreSignalSource = `
61package main
62
63import (
64	"flag"
65	"fmt"
66	"os"
67	"runtime/debug"
68	"syscall"
69)
70
71var pipeFD = flag.Int("pipe-fd", -1, "FD of write end of control pipe")
72
73func enableCore() {
74	debug.SetTraceback("crash")
75
76	var lim syscall.Rlimit
77	err := syscall.Getrlimit(syscall.RLIMIT_CORE, &lim)
78	if err != nil {
79		panic(fmt.Sprintf("error getting rlimit: %v", err))
80	}
81	lim.Cur = lim.Max
82	fmt.Fprintf(os.Stderr, "Setting RLIMIT_CORE = %+#v\n", lim)
83	err = syscall.Setrlimit(syscall.RLIMIT_CORE, &lim)
84	if err != nil {
85		panic(fmt.Sprintf("error setting rlimit: %v", err))
86	}
87}
88
89func main() {
90	flag.Parse()
91
92	enableCore()
93
94	// Ready to go. Notify parent.
95	if err := syscall.Close(*pipeFD); err != nil {
96		panic(fmt.Sprintf("error closing control pipe fd %d: %v", *pipeFD, err))
97	}
98
99	for {}
100}
101`
102
103// TestGdbCoreSignalBacktrace tests that gdb can unwind the stack correctly
104// through a signal handler in a core file
105func TestGdbCoreSignalBacktrace(t *testing.T) {
106	if runtime.GOOS != "linux" {
107		// N.B. This test isn't fundamentally Linux-only, but it needs
108		// to know how to enable/find core files on each OS.
109		t.Skip("Test only supported on Linux")
110	}
111	if runtime.GOARCH != "386" && runtime.GOARCH != "amd64" {
112		// TODO(go.dev/issue/25218): Other architectures use sigreturn
113		// via VDSO, which we somehow don't handle correctly.
114		t.Skip("Backtrace through signal handler only works on 386 and amd64")
115	}
116
117	checkGdbEnvironment(t)
118	t.Parallel()
119	checkGdbVersion(t)
120
121	coreUsesPID := canGenerateCore(t)
122
123	// Build the source code.
124	dir := t.TempDir()
125	src := filepath.Join(dir, "main.go")
126	err := os.WriteFile(src, []byte(coreSignalSource), 0644)
127	if err != nil {
128		t.Fatalf("failed to create file: %v", err)
129	}
130	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go")
131	cmd.Dir = dir
132	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
133	if err != nil {
134		t.Fatalf("building source %v\n%s", err, out)
135	}
136
137	r, w, err := os.Pipe()
138	if err != nil {
139		t.Fatalf("error creating control pipe: %v", err)
140	}
141	defer r.Close()
142
143	// Start the test binary.
144	cmd = testenv.Command(t, "./a.exe", "-pipe-fd=3")
145	cmd.Dir = dir
146	cmd.ExtraFiles = []*os.File{w}
147	var output bytes.Buffer
148	cmd.Stdout = &output // for test logging
149	cmd.Stderr = &output
150
151	if err := cmd.Start(); err != nil {
152		t.Fatalf("error starting test binary: %v", err)
153	}
154	w.Close()
155
156	pid := cmd.Process.Pid
157
158	// Wait for child to be ready.
159	var buf [1]byte
160	if _, err := r.Read(buf[:]); err != io.EOF {
161		t.Fatalf("control pipe read get err %v want io.EOF", err)
162	}
163
164	// ��
165	if err := cmd.Process.Signal(os.Signal(syscall.SIGABRT)); err != nil {
166		t.Fatalf("erroring signaling child: %v", err)
167	}
168
169	err = cmd.Wait()
170	t.Logf("child output:\n%s", output.String())
171	if err == nil {
172		t.Fatalf("Wait succeeded, want SIGABRT")
173	}
174	ee, ok := err.(*exec.ExitError)
175	if !ok {
176		t.Fatalf("Wait err got %T %v, want exec.ExitError", ee, ee)
177	}
178	ws, ok := ee.Sys().(syscall.WaitStatus)
179	if !ok {
180		t.Fatalf("Sys got %T %v, want syscall.WaitStatus", ee.Sys(), ee.Sys())
181	}
182	if ws.Signal() != syscall.SIGABRT {
183		t.Fatalf("Signal got %d want SIGABRT", ws.Signal())
184	}
185	if !ws.CoreDump() {
186		t.Fatalf("CoreDump got %v want true", ws.CoreDump())
187	}
188
189	coreFile := "core"
190	if coreUsesPID {
191		coreFile += fmt.Sprintf(".%d", pid)
192	}
193
194	// Execute gdb commands.
195	args := []string{"-nx", "-batch",
196		"-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"),
197		"-ex", "backtrace",
198		filepath.Join(dir, "a.exe"),
199		filepath.Join(dir, coreFile),
200	}
201	cmd = testenv.Command(t, "gdb", args...)
202
203	got, err := cmd.CombinedOutput()
204	t.Logf("gdb output:\n%s", got)
205	if err != nil {
206		t.Fatalf("gdb exited with error: %v", err)
207	}
208
209	// We don't know which thread the fatal signal will land on, but we can still check for basics:
210	//
211	// 1. A frame in the signal handler: runtime.sigtramp
212	// 2. GDB detection of the signal handler: <signal handler called>
213	// 3. A frame before the signal handler: this could be foo, or somewhere in the scheduler
214
215	re := regexp.MustCompile(`#.* runtime\.sigtramp `)
216	if found := re.Find(got) != nil; !found {
217		t.Fatalf("could not find sigtramp in backtrace")
218	}
219
220	re = regexp.MustCompile("#.* <signal handler called>")
221	loc := re.FindIndex(got)
222	if loc == nil {
223		t.Fatalf("could not find signal handler marker in backtrace")
224	}
225	rest := got[loc[1]:]
226
227	// Look for any frames after the signal handler. We want to see
228	// symbolized frames, not garbage unknown frames.
229	//
230	// Since the signal might not be delivered to the main thread we can't
231	// look for main.main. Every thread should have a runtime frame though.
232	re = regexp.MustCompile(`#.* runtime\.`)
233	if found := re.Find(rest) != nil; !found {
234		t.Fatalf("could not find runtime symbol in backtrace after signal handler:\n%s", rest)
235	}
236}
237
238const coreCrashThreadSource = `
239package main
240
241/*
242#cgo CFLAGS: -g -O0
243#include <stdio.h>
244#include <stddef.h>
245void trigger_crash()
246{
247	int* ptr = NULL;
248	*ptr = 1024;
249}
250*/
251import "C"
252import (
253	"flag"
254	"fmt"
255	"os"
256	"runtime/debug"
257	"syscall"
258)
259
260func enableCore() {
261	debug.SetTraceback("crash")
262
263	var lim syscall.Rlimit
264	err := syscall.Getrlimit(syscall.RLIMIT_CORE, &lim)
265	if err != nil {
266		panic(fmt.Sprintf("error getting rlimit: %v", err))
267	}
268	lim.Cur = lim.Max
269	fmt.Fprintf(os.Stderr, "Setting RLIMIT_CORE = %+#v\n", lim)
270	err = syscall.Setrlimit(syscall.RLIMIT_CORE, &lim)
271	if err != nil {
272		panic(fmt.Sprintf("error setting rlimit: %v", err))
273	}
274}
275
276func main() {
277	flag.Parse()
278
279	enableCore()
280
281	C.trigger_crash()
282}
283`
284
285// TestGdbCoreCrashThreadBacktrace tests that runtime could let the fault thread to crash process
286// and make fault thread as number one thread while gdb in a core file
287func TestGdbCoreCrashThreadBacktrace(t *testing.T) {
288	if runtime.GOOS != "linux" {
289		// N.B. This test isn't fundamentally Linux-only, but it needs
290		// to know how to enable/find core files on each OS.
291		t.Skip("Test only supported on Linux")
292	}
293	if runtime.GOARCH != "386" && runtime.GOARCH != "amd64" {
294		// TODO(go.dev/issue/25218): Other architectures use sigreturn
295		// via VDSO, which we somehow don't handle correctly.
296		t.Skip("Backtrace through signal handler only works on 386 and amd64")
297	}
298
299	testenv.SkipFlaky(t, 65138)
300
301	testenv.MustHaveCGO(t)
302	checkGdbEnvironment(t)
303	t.Parallel()
304	checkGdbVersion(t)
305
306	coreUsesPID := canGenerateCore(t)
307
308	// Build the source code.
309	dir := t.TempDir()
310	src := filepath.Join(dir, "main.go")
311	err := os.WriteFile(src, []byte(coreCrashThreadSource), 0644)
312	if err != nil {
313		t.Fatalf("failed to create file: %v", err)
314	}
315	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go")
316	cmd.Dir = dir
317	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
318	if err != nil {
319		t.Fatalf("building source %v\n%s", err, out)
320	}
321
322	// Start the test binary.
323	cmd = testenv.Command(t, "./a.exe")
324	cmd.Dir = dir
325	var output bytes.Buffer
326	cmd.Stdout = &output // for test logging
327	cmd.Stderr = &output
328
329	if err := cmd.Start(); err != nil {
330		t.Fatalf("error starting test binary: %v", err)
331	}
332
333	pid := cmd.Process.Pid
334
335	err = cmd.Wait()
336	t.Logf("child output:\n%s", output.String())
337	if err == nil {
338		t.Fatalf("Wait succeeded, want SIGABRT")
339	}
340	ee, ok := err.(*exec.ExitError)
341	if !ok {
342		t.Fatalf("Wait err got %T %v, want exec.ExitError", ee, ee)
343	}
344	ws, ok := ee.Sys().(syscall.WaitStatus)
345	if !ok {
346		t.Fatalf("Sys got %T %v, want syscall.WaitStatus", ee.Sys(), ee.Sys())
347	}
348	if ws.Signal() != syscall.SIGABRT {
349		t.Fatalf("Signal got %d want SIGABRT", ws.Signal())
350	}
351	if !ws.CoreDump() {
352		t.Fatalf("CoreDump got %v want true", ws.CoreDump())
353	}
354
355	coreFile := "core"
356	if coreUsesPID {
357		coreFile += fmt.Sprintf(".%d", pid)
358	}
359
360	// Execute gdb commands.
361	args := []string{"-nx", "-batch",
362		"-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"),
363		"-ex", "backtrace",
364		filepath.Join(dir, "a.exe"),
365		filepath.Join(dir, coreFile),
366	}
367	cmd = testenv.Command(t, "gdb", args...)
368
369	got, err := cmd.CombinedOutput()
370	t.Logf("gdb output:\n%s", got)
371	if err != nil {
372		t.Fatalf("gdb exited with error: %v", err)
373	}
374
375	re := regexp.MustCompile(`#.* trigger_crash`)
376	if found := re.Find(got) != nil; !found {
377		t.Fatalf("could not find trigger_crash in backtrace")
378	}
379}
380