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