Lorenz Brun | f8da2e7 | 2022-06-14 12:39:32 +0200 | [diff] [blame^] | 1 | // The pstore package provides functions for interfacing with the Linux kernel's |
| 2 | // pstore (persistent storage) system. |
| 3 | // Documentation for pstore itself can be found at |
| 4 | // https://docs.kernel.org/admin-guide/abi-testing.html#abi-sys-fs-pstore. |
| 5 | package pstore |
| 6 | |
| 7 | import ( |
| 8 | "bufio" |
| 9 | "errors" |
| 10 | "fmt" |
| 11 | "io/fs" |
| 12 | "os" |
| 13 | "path/filepath" |
| 14 | "regexp" |
| 15 | "sort" |
| 16 | "strconv" |
| 17 | "time" |
| 18 | ) |
| 19 | |
| 20 | // CanonicalMountPath contains the canonical mount path of the pstore filesystem |
| 21 | const CanonicalMountPath = "/sys/fs/pstore" |
| 22 | |
| 23 | // pstoreDmesgHeader contains parsed header data from a pstore header. |
| 24 | type pstoreDmesgHeader struct { |
| 25 | Reason string |
| 26 | Counter uint64 |
| 27 | Part uint64 |
| 28 | } |
| 29 | |
| 30 | var headerRegexp = regexp.MustCompile("^([^#]+)#([0-9]+) Part([0-9]+)$") |
| 31 | |
| 32 | // parseDmesgHeader parses textual pstore entry headers as assembled by |
| 33 | // @linux//fs/pstore/platform.c:pstore_dump back into a structured format. |
| 34 | // The input must be the first line of a file with the terminating \n stripped. |
| 35 | func parseDmesgHeader(hdr string) (*pstoreDmesgHeader, error) { |
| 36 | parts := headerRegexp.FindStringSubmatch(hdr) |
| 37 | if parts == nil { |
| 38 | return nil, errors.New("unable to parse pstore entry header") |
| 39 | } |
| 40 | counter, err := strconv.ParseUint(parts[2], 10, 64) |
| 41 | if err != nil { |
| 42 | return nil, fmt.Errorf("failed to parse pstore header count: %w", err) |
| 43 | } |
| 44 | part, err := strconv.ParseUint(parts[3], 10, 64) |
| 45 | if err != nil { |
| 46 | return nil, fmt.Errorf("failed to parse pstore header part: %w", err) |
| 47 | } |
| 48 | return &pstoreDmesgHeader{ |
| 49 | Reason: parts[1], |
| 50 | Counter: counter, |
| 51 | Part: part, |
| 52 | }, nil |
| 53 | } |
| 54 | |
| 55 | // A reassembled kernel message buffer dump from pstore. |
| 56 | type KmsgDump struct { |
| 57 | // The reason why the dump was created. Common values include "Panic" and |
| 58 | // "Oops", but depending on the setting `printk.always_kmsg_dump` and |
| 59 | // potential future reasons this is likely unbounded. |
| 60 | Reason string |
| 61 | // The CLOCK_REALTIME value of the first entry in the dump (which is the |
| 62 | // closest to the actual time the dump happened). This can be zero or |
| 63 | // garbage if the RTC hasn't been initialized or the system has no working |
| 64 | // clock source. |
| 65 | OccurredAt time.Time |
| 66 | // A counter counting up for every dump created. Can be used to order dumps |
| 67 | // when the OccurredAt value is not usable due to system issues. |
| 68 | Counter uint64 |
| 69 | // A list of kernel log lines in oldest-to-newest order, i.e. the oldest |
| 70 | // message comes first. The actual cause is generally reported last. |
| 71 | Lines []string |
| 72 | } |
| 73 | |
| 74 | var dmesgFileRegexp = regexp.MustCompile("^dmesg-.*-([0-9]+)") |
| 75 | |
| 76 | type pstoreDmesgFile struct { |
| 77 | hdr pstoreDmesgHeader |
| 78 | ctime time.Time |
| 79 | lines []string |
| 80 | } |
| 81 | |
| 82 | // GetKmsgDumps returns a list of events where the kernel has dumped its kmsg |
| 83 | // (kernel log) buffer into pstore because of a kernel oops or panic. |
| 84 | func GetKmsgDumps() ([]KmsgDump, error) { |
| 85 | return getKmsgDumpsFromFS(os.DirFS(CanonicalMountPath)) |
| 86 | } |
| 87 | |
| 88 | // f is injected here for testing |
| 89 | func getKmsgDumpsFromFS(f fs.FS) ([]KmsgDump, error) { |
| 90 | var events []KmsgDump |
| 91 | eventMap := make(map[string][]pstoreDmesgFile) |
| 92 | pstoreEntries, err := fs.ReadDir(f, ".") |
| 93 | if err != nil { |
| 94 | return events, fmt.Errorf("failed to list files in pstore: %w", err) |
| 95 | } |
| 96 | for _, entry := range pstoreEntries { |
| 97 | if !dmesgFileRegexp.MatchString(entry.Name()) { |
| 98 | continue |
| 99 | } |
| 100 | f, err := f.Open(entry.Name()) |
| 101 | if err != nil { |
| 102 | return events, fmt.Errorf("failed to open pstore entry file: %w", err) |
| 103 | } |
| 104 | // This only closes after all files have been read, but the number of |
| 105 | // files is heavily bound by very small amounts of pstore space. |
| 106 | defer f.Close() |
| 107 | finfo, err := f.Stat() |
| 108 | if err != nil { |
| 109 | return events, fmt.Errorf("failed to stat pstore entry file: %w", err) |
| 110 | } |
| 111 | s := bufio.NewScanner(f) |
| 112 | if !s.Scan() { |
| 113 | return events, fmt.Errorf("cannot read first line header of pstore entry %q: %w", entry.Name(), s.Err()) |
| 114 | } |
| 115 | hdr, err := parseDmesgHeader(s.Text()) |
| 116 | if err != nil { |
| 117 | return events, fmt.Errorf("failed to parse header of file %q: %w", entry.Name(), err) |
| 118 | } |
| 119 | var lines []string |
| 120 | for s.Scan() { |
| 121 | lines = append(lines, s.Text()) |
| 122 | } |
| 123 | // Same textual encoding is used in the header itself, so this |
| 124 | // is as unique as it gets. |
| 125 | key := fmt.Sprintf("%v#%d", hdr.Reason, hdr.Counter) |
| 126 | eventMap[key] = append(eventMap[key], pstoreDmesgFile{hdr: *hdr, ctime: finfo.ModTime(), lines: lines}) |
| 127 | } |
| 128 | |
| 129 | for _, event := range eventMap { |
| 130 | sort.Slice(event, func(i, j int) bool { |
| 131 | return event[i].hdr.Part > event[j].hdr.Part |
| 132 | }) |
| 133 | ev := KmsgDump{ |
| 134 | Counter: event[len(event)-1].hdr.Counter, |
| 135 | Reason: event[len(event)-1].hdr.Reason, |
| 136 | // Entries get created in reverse order, so the most accurate |
| 137 | // timestamp is the first one. |
| 138 | OccurredAt: event[len(event)-1].ctime, |
| 139 | } |
| 140 | for _, entry := range event { |
| 141 | ev.Lines = append(ev.Lines, entry.lines...) |
| 142 | } |
| 143 | events = append(events, ev) |
| 144 | } |
| 145 | sort.Slice(events, func(i, j int) bool { |
| 146 | return !events[i].OccurredAt.Before(events[j].OccurredAt) |
| 147 | }) |
| 148 | return events, nil |
| 149 | } |
| 150 | |
| 151 | // ClearAll clears out all existing entries from the pstore. This should be done |
| 152 | // after every start (after the relevant data has been read out) to ensure that |
| 153 | // there is always space to store new pstore entries and to minimize the risk |
| 154 | // of breaking badly-programmed firmware. |
| 155 | func ClearAll() error { |
| 156 | pstoreEntries, err := os.ReadDir(CanonicalMountPath) |
| 157 | if err != nil { |
| 158 | return fmt.Errorf("failed to list files in pstore: %w", err) |
| 159 | } |
| 160 | for _, entry := range pstoreEntries { |
| 161 | if err := os.Remove(filepath.Join(CanonicalMountPath, entry.Name())); err != nil { |
| 162 | return fmt.Errorf("failed to clear pstore entry: %w", err) |
| 163 | } |
| 164 | } |
| 165 | return nil |
| 166 | } |