| // 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, debug.CrashOptions{}) | |
| 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() | |
| } |