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