| // The pstore package provides functions for interfacing with the Linux kernel's |
| // pstore (persistent storage) system. |
| // Documentation for pstore itself can be found at |
| // https://docs.kernel.org/admin-guide/abi-testing.html#abi-sys-fs-pstore. |
| package pstore |
| |
| import ( |
| "bufio" |
| "errors" |
| "fmt" |
| "io/fs" |
| "os" |
| "path/filepath" |
| "regexp" |
| "sort" |
| "strconv" |
| "time" |
| ) |
| |
| // CanonicalMountPath contains the canonical mount path of the pstore filesystem |
| const CanonicalMountPath = "/sys/fs/pstore" |
| |
| // pstoreDmesgHeader contains parsed header data from a pstore header. |
| type pstoreDmesgHeader struct { |
| Reason string |
| Counter uint64 |
| Part uint64 |
| } |
| |
| var headerRegexp = regexp.MustCompile("^([^#]+)#([0-9]+) Part([0-9]+)$") |
| |
| // parseDmesgHeader parses textual pstore entry headers as assembled by |
| // @linux//fs/pstore/platform.c:pstore_dump back into a structured format. |
| // The input must be the first line of a file with the terminating \n stripped. |
| func parseDmesgHeader(hdr string) (*pstoreDmesgHeader, error) { |
| parts := headerRegexp.FindStringSubmatch(hdr) |
| if parts == nil { |
| return nil, errors.New("unable to parse pstore entry header") |
| } |
| counter, err := strconv.ParseUint(parts[2], 10, 64) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse pstore header count: %w", err) |
| } |
| part, err := strconv.ParseUint(parts[3], 10, 64) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse pstore header part: %w", err) |
| } |
| return &pstoreDmesgHeader{ |
| Reason: parts[1], |
| Counter: counter, |
| Part: part, |
| }, nil |
| } |
| |
| // A reassembled kernel message buffer dump from pstore. |
| type KmsgDump struct { |
| // The reason why the dump was created. Common values include "Panic" and |
| // "Oops", but depending on the setting `printk.always_kmsg_dump` and |
| // potential future reasons this is likely unbounded. |
| Reason string |
| // The CLOCK_REALTIME value of the first entry in the dump (which is the |
| // closest to the actual time the dump happened). This can be zero or |
| // garbage if the RTC hasn't been initialized or the system has no working |
| // clock source. |
| OccurredAt time.Time |
| // A counter counting up for every dump created. Can be used to order dumps |
| // when the OccurredAt value is not usable due to system issues. |
| Counter uint64 |
| // A list of kernel log lines in oldest-to-newest order, i.e. the oldest |
| // message comes first. The actual cause is generally reported last. |
| Lines []string |
| } |
| |
| var dmesgFileRegexp = regexp.MustCompile("^dmesg-.*-([0-9]+)") |
| |
| type pstoreDmesgFile struct { |
| hdr pstoreDmesgHeader |
| ctime time.Time |
| lines []string |
| } |
| |
| // GetKmsgDumps returns a list of events where the kernel has dumped its kmsg |
| // (kernel log) buffer into pstore because of a kernel oops or panic. |
| func GetKmsgDumps() ([]KmsgDump, error) { |
| return getKmsgDumpsFromFS(os.DirFS(CanonicalMountPath)) |
| } |
| |
| // f is injected here for testing |
| func getKmsgDumpsFromFS(f fs.FS) ([]KmsgDump, error) { |
| var events []KmsgDump |
| eventMap := make(map[string][]pstoreDmesgFile) |
| pstoreEntries, err := fs.ReadDir(f, ".") |
| if err != nil { |
| return events, fmt.Errorf("failed to list files in pstore: %w", err) |
| } |
| for _, entry := range pstoreEntries { |
| if !dmesgFileRegexp.MatchString(entry.Name()) { |
| continue |
| } |
| f, err := f.Open(entry.Name()) |
| if err != nil { |
| return events, fmt.Errorf("failed to open pstore entry file: %w", err) |
| } |
| // This only closes after all files have been read, but the number of |
| // files is heavily bound by very small amounts of pstore space. |
| defer f.Close() |
| finfo, err := f.Stat() |
| if err != nil { |
| return events, fmt.Errorf("failed to stat pstore entry file: %w", err) |
| } |
| s := bufio.NewScanner(f) |
| if !s.Scan() { |
| return events, fmt.Errorf("cannot read first line header of pstore entry %q: %w", entry.Name(), s.Err()) |
| } |
| hdr, err := parseDmesgHeader(s.Text()) |
| if err != nil { |
| return events, fmt.Errorf("failed to parse header of file %q: %w", entry.Name(), err) |
| } |
| var lines []string |
| for s.Scan() { |
| lines = append(lines, s.Text()) |
| } |
| // Same textual encoding is used in the header itself, so this |
| // is as unique as it gets. |
| key := fmt.Sprintf("%v#%d", hdr.Reason, hdr.Counter) |
| eventMap[key] = append(eventMap[key], pstoreDmesgFile{hdr: *hdr, ctime: finfo.ModTime(), lines: lines}) |
| } |
| |
| for _, event := range eventMap { |
| sort.Slice(event, func(i, j int) bool { |
| return event[i].hdr.Part > event[j].hdr.Part |
| }) |
| ev := KmsgDump{ |
| Counter: event[len(event)-1].hdr.Counter, |
| Reason: event[len(event)-1].hdr.Reason, |
| // Entries get created in reverse order, so the most accurate |
| // timestamp is the first one. |
| OccurredAt: event[len(event)-1].ctime, |
| } |
| for _, entry := range event { |
| ev.Lines = append(ev.Lines, entry.lines...) |
| } |
| events = append(events, ev) |
| } |
| sort.Slice(events, func(i, j int) bool { |
| return !events[i].OccurredAt.Before(events[j].OccurredAt) |
| }) |
| return events, nil |
| } |
| |
| // ClearAll clears out all existing entries from the pstore. This should be done |
| // after every start (after the relevant data has been read out) to ensure that |
| // there is always space to store new pstore entries and to minimize the risk |
| // of breaking badly-programmed firmware. |
| func ClearAll() error { |
| pstoreEntries, err := os.ReadDir(CanonicalMountPath) |
| if err != nil { |
| return fmt.Errorf("failed to list files in pstore: %w", err) |
| } |
| for _, entry := range pstoreEntries { |
| if err := os.Remove(filepath.Join(CanonicalMountPath, entry.Name())); err != nil { |
| return fmt.Errorf("failed to clear pstore entry: %w", err) |
| } |
| } |
| return nil |
| } |