blob: 3958498dd60e9cbdb06ed2efbbb7e3de8ef2f302 [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
Lorenz Brund1f82e92024-02-08 19:27:46 +010076var pmsgFileRegexp = regexp.MustCompile("^pmsg-.*-([0-9]+)")
77
Lorenz Brunf8da2e72022-06-14 12:39:32 +020078type pstoreDmesgFile struct {
79 hdr pstoreDmesgHeader
80 ctime time.Time
81 lines []string
82}
83
84// GetKmsgDumps returns a list of events where the kernel has dumped its kmsg
85// (kernel log) buffer into pstore because of a kernel oops or panic.
86func GetKmsgDumps() ([]KmsgDump, error) {
87 return getKmsgDumpsFromFS(os.DirFS(CanonicalMountPath))
88}
89
Lorenz Brund1f82e92024-02-08 19:27:46 +010090// GetPmsgDump returns lines written into /dev/pmsg0
91func GetPmsgDump() ([]string, error) {
92 var lines []string
93 pstoreEntries, err := os.ReadDir(CanonicalMountPath)
94 if err != nil {
Tim Windelschmidt3074ec62024-04-23 15:08:05 +020095 return nil, fmt.Errorf("failed to list files in pstore: %w", err)
Lorenz Brund1f82e92024-02-08 19:27:46 +010096 }
97 for _, entry := range pstoreEntries {
98 if !pmsgFileRegexp.MatchString(entry.Name()) {
99 continue
100 }
101 f, err := os.Open(filepath.Join(CanonicalMountPath, entry.Name()))
102 if err != nil {
103 return lines, fmt.Errorf("failed to open pstore entry file: %w", err)
104 }
105 // This only closes after all files have been read, but the number of
106 // files is heavily bound by very small amounts of pstore space.
107 defer f.Close()
108 s := bufio.NewScanner(f)
109 for s.Scan() {
110 lines = append(lines, s.Text())
111 }
112 }
113 return lines, nil
114}
115
Lorenz Brunf8da2e72022-06-14 12:39:32 +0200116// f is injected here for testing
117func getKmsgDumpsFromFS(f fs.FS) ([]KmsgDump, error) {
118 var events []KmsgDump
119 eventMap := make(map[string][]pstoreDmesgFile)
120 pstoreEntries, err := fs.ReadDir(f, ".")
121 if err != nil {
122 return events, fmt.Errorf("failed to list files in pstore: %w", err)
123 }
124 for _, entry := range pstoreEntries {
125 if !dmesgFileRegexp.MatchString(entry.Name()) {
126 continue
127 }
128 f, err := f.Open(entry.Name())
129 if err != nil {
130 return events, fmt.Errorf("failed to open pstore entry file: %w", err)
131 }
132 // This only closes after all files have been read, but the number of
133 // files is heavily bound by very small amounts of pstore space.
134 defer f.Close()
135 finfo, err := f.Stat()
136 if err != nil {
137 return events, fmt.Errorf("failed to stat pstore entry file: %w", err)
138 }
139 s := bufio.NewScanner(f)
140 if !s.Scan() {
141 return events, fmt.Errorf("cannot read first line header of pstore entry %q: %w", entry.Name(), s.Err())
142 }
143 hdr, err := parseDmesgHeader(s.Text())
144 if err != nil {
145 return events, fmt.Errorf("failed to parse header of file %q: %w", entry.Name(), err)
146 }
147 var lines []string
148 for s.Scan() {
149 lines = append(lines, s.Text())
150 }
151 // Same textual encoding is used in the header itself, so this
152 // is as unique as it gets.
153 key := fmt.Sprintf("%v#%d", hdr.Reason, hdr.Counter)
154 eventMap[key] = append(eventMap[key], pstoreDmesgFile{hdr: *hdr, ctime: finfo.ModTime(), lines: lines})
155 }
156
157 for _, event := range eventMap {
158 sort.Slice(event, func(i, j int) bool {
159 return event[i].hdr.Part > event[j].hdr.Part
160 })
161 ev := KmsgDump{
162 Counter: event[len(event)-1].hdr.Counter,
163 Reason: event[len(event)-1].hdr.Reason,
164 // Entries get created in reverse order, so the most accurate
165 // timestamp is the first one.
166 OccurredAt: event[len(event)-1].ctime,
167 }
168 for _, entry := range event {
169 ev.Lines = append(ev.Lines, entry.lines...)
170 }
171 events = append(events, ev)
172 }
173 sort.Slice(events, func(i, j int) bool {
174 return !events[i].OccurredAt.Before(events[j].OccurredAt)
175 })
176 return events, nil
177}
178
179// ClearAll clears out all existing entries from the pstore. This should be done
180// after every start (after the relevant data has been read out) to ensure that
181// there is always space to store new pstore entries and to minimize the risk
182// of breaking badly-programmed firmware.
183func ClearAll() error {
184 pstoreEntries, err := os.ReadDir(CanonicalMountPath)
185 if err != nil {
186 return fmt.Errorf("failed to list files in pstore: %w", err)
187 }
188 for _, entry := range pstoreEntries {
189 if err := os.Remove(filepath.Join(CanonicalMountPath, entry.Name())); err != nil {
190 return fmt.Errorf("failed to clear pstore entry: %w", err)
191 }
192 }
193 return nil
194}