// 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/panic. It prints a crash report to stdout. func crashViaPanic() { // 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 {} } // This is the entrypoint of the child process used by // TestTracebackSystem/trap. It prints a crash report to stdout. func crashViaTrap() { // 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. trap1() }() 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") } func trap1() { trap2() } var sinkPtr *int func trap2() { trap3(sinkPtr) } func trap3(i *int) { *i = 42 } // 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") } tests := []struct{ name string want string }{ { name: "panic", want: `redacted.go:0: runtime.gopanic traceback_system_test.go:100: runtime_test.child7: panic("oops") traceback_system_test.go:83: runtime_test.child6: child7() // appears in stack trace traceback_system_test.go:74: runtime_test.child5: child6() // appears in stack trace traceback_system_test.go:68: runtime_test.child4: child5() traceback_system_test.go:64: runtime_test.child3: child4() traceback_system_test.go:60: runtime_test.child2: child3() traceback_system_test.go:56: runtime_test.child1: child2() traceback_system_test.go:35: runtime_test.crashViaPanic.func1: child1() redacted.go:0: runtime.goexit `, }, { // Test panic via trap. x/telemetry is aware that trap // PCs follow runtime.sigpanic and need to be // incremented to offset the decrement done by // CallersFrames. name: "trap", want: `redacted.go:0: runtime.gopanic redacted.go:0: runtime.panicmem redacted.go:0: runtime.sigpanic traceback_system_test.go:114: runtime_test.trap3: *i = 42 traceback_system_test.go:110: runtime_test.trap2: trap3(sinkPtr) traceback_system_test.go:104: runtime_test.trap1: trap2() traceback_system_test.go:50: runtime_test.crashViaTrap.func1: trap1() redacted.go:0: runtime.goexit `, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // 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+"="+tc.name) 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) if strings.TrimSpace(got) != strings.TrimSpace(tc.want) { t.Errorf("got:\n%swant:\n%s", got, tc.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) { // getSymbol parses the symbol name out of a line of the form: // SYMBOL(ARGS) // // Note: SYMBOL may contain parens "pkg.(*T).method". However, type // parameters are always replaced with ..., so they cannot introduce // more parens. e.g., "pkg.(*T[...]).method". // // ARGS can contain parens. We want the first paren that is not // immediately preceded by a ".". // // TODO(prattmic): This is mildly complicated and is only used to find // runtime.sigpanic, so perhaps simplify this by checking explicitly // for sigpanic. getSymbol := func(line string) (string, error) { var prev rune for i, c := range line { if line[i] != '(' { prev = c continue } if prev == '.' { prev = c continue } return line[:i], nil } return "", fmt.Errorf("no symbol for stack frame: %s", line) } // 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") symLine = true // within a goroutine, every other line is a symbol or file/line/pc location, starting with symbol. currSymbol string prevSymbol string // symbol of the most recent previous frame with a PC. ) 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. if symLine { var err error currSymbol, err = getSymbol(line) if err != nil { return nil, fmt.Errorf("error extracting symbol: %v", err) } symLine = false // Next line is FILE:LINE. } else { // 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. // Done with this frame. Next line is a new frame. // // Don't update prevSymbol; we only want to // track frames with a PC. currSymbol = "" symLine = true continue } pc = pc-parentSentinel+childSentinel // If the previous frame was sigpanic, then this frame // was a trap (e.g., SIGSEGV). // // Typically all middle frames are calls, and report // the "return PC". That is, the instruction following // the CALL where the callee will eventually return to. // // runtime.CallersFrames is aware of this property and // will decrement each PC by 1 to "back up" to the // location of the CALL, which is the actual line // number the user expects. // // This does not work for traps, as a trap is not a // call, so the reported PC is not the return PC, but // the actual PC of the trap. // // runtime.Callers is aware of this and will // intentionally increment trap PCs in order to correct // for the decrement performed by // runtime.CallersFrames. See runtime.tracebackPCs and // runtume.(*unwinder).symPC. // // We must emulate the same behavior, otherwise we will // report the location of the instruction immediately // prior to the trap, which may be on a different line, // or even a different inlined functions. // // TODO(prattmic): The runtime applies the same trap // behavior for other "injected calls", see injectCall // in runtime.(*unwinder).next. Do we want to handle // those as well? I don't believe we'd ever see // runtime.asyncPreempt or runtime.debugCallV2 in a // typical crash. if prevSymbol == "runtime.sigpanic" { pc++ } pcs = append(pcs, uintptr(pc)) // Done with this frame. Next line is a new frame. prevSymbol = currSymbol currSymbol = "" symLine = true } } 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() }