Source file src/cmd/vendor/golang.org/x/telemetry/internal/crashmonitor/monitor.go
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 5 package crashmonitor 6 7 // This file defines a monitor that reports arbitrary Go runtime 8 // crashes to telemetry. 9 10 import ( 11 "bytes" 12 "fmt" 13 "io" 14 "log" 15 "os" 16 "reflect" 17 "runtime/debug" 18 "strconv" 19 "strings" 20 21 "golang.org/x/telemetry/internal/counter" 22 ) 23 24 // Parent sets up the parent side of the crashmonitor. It requires 25 // exclusive use of a writable pipe connected to the child process's stdin. 26 func Parent(pipe *os.File) { 27 writeSentinel(pipe) 28 // Ensure that we get pc=0x%x values in the traceback. 29 debug.SetTraceback("system") 30 debug.SetCrashOutput(pipe, debug.CrashOptions{}) // ignore error 31 } 32 33 // Child runs the part of the crashmonitor that runs in the child process. 34 // It expects its stdin to be connected via a pipe to the parent which has 35 // run Parent. 36 func Child() { 37 // Wait for parent process's dying gasp. 38 // If the parent dies for any reason this read will return. 39 data, err := io.ReadAll(os.Stdin) 40 if err != nil { 41 log.Fatalf("failed to read from input pipe: %v", err) 42 } 43 44 // If the only line is the sentinel, it wasn't a crash. 45 if bytes.Count(data, []byte("\n")) < 2 { 46 childExitHook() 47 os.Exit(0) // parent exited without crash report 48 } 49 50 log.Printf("parent reported crash:\n%s", data) 51 52 // Parse the stack out of the crash report 53 // and record a telemetry count for it. 54 name, err := telemetryCounterName(data) 55 if err != nil { 56 // Keep count of how often this happens 57 // so that we can investigate if necessary. 58 incrementCounter("crash/malformed") 59 60 // Something went wrong. 61 // Save the crash securely in the file system. 62 f, err := os.CreateTemp(os.TempDir(), "*.crash") 63 if err != nil { 64 log.Fatal(err) 65 } 66 if _, err := f.Write(data); err != nil { 67 log.Fatal(err) 68 } 69 if err := f.Close(); err != nil { 70 log.Fatal(err) 71 } 72 log.Printf("failed to report crash to telemetry: %v", err) 73 log.Fatalf("crash report saved at %s", f.Name()) 74 } 75 76 incrementCounter(name) 77 78 childExitHook() 79 log.Fatalf("telemetry crash recorded") 80 } 81 82 // (stubbed by test) 83 var ( 84 incrementCounter = func(name string) { counter.New(name).Inc() } 85 childExitHook = func() {} 86 ) 87 88 // The sentinel function returns its address. The difference between 89 // this value as observed by calls in two different processes of the 90 // same executable tells us the relative offset of their text segments. 91 // 92 // It would be nice if SetCrashOutput took care of this as it's fiddly 93 // and likely to confuse every user at first. 94 func sentinel() uint64 { 95 return uint64(reflect.ValueOf(sentinel).Pointer()) 96 } 97 98 func writeSentinel(out io.Writer) { 99 fmt.Fprintf(out, "sentinel %x\n", sentinel()) 100 } 101 102 // telemetryCounterName parses a crash report produced by the Go 103 // runtime, extracts the stack of the first runnable goroutine, 104 // converts each line into telemetry form ("symbol:relative-line"), 105 // and returns this as the name of a counter. 106 func telemetryCounterName(crash []byte) (string, error) { 107 pcs, err := parseStackPCs(string(crash)) 108 if err != nil { 109 return "", err 110 } 111 112 // Limit the number of frames we request. 113 pcs = pcs[:min(len(pcs), 16)] 114 115 if len(pcs) == 0 { 116 // This can occur if all goroutines are idle, as when 117 // caught in a deadlock, or killed by an async signal 118 // while blocked. 119 // 120 // TODO(adonovan): consider how to report such 121 // situations. Reporting a goroutine in [sleep] or 122 // [select] state could be quite confusing without 123 // further information about the nature of the crash, 124 // as the problem is not local to the code location. 125 // 126 // For now, we keep count of this situation so that we 127 // can access whether it needs a more involved solution. 128 return "crash/no-running-goroutine", nil 129 } 130 131 // This string appears at the start of all 132 // crashmonitor-generated counter names. 133 // 134 // It is tempting to expose this as a parameter of Start, but 135 // it is not without risk. What value should most programs 136 // provide? There's no point giving the name of the executable 137 // as this is already recorded by telemetry. What if the 138 // application runs in multiple modes? Then it might be useful 139 // to record the mode. The problem is that an application with 140 // multiple modes probably doesn't know its mode by line 1 of 141 // main.main: it might require flag or argument parsing, or 142 // even validation of an environment variable, and we really 143 // want to steer users aware from any logic before Start. The 144 // flags and arguments will be wrong in the child process, and 145 // every extra conditional branch creates a risk that the 146 // recursively executed child program will behave not like the 147 // monitor but like the application. If the child process 148 // exits before calling Start, then the parent application 149 // will not have a monitor, and its crash reports will be 150 // discarded (written in to a pipe that is never read). 151 // 152 // So for now, we use this constant string. 153 const prefix = "crash/crash" 154 return counter.EncodeStack(pcs, prefix), nil 155 } 156 157 // parseStackPCs parses the parent process's program counters for the 158 // first running goroutine out of a GOTRACEBACK=system traceback, 159 // adjusting them so that they are valid for the child process's text 160 // segment. 161 // 162 // This function returns only program counter values, ensuring that 163 // there is no possibility of strings from the crash report (which may 164 // contain PII) leaking into the telemetry system. 165 func parseStackPCs(crash string) ([]uintptr, error) { 166 // getSymbol parses the symbol name out of a line of the form: 167 // SYMBOL(ARGS) 168 // 169 // Note: SYMBOL may contain parens "pkg.(*T).method". However, type 170 // parameters are always replaced with ..., so they cannot introduce 171 // more parens. e.g., "pkg.(*T[...]).method". 172 // 173 // ARGS can contain parens. We want the first paren that is not 174 // immediately preceded by a ".". 175 // 176 // TODO(prattmic): This is mildly complicated and is only used to find 177 // runtime.sigpanic, so perhaps simplify this by checking explicitly 178 // for sigpanic. 179 getSymbol := func(line string) (string, error) { 180 var prev rune 181 for i, c := range line { 182 if line[i] != '(' { 183 prev = c 184 continue 185 } 186 if prev == '.' { 187 prev = c 188 continue 189 } 190 return line[:i], nil 191 } 192 return "", fmt.Errorf("no symbol for stack frame: %s", line) 193 } 194 195 // getPC parses the PC out of a line of the form: 196 // \tFILE:LINE +0xRELPC sp=... fp=... pc=... 197 getPC := func(line string) (uint64, error) { 198 _, pcstr, ok := strings.Cut(line, " pc=") // e.g. pc=0x%x 199 if !ok { 200 return 0, fmt.Errorf("no pc= for stack frame: %s", line) 201 } 202 return strconv.ParseUint(pcstr, 0, 64) // 0 => allow 0x prefix 203 } 204 205 type goroutine struct { 206 id uint64 207 pcs []uintptr 208 system bool // stack starts with runtime.systemstack_switch 209 } 210 211 var ( 212 glist []*goroutine 213 parentSentinel uint64 214 childSentinel = sentinel() 215 lines = strings.Split(crash, "\n") 216 217 // Parsing state for the current goroutine. 218 g *goroutine 219 symLine = true // a symbol (not a filename) is expected on this line 220 currSymbol string 221 prevSymbol string 222 ) 223 224 for _, line := range lines { 225 // Read sentinel value. 226 if parentSentinel == 0 && strings.HasPrefix(line, "sentinel ") { 227 _, err := fmt.Sscanf(line, "sentinel %x", &parentSentinel) 228 if err != nil { 229 return nil, fmt.Errorf("can't read sentinel line") 230 } 231 continue 232 } 233 234 // Check for a "goroutine GID [STATUS]" header. 235 if strings.HasPrefix(line, "goroutine ") { 236 if parentSentinel == 0 { 237 return nil, fmt.Errorf("no sentinel value in crash report") 238 } 239 isG0 := strings.HasPrefix(line, "goroutine 0 ") 240 isRunning := strings.Contains(line, " [running]:") 241 if isG0 || isRunning { 242 var id uint64 243 if parts := strings.Fields(line); len(parts) > 1 { 244 id, _ = strconv.ParseUint(parts[1], 10, 64) 245 } 246 g = &goroutine{id: id} 247 glist = append(glist, g) 248 symLine = true 249 currSymbol = "" 250 prevSymbol = "" 251 } else { 252 g = nil 253 } 254 continue 255 } 256 257 if g == nil { 258 continue 259 } 260 261 // A blank line or "created by " marks the end of the goroutine stack. 262 if line == "" || strings.HasPrefix(line, "created by ") { 263 g = nil 264 continue 265 } 266 267 // Expect a pair of lines: 268 // SYMBOL(ARGS) 269 // \tFILE:LINE +0xRELPC sp=0x%x fp=0x%x pc=0x%x 270 // Note: SYMBOL may contain parens "pkg.(*T).method" 271 // The RELPC is sometimes missing. 272 273 if symLine { 274 var err error 275 currSymbol, err = getSymbol(line) 276 if err != nil { 277 return nil, fmt.Errorf("error extracting symbol: %v", err) 278 } 279 280 symLine = false // Next line is FILE:LINE. 281 } else { 282 // Parse the PC, and correct for the parent and child's 283 // different mappings of the text section. 284 pc, err := getPC(line) 285 if err != nil { 286 // Inlined frame, perhaps; skip it. 287 288 // Done with this frame. Next line is a new frame. 289 // 290 // Don't update prevSymbol; we only want to 291 // track frames with a PC. 292 currSymbol = "" 293 symLine = true 294 continue 295 } 296 297 pc = pc - parentSentinel + childSentinel 298 299 // If the previous frame was sigpanic, then this frame 300 // was a trap (e.g., SIGSEGV). 301 // 302 // Typically all middle frames are calls, and report 303 // the "return PC". That is, the instruction following 304 // the CALL where the callee will eventually return to. 305 // 306 // runtime.CallersFrames is aware of this property and 307 // will decrement each PC by 1 to "back up" to the 308 // location of the CALL, which is the actual line 309 // number the user expects. 310 // 311 // This does not work for traps, as a trap is not a 312 // call, so the reported PC is not the return PC, but 313 // the actual PC of the trap. 314 // 315 // runtime.Callers is aware of this and will 316 // intentionally increment trap PCs in order to correct 317 // for the decrement performed by 318 // runtime.CallersFrames. See runtime.tracebackPCs and 319 // runtume.(*unwinder).symPC. 320 // 321 // We must emulate the same behavior, otherwise we will 322 // report the location of the instruction immediately 323 // prior to the trap, which may be on a different line, 324 // or even a different inlined functions. 325 // 326 // TODO(prattmic): The runtime applies the same trap 327 // behavior for other "injected calls", see injectCall 328 // in runtime.(*unwinder).next. Do we want to handle 329 // those as well? I don't believe we'd ever see 330 // runtime.asyncPreempt or runtime.debugCallV2 in a 331 // typical crash. 332 if prevSymbol == "runtime.sigpanic" { 333 pc++ 334 } 335 336 if len(g.pcs) == 0 && currSymbol == "runtime.systemstack_switch" { 337 g.system = true 338 } 339 g.pcs = append(g.pcs, uintptr(pc)) 340 341 // Done with this frame. Next line is a new frame. 342 prevSymbol = currSymbol 343 currSymbol = "" 344 symLine = true 345 } 346 } 347 348 if len(glist) == 0 { 349 return nil, nil 350 } 351 352 // The first goroutine in the dump is the one that crashed. 353 firstG := glist[0] 354 355 // If the first goroutine is g0 (the system stack), we want to find the user 356 // goroutine that called it, which will start with systemstack_switch. 357 if firstG.id == 0 { 358 for _, g := range glist[1:] { 359 if g.system && len(g.pcs) > 0 { 360 // Stitch the g0 stack and user stack (skipping systemstack_switch). 361 return append(firstG.pcs, g.pcs[1:]...), nil 362 } 363 } 364 } 365 366 return firstG.pcs, nil 367 } 368