| // Copyright 2018 The Chromium OS 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 breakpad processes minidump crash reports created by Breakpad. |
| // |
| // See https://chromium.googlesource.com/breakpad/breakpad/ for more details about Breakpad. |
| package breakpad |
| |
| import ( |
| "bufio" |
| "bytes" |
| "encoding/binary" |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "strings" |
| ) |
| |
| const ( |
| debugSuffix = ".debug" // suffix that Chrome OS build process adds to files with debugging symbols |
| mdMagic = "MDMP" // magic bytes occurring at the beginning of minidump files |
| mdMaxStreams = 32 // max streams to read from minidump file |
| mdReleaseStreamType = 0x47670005 // minidump stream type used for /etc/lsb-release data |
| mdReleaseStreamMaxSize = 4096 // max bytes to read from mdReleaseStreamType streams |
| ) |
| |
| // missingRegexp extracts module paths and IDs from messages logged by minidump_stackwalk. |
| var missingRegexp *regexp.Regexp |
| |
| func init() { |
| // minidump_stackwalk writes a message like the following to stderr for each missing symbol file: |
| // 2017-12-07 11:05:36: stackwalker.cc:103: INFO: Couldn't load symbols for: /lib64/libc-2.23.so|7219A63C9901FA247C197BCFED143B110 |
| // 2017-12-07 11:05:36: stackwalker.cc:103: INFO: Couldn't load symbols for: /usr/lib64/libevent_core-2.1.so.6.0.2|3F4224A41349B3B9600315956E3D6CA70 |
| // The hexadecimal string at the end corresponds to ModuleInfo.ID. |
| missingRegexp = regexp.MustCompile("Couldn't load symbols for:\\s+([^|]+)\\|(\\S+)") |
| } |
| |
| // ModuleInfo contains data from a Breakpad symbol file's MODULE record. |
| // See https://chromium.googlesource.com/breakpad/breakpad/+/master/docs/symbol_files.md#records-1 for details. |
| type ModuleInfo struct { |
| // OS identifies the operating system on which the executable or shared library was intended to run. |
| OS string |
| // Arch indicates the processor architecture the executable or shared library contains machine code for. |
| Arch string |
| // ID is an opaque sequence of hexadecimal digits that identifies the exact executable or library whose |
| // contents the symbol file describes. |
| ID string |
| // Name contains the base name (the final component of the directory path) of the executable or library. |
| Name string |
| } |
| |
| // GetDebugBinaryPath returns the absolute path under buildRoot (e.g. "/build/lumpy") |
| // where the debug binary should be located for path (e.g. "/bin/grep"). |
| func GetDebugBinaryPath(buildRoot, path string) string { |
| return filepath.Join(buildRoot, "/usr/lib/debug", path+debugSuffix) |
| } |
| |
| // parseSymbolFileModuleRecord parses the MODULE record from a Breakpad symbol file. |
| func parseSymbolFileModuleRecord(line string) (*ModuleInfo, error) { |
| parts := strings.Fields(line) |
| if len(parts) != 5 { |
| return nil, fmt.Errorf("got %v part(s) instead of 5", len(parts)) |
| } |
| if parts[0] != "MODULE" { |
| return nil, errors.New("didn't start with MODULE") |
| } |
| return &ModuleInfo{parts[1], parts[2], parts[3], parts[4]}, nil |
| } |
| |
| // GetSymbolFilePath returns the path within symbol directory dir where the Breakpad |
| // symbol file for the binary with basename base and ModuleInfo.ID id should be located. |
| func GetSymbolFilePath(dir, base, id string) string { |
| return filepath.Join(dir, base, id, fmt.Sprintf("%s.sym", base)) |
| } |
| |
| // WriteSymbolFile writes a Breakpad symbols file for binPath, a binary file with debugging symbols, |
| // to the Breakpad-expected location within outDir. |
| // Breakpad's dump_syms program must be present. |
| func WriteSymbolFile(binPath, outDir string) (*ModuleInfo, error) { |
| cmd := exec.Command("dump_syms", binPath) |
| cl := strings.Join(cmd.Args, " ") |
| |
| stdout, err := cmd.StdoutPipe() |
| if err != nil { |
| return nil, fmt.Errorf("failed to get stdout pipe for %v: %v", cl, err) |
| } |
| defer stdout.Close() |
| |
| if err = cmd.Start(); err != nil { |
| return nil, fmt.Errorf("failed to start %v: %v", cl, err) |
| } |
| |
| // Read the first line of output to determine what we're generating. |
| br := bufio.NewReader(stdout) |
| line, err := br.ReadString('\n') |
| if err != nil { |
| return nil, fmt.Errorf("failed to read MODULE record from %v: %v", cl, err) |
| } |
| mi, err := parseSymbolFileModuleRecord(line) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse MODULE record %q: %v", line, err) |
| } |
| |
| // Now copy the line and the rest of the output to the file. |
| // Omit the ".debug" suffix since it probably won't be present in the minidump. |
| name := mi.Name |
| if strings.HasSuffix(name, debugSuffix) { |
| name = name[0 : len(name)-len(debugSuffix)] |
| } |
| p := GetSymbolFilePath(outDir, name, mi.ID) |
| if err = os.MkdirAll(filepath.Dir(p), 0755); err != nil { |
| return mi, err |
| } |
| f, err := os.Create(p) |
| if err != nil { |
| return mi, err |
| } |
| defer f.Close() |
| |
| if _, err = io.WriteString(f, line); err != nil { |
| return mi, fmt.Errorf("failed to write MODULE record to %v: %v", p, err) |
| } |
| if _, err = io.Copy(f, br); err != nil { |
| return mi, fmt.Errorf("failed to copy symbols to %v: %v", p, err) |
| } |
| return mi, cmd.Wait() |
| } |
| |
| // SymbolFileMap maps from the full paths of binaries (as absolute on-device paths, e.g. |
| // "/usr/lib64/libbase-core-395517.so") to the corresponding symbol file IDs wanted by |
| // minidump_stackwalk (i.e. breakpad.ModuleInfo.ID). |
| type SymbolFileMap map[string]string |
| |
| // WalkMinidump writes a human-readable representation of the stack trace contained within |
| // the minidump file at path to w. symDir is a directory containing symbol files that will |
| // be used to make a best-effort attempt symbolize the trace. Binaries with missing symbol |
| // files are returned. |
| // |
| // Minidump's minidump_stackwalk program must be present, and symDir should follow the |
| // <module>/<ID>/<module>.sym layout directory expected by it, e.g. |
| // libc-2.23.so/7219A63C9901FA247C197BCFED143B110/libc-2.23.so.sym. |
| func WalkMinidump(path, symDir string, w io.Writer) (missing SymbolFileMap, err error) { |
| cmd := exec.Command("minidump_stackwalk", path, symDir) |
| cl := strings.Join(cmd.Args, " ") |
| |
| stderr := bytes.Buffer{} |
| cmd.Stderr = &stderr |
| cmd.Stdout = w |
| |
| if err = cmd.Run(); err != nil { |
| return nil, fmt.Errorf("failed to run %v: %v", cl, err) |
| } |
| |
| missing = make(SymbolFileMap) |
| sc := bufio.NewScanner(strings.NewReader(stderr.String())) |
| for sc.Scan() { |
| m := missingRegexp.FindStringSubmatch(sc.Text()) |
| if m != nil { |
| missing[m[1]] = m[2] |
| } |
| } |
| return missing, nil |
| } |
| |
| // IsMinidump returns true if r (which should be at the beginning of a file) is a minidump file. |
| func IsMinidump(r io.Reader) (bool, error) { |
| b := make([]byte, len(mdMagic)) |
| if _, err := io.ReadFull(r, b); err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { |
| return false, err |
| } |
| return bytes.Equal(b, []byte(mdMagic)), nil |
| } |
| |
| // mdStreamInfo contains information about a stream located in a minidump file. |
| type mdStreamInfo struct { |
| streamType uint32 // identifies type of data in stream |
| offset uint32 // from beginning of file |
| size uint32 |
| } |
| |
| // readMinidumpStreamInfo returns stream information from f, a minidump file. |
| // |
| // Here are the relevant parts near the start of a minidump file: |
| // ... |
| // 0x08 stream count (4 bytes) |
| // ... |
| // 0x20 stream 0 type (4 bytes) |
| // 0x24 stream 0 size (4 bytes) |
| // 0x28 stream 0 offset (4 bytes) |
| // 0x2c stream 1 type (4 bytes) |
| // etc. |
| // |
| // See https://chromium.googlesource.com/breakpad/breakpad/+/master/src/google_breakpad/common/minidump_format.h |
| // for more details. |
| func readMinidumpStreamInfo(f *os.File) ([]mdStreamInfo, error) { |
| // First read the stream count. |
| if _, err := f.Seek(0x8, 0); err != nil { |
| return nil, err |
| } |
| var numStreams uint32 |
| if err := binary.Read(f, binary.LittleEndian, &numStreams); err != nil { |
| return nil, err |
| } |
| if numStreams > mdMaxStreams { |
| return nil, fmt.Errorf("too many streams (%v)", numStreams) |
| } |
| |
| // Now iterate over all of the stream directory listings to get their |
| // types and bounds. |
| if _, err := f.Seek(0x20, 0); err != nil { |
| return nil, err |
| } |
| infos := make([]mdStreamInfo, numStreams) |
| for i := uint32(0); i < numStreams; i++ { |
| b := make([]uint32, 3) |
| if err := binary.Read(f, binary.LittleEndian, &b); err != nil { |
| return nil, err |
| } |
| infos[i].streamType = b[0] |
| infos[i].size = b[1] |
| infos[i].offset = b[2] |
| } |
| return infos, nil |
| } |
| |
| // GetMinidumpReleaseInfo returns the contents of /etc/lsb-release if it |
| // is present in f, a minidump file. The Linux version of Breakpad includes |
| // this information in crashes. |
| func GetMinidumpReleaseInfo(f *os.File) (string, error) { |
| infos, err := readMinidumpStreamInfo(f) |
| if err != nil { |
| return "", err |
| } |
| var releaseInfo *mdStreamInfo |
| for _, info := range infos { |
| if info.streamType == mdReleaseStreamType { |
| releaseInfo = &info |
| break |
| } |
| } |
| if releaseInfo == nil { |
| return "", errors.New("no lsb-release stream") |
| } |
| |
| if _, err = f.Seek(int64(releaseInfo.offset), 0); err != nil { |
| return "", err |
| } |
| b := make([]byte, releaseInfo.size) |
| if _, err = io.ReadFull(f, b); err != nil { |
| return "", err |
| } |
| return string(b), nil |
| } |