...

Source file src/runtime/traceback_system_test.go

Documentation: runtime

     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 runtime_test
     6  
     7  // This test of GOTRACEBACK=system has its own file,
     8  // to minimize line-number perturbation.
     9  
    10  import (
    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.
    27  func 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  
    40  func child1() {
    41  	child2()
    42  }
    43  
    44  func child2() {
    45  	child3()
    46  }
    47  
    48  func child3() {
    49  	child4()
    50  }
    51  
    52  func child4() {
    53  	child5()
    54  }
    55  
    56  //go:noinline
    57  func child5() { // test trace through second of two call instructions
    58  	child6bad()
    59  	child6() // appears in stack trace
    60  }
    61  
    62  //go:noinline
    63  func child6bad() {
    64  }
    65  
    66  //go:noinline
    67  func child6() { // test trace through first of two call instructions
    68  	child7() // appears in stack trace
    69  	child7bad()
    70  }
    71  
    72  //go:noinline
    73  func child7bad() {
    74  }
    75  
    76  //go:noinline
    77  func 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.
    97  func 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
   131  traceback_system_test.go:85: runtime_test.child7: 	panic("oops")
   132  traceback_system_test.go:68: runtime_test.child6: 	child7() // appears in stack trace
   133  traceback_system_test.go:59: runtime_test.child5: 	child6() // appears in stack trace
   134  traceback_system_test.go:53: runtime_test.child4: 	child5()
   135  traceback_system_test.go:49: runtime_test.child3: 	child4()
   136  traceback_system_test.go:45: runtime_test.child2: 	child3()
   137  traceback_system_test.go:41: runtime_test.child1: 	child2()
   138  traceback_system_test.go:35: runtime_test.crash.func1: 		child1()
   139  redacted.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.)
   156  func 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.
   240  func sentinel() uint64 {
   241  	return uint64(reflect.ValueOf(sentinel).Pointer())
   242  }
   243  
   244  func 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.
   250  func 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  

View as plain text