src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

monitor.go (11335B)


      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 }