| // Copyright 2024 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. |
| |
| package runtime_test |
| |
| // This test of GOTRACEBACK=system has its own file, |
| // to minimize line-number perturbation. |
| |
| import ( |
| "bytes" |
| "fmt" |
| "internal/testenv" |
| "io" |
| "os" |
| "path/filepath" |
| "reflect" |
| "runtime" |
| "runtime/debug" |
| "strconv" |
| "strings" |
| "testing" |
| ) |
| |
| // This is the entrypoint of the child process used by |
| // TestTracebackSystem. It prints a crash report to stdout. |
| func crash() { |
| // Ensure that we get pc=0x%x values in the traceback. |
| debug.SetTraceback("system") |
| writeSentinel(os.Stdout) |
| debug.SetCrashOutput(os.Stdout) |
| |
| go func() { |
| // This call is typically inlined. |
| child1() |
| }() |
| select {} |
| } |
| |
| func child1() { |
| child2() |
| } |
| |
| func child2() { |
| child3() |
| } |
| |
| func child3() { |
| child4() |
| } |
| |
| func child4() { |
| child5() |
| } |
| |
| //go:noinline |
| func child5() { // test trace through second of two call instructions |
| child6bad() |
| child6() // appears in stack trace |
| } |
| |
| //go:noinline |
| func child6bad() { |
| } |
| |
| //go:noinline |
| func child6() { // test trace through first of two call instructions |
| child7() // appears in stack trace |
| child7bad() |
| } |
| |
| //go:noinline |
| func child7bad() { |
| } |
| |
| //go:noinline |
| func child7() { |
| // Write runtime.Caller's view of the stack to stderr, for debugging. |
| var pcs [16]uintptr |
| n := runtime.Callers(1, pcs[:]) |
| fmt.Fprintf(os.Stderr, "Callers: %#x\n", pcs[:n]) |
| io.WriteString(os.Stderr, formatStack(pcs[:n])) |
| |
| // Cause the crash report to be written to stdout. |
| panic("oops") |
| } |
| |
| // TestTracebackSystem tests that the syntax of crash reports produced |
| // by GOTRACEBACK=system (see traceback2) contains a complete, |
| // parseable list of program counters for the running goroutine that |
| // can be parsed and fed to runtime.CallersFrames to obtain accurate |
| // information about the logical call stack, even in the presence of |
| // inlining. |
| // |
| // The test is a distillation of the crash monitor in |
| // golang.org/x/telemetry/crashmonitor. |
| func TestTracebackSystem(t *testing.T) { |
| testenv.MustHaveExec(t) |
| if runtime.GOOS == "android" { |
| t.Skip("Can't read source code for this file on Android") |
| } |
| |
| // Fork+exec the crashing process. |
| exe, err := os.Executable() |
| if err != nil { |
| t.Fatal(err) |
| } |
| cmd := testenv.Command(t, exe) |
| cmd.Env = append(cmd.Environ(), entrypointVar+"=crash") |
| var stdout, stderr bytes.Buffer |
| cmd.Stdout = &stdout |
| cmd.Stderr = &stderr |
| cmd.Run() // expected to crash |
| t.Logf("stderr:\n%s\nstdout: %s\n", stderr.Bytes(), stdout.Bytes()) |
| crash := stdout.String() |
| |
| // If the only line is the sentinel, it wasn't a crash. |
| if strings.Count(crash, "\n") < 2 { |
| t.Fatalf("child process did not produce a crash report") |
| } |
| |
| // Parse the PCs out of the child's crash report. |
| pcs, err := parseStackPCs(crash) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Unwind the stack using this executable's symbol table. |
| got := formatStack(pcs) |
| want := `redacted.go:0: runtime.gopanic |
| traceback_system_test.go:85: runtime_test.child7: panic("oops") |
| traceback_system_test.go:68: runtime_test.child6: child7() // appears in stack trace |
| traceback_system_test.go:59: runtime_test.child5: child6() // appears in stack trace |
| traceback_system_test.go:53: runtime_test.child4: child5() |
| traceback_system_test.go:49: runtime_test.child3: child4() |
| traceback_system_test.go:45: runtime_test.child2: child3() |
| traceback_system_test.go:41: runtime_test.child1: child2() |
| traceback_system_test.go:35: runtime_test.crash.func1: child1() |
| redacted.go:0: runtime.goexit |
| ` |
| if strings.TrimSpace(got) != strings.TrimSpace(want) { |
| t.Errorf("got:\n%swant:\n%s", got, want) |
| } |
| } |
| |
| // parseStackPCs parses the parent process's program counters for the |
| // first running goroutine out of a GOTRACEBACK=system traceback, |
| // adjusting them so that they are valid for the child process's text |
| // segment. |
| // |
| // This function returns only program counter values, ensuring that |
| // there is no possibility of strings from the crash report (which may |
| // contain PII) leaking into the telemetry system. |
| // |
| // (Copied from golang.org/x/telemetry/crashmonitor.parseStackPCs.) |
| func parseStackPCs(crash string) ([]uintptr, error) { |
| // getPC parses the PC out of a line of the form: |
| // \tFILE:LINE +0xRELPC sp=... fp=... pc=... |
| getPC := func(line string) (uint64, error) { |
| _, pcstr, ok := strings.Cut(line, " pc=") // e.g. pc=0x%x |
| if !ok { |
| return 0, fmt.Errorf("no pc= for stack frame: %s", line) |
| } |
| return strconv.ParseUint(pcstr, 0, 64) // 0 => allow 0x prefix |
| } |
| |
| var ( |
| pcs []uintptr |
| parentSentinel uint64 |
| childSentinel = sentinel() |
| on = false // are we in the first running goroutine? |
| lines = strings.Split(crash, "\n") |
| ) |
| for i := 0; i < len(lines); i++ { |
| line := lines[i] |
| |
| // Read sentinel value. |
| if parentSentinel == 0 && strings.HasPrefix(line, "sentinel ") { |
| _, err := fmt.Sscanf(line, "sentinel %x", &parentSentinel) |
| if err != nil { |
| return nil, fmt.Errorf("can't read sentinel line") |
| } |
| continue |
| } |
| |
| // Search for "goroutine GID [STATUS]" |
| if !on { |
| if strings.HasPrefix(line, "goroutine ") && |
| strings.Contains(line, " [running]:") { |
| on = true |
| |
| if parentSentinel == 0 { |
| return nil, fmt.Errorf("no sentinel value in crash report") |
| } |
| } |
| continue |
| } |
| |
| // A blank line marks end of a goroutine stack. |
| if line == "" { |
| break |
| } |
| |
| // Skip the final "created by SYMBOL in goroutine GID" part. |
| if strings.HasPrefix(line, "created by ") { |
| break |
| } |
| |
| // Expect a pair of lines: |
| // SYMBOL(ARGS) |
| // \tFILE:LINE +0xRELPC sp=0x%x fp=0x%x pc=0x%x |
| // Note: SYMBOL may contain parens "pkg.(*T).method" |
| // The RELPC is sometimes missing. |
| |
| // Skip the symbol(args) line. |
| i++ |
| if i == len(lines) { |
| break |
| } |
| line = lines[i] |
| |
| // Parse the PC, and correct for the parent and child's |
| // different mappings of the text section. |
| pc, err := getPC(line) |
| if err != nil { |
| // Inlined frame, perhaps; skip it. |
| continue |
| } |
| pcs = append(pcs, uintptr(pc-parentSentinel+childSentinel)) |
| } |
| return pcs, nil |
| } |
| |
| // The sentinel function returns its address. The difference between |
| // this value as observed by calls in two different processes of the |
| // same executable tells us the relative offset of their text segments. |
| // |
| // It would be nice if SetCrashOutput took care of this as it's fiddly |
| // and likely to confuse every user at first. |
| func sentinel() uint64 { |
| return uint64(reflect.ValueOf(sentinel).Pointer()) |
| } |
| |
| func writeSentinel(out io.Writer) { |
| fmt.Fprintf(out, "sentinel %x\n", sentinel()) |
| } |
| |
| // formatStack formats a stack of PC values using the symbol table, |
| // redacting information that cannot be relied upon in the test. |
| func formatStack(pcs []uintptr) string { |
| // When debugging, show file/line/content of files other than this one. |
| const debug = false |
| |
| var buf strings.Builder |
| i := 0 |
| frames := runtime.CallersFrames(pcs) |
| for { |
| fr, more := frames.Next() |
| if debug { |
| fmt.Fprintf(&buf, "pc=%x ", pcs[i]) |
| i++ |
| } |
| if base := filepath.Base(fr.File); base == "traceback_system_test.go" || debug { |
| content, err := os.ReadFile(fr.File) |
| if err != nil { |
| panic(err) |
| } |
| lines := bytes.Split(content, []byte("\n")) |
| fmt.Fprintf(&buf, "%s:%d: %s: %s\n", base, fr.Line, fr.Function, lines[fr.Line-1]) |
| } else { |
| // For robustness, don't show file/line for functions from other files. |
| fmt.Fprintf(&buf, "redacted.go:0: %s\n", fr.Function) |
| } |
| |
| if !more { |
| break |
| } |
| } |
| return buf.String() |
| } |