blob: b553c3cfb8b9c2ae76020782d71f3b627290dd04 [file] [log] [blame]
// 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
}