blob: b553c3cfb8b9c2ae76020782d71f3b627290dd04 [file] [log] [blame]
Lorenz Brunf8da2e72022-06-14 12:39:32 +02001// 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.
5package pstore
6
7import (
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
21const CanonicalMountPath = "/sys/fs/pstore"
22
23// pstoreDmesgHeader contains parsed header data from a pstore header.
24type pstoreDmesgHeader struct {
25 Reason string
26 Counter uint64
27 Part uint64
28}
29
30var 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.
35func 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.
56type 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
74var dmesgFileRegexp = regexp.MustCompile("^dmesg-.*-([0-9]+)")
75
76type 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.
84func GetKmsgDumps() ([]KmsgDump, error) {
85 return getKmsgDumpsFromFS(os.DirFS(CanonicalMountPath))
86}
87
88// f is injected here for testing
89func 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.
155func 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}