1// Copyright 2024 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
5package runtime_test
6
7// This test of GOTRACEBACK=system has its own file,
8// to minimize line-number perturbation.
9
10import (
11	"bytes"
12	"fmt"
13	"internal/testenv"
14	"io"
15	"os"
16	"path/filepath"
17	"reflect"
18	"runtime"
19	"runtime/debug"
20	"strconv"
21	"strings"
22	"testing"
23)
24
25// This is the entrypoint of the child process used by
26// TestTracebackSystem. It prints a crash report to stdout.
27func crash() {
28	// Ensure that we get pc=0x%x values in the traceback.
29	debug.SetTraceback("system")
30	writeSentinel(os.Stdout)
31	debug.SetCrashOutput(os.Stdout, debug.CrashOptions{})
32
33	go func() {
34		// This call is typically inlined.
35		child1()
36	}()
37	select {}
38}
39
40func child1() {
41	child2()
42}
43
44func child2() {
45	child3()
46}
47
48func child3() {
49	child4()
50}
51
52func child4() {
53	child5()
54}
55
56//go:noinline
57func child5() { // test trace through second of two call instructions
58	child6bad()
59	child6() // appears in stack trace
60}
61
62//go:noinline
63func child6bad() {
64}
65
66//go:noinline
67func child6() { // test trace through first of two call instructions
68	child7() // appears in stack trace
69	child7bad()
70}
71
72//go:noinline
73func child7bad() {
74}
75
76//go:noinline
77func child7() {
78	// Write runtime.Caller's view of the stack to stderr, for debugging.
79	var pcs [16]uintptr
80	n := runtime.Callers(1, pcs[:])
81	fmt.Fprintf(os.Stderr, "Callers: %#x\n", pcs[:n])
82	io.WriteString(os.Stderr, formatStack(pcs[:n]))
83
84	// Cause the crash report to be written to stdout.
85	panic("oops")
86}
87
88// TestTracebackSystem tests that the syntax of crash reports produced
89// by GOTRACEBACK=system (see traceback2) contains a complete,
90// parseable list of program counters for the running goroutine that
91// can be parsed and fed to runtime.CallersFrames to obtain accurate
92// information about the logical call stack, even in the presence of
93// inlining.
94//
95// The test is a distillation of the crash monitor in
96// golang.org/x/telemetry/crashmonitor.
97func TestTracebackSystem(t *testing.T) {
98	testenv.MustHaveExec(t)
99	if runtime.GOOS == "android" {
100		t.Skip("Can't read source code for this file on Android")
101	}
102
103	// Fork+exec the crashing process.
104	exe, err := os.Executable()
105	if err != nil {
106		t.Fatal(err)
107	}
108	cmd := testenv.Command(t, exe)
109	cmd.Env = append(cmd.Environ(), entrypointVar+"=crash")
110	var stdout, stderr bytes.Buffer
111	cmd.Stdout = &stdout
112	cmd.Stderr = &stderr
113	cmd.Run() // expected to crash
114	t.Logf("stderr:\n%s\nstdout: %s\n", stderr.Bytes(), stdout.Bytes())
115	crash := stdout.String()
116
117	// If the only line is the sentinel, it wasn't a crash.
118	if strings.Count(crash, "\n") < 2 {
119		t.Fatalf("child process did not produce a crash report")
120	}
121
122	// Parse the PCs out of the child's crash report.
123	pcs, err := parseStackPCs(crash)
124	if err != nil {
125		t.Fatal(err)
126	}
127
128	// Unwind the stack using this executable's symbol table.
129	got := formatStack(pcs)
130	want := `redacted.go:0: runtime.gopanic
131traceback_system_test.go:85: runtime_test.child7: 	panic("oops")
132traceback_system_test.go:68: runtime_test.child6: 	child7() // appears in stack trace
133traceback_system_test.go:59: runtime_test.child5: 	child6() // appears in stack trace
134traceback_system_test.go:53: runtime_test.child4: 	child5()
135traceback_system_test.go:49: runtime_test.child3: 	child4()
136traceback_system_test.go:45: runtime_test.child2: 	child3()
137traceback_system_test.go:41: runtime_test.child1: 	child2()
138traceback_system_test.go:35: runtime_test.crash.func1: 		child1()
139redacted.go:0: runtime.goexit
140`
141	if strings.TrimSpace(got) != strings.TrimSpace(want) {
142		t.Errorf("got:\n%swant:\n%s", got, want)
143	}
144}
145
146// parseStackPCs parses the parent process's program counters for the
147// first running goroutine out of a GOTRACEBACK=system traceback,
148// adjusting them so that they are valid for the child process's text
149// segment.
150//
151// This function returns only program counter values, ensuring that
152// there is no possibility of strings from the crash report (which may
153// contain PII) leaking into the telemetry system.
154//
155// (Copied from golang.org/x/telemetry/crashmonitor.parseStackPCs.)
156func parseStackPCs(crash string) ([]uintptr, error) {
157	// getPC parses the PC out of a line of the form:
158	//     \tFILE:LINE +0xRELPC sp=... fp=... pc=...
159	getPC := func(line string) (uint64, error) {
160		_, pcstr, ok := strings.Cut(line, " pc=") // e.g. pc=0x%x
161		if !ok {
162			return 0, fmt.Errorf("no pc= for stack frame: %s", line)
163		}
164		return strconv.ParseUint(pcstr, 0, 64) // 0 => allow 0x prefix
165	}
166
167	var (
168		pcs            []uintptr
169		parentSentinel uint64
170		childSentinel  = sentinel()
171		on             = false // are we in the first running goroutine?
172		lines          = strings.Split(crash, "\n")
173	)
174	for i := 0; i < len(lines); i++ {
175		line := lines[i]
176
177		// Read sentinel value.
178		if parentSentinel == 0 && strings.HasPrefix(line, "sentinel ") {
179			_, err := fmt.Sscanf(line, "sentinel %x", &parentSentinel)
180			if err != nil {
181				return nil, fmt.Errorf("can't read sentinel line")
182			}
183			continue
184		}
185
186		// Search for "goroutine GID [STATUS]"
187		if !on {
188			if strings.HasPrefix(line, "goroutine ") &&
189				strings.Contains(line, " [running]:") {
190				on = true
191
192				if parentSentinel == 0 {
193					return nil, fmt.Errorf("no sentinel value in crash report")
194				}
195			}
196			continue
197		}
198
199		// A blank line marks end of a goroutine stack.
200		if line == "" {
201			break
202		}
203
204		// Skip the final "created by SYMBOL in goroutine GID" part.
205		if strings.HasPrefix(line, "created by ") {
206			break
207		}
208
209		// Expect a pair of lines:
210		//   SYMBOL(ARGS)
211		//   \tFILE:LINE +0xRELPC sp=0x%x fp=0x%x pc=0x%x
212		// Note: SYMBOL may contain parens "pkg.(*T).method"
213		// The RELPC is sometimes missing.
214
215		// Skip the symbol(args) line.
216		i++
217		if i == len(lines) {
218			break
219		}
220		line = lines[i]
221
222		// Parse the PC, and correct for the parent and child's
223		// different mappings of the text section.
224		pc, err := getPC(line)
225		if err != nil {
226			// Inlined frame, perhaps; skip it.
227			continue
228		}
229		pcs = append(pcs, uintptr(pc-parentSentinel+childSentinel))
230	}
231	return pcs, nil
232}
233
234// The sentinel function returns its address. The difference between
235// this value as observed by calls in two different processes of the
236// same executable tells us the relative offset of their text segments.
237//
238// It would be nice if SetCrashOutput took care of this as it's fiddly
239// and likely to confuse every user at first.
240func sentinel() uint64 {
241	return uint64(reflect.ValueOf(sentinel).Pointer())
242}
243
244func writeSentinel(out io.Writer) {
245	fmt.Fprintf(out, "sentinel %x\n", sentinel())
246}
247
248// formatStack formats a stack of PC values using the symbol table,
249// redacting information that cannot be relied upon in the test.
250func formatStack(pcs []uintptr) string {
251	// When debugging, show file/line/content of files other than this one.
252	const debug = false
253
254	var buf strings.Builder
255	i := 0
256	frames := runtime.CallersFrames(pcs)
257	for {
258		fr, more := frames.Next()
259		if debug {
260			fmt.Fprintf(&buf, "pc=%x ", pcs[i])
261			i++
262		}
263		if base := filepath.Base(fr.File); base == "traceback_system_test.go" || debug {
264			content, err := os.ReadFile(fr.File)
265			if err != nil {
266				panic(err)
267			}
268			lines := bytes.Split(content, []byte("\n"))
269			fmt.Fprintf(&buf, "%s:%d: %s: %s\n", base, fr.Line, fr.Function, lines[fr.Line-1])
270		} else {
271			// For robustness, don't show file/line for functions from other files.
272			fmt.Fprintf(&buf, "redacted.go:0: %s\n", fr.Function)
273		}
274
275		if !more {
276			break
277		}
278	}
279	return buf.String()
280}
281