From 094cebbb6ca9aacb77bb3ce3ad27fa105ce387f6 Mon Sep 17 00:00:00 2001 From: Lakshmi Pradeep Date: Tue, 20 Oct 2026 11:21:26 -0700 Subject: [PATCH 1/2] sysfs: add AER counters to PciDevice Parse aer_dev_correctable, aer_dev_fatal and aer_dev_nonfatal for every PCI device under /sys/bus/pci/devices, exposing them as the optional AerCorrectable, AerFatal and AerNonFatal fields. They are nil when the kernel does not expose AER statistics for a device. AER parsing was previously only reachable through network interfaces (/sys/class/net//device). The file parsers are refactored to take a file path so both callers share them; the net class behaviour is unchanged. Signed-off-by: Lakshmi Pradeep --- sysfs/net_class_aer.go | 37 +++++++++++++++++++++++++------------ sysfs/net_class_aer_test.go | 7 ++++++- sysfs/pci_device.go | 33 +++++++++++++++++++++++++++++++++ sysfs/pci_device_test.go | 14 ++++++++++---- testdata/fixtures.ttar | 18 +++++++++--------- 5 files changed, 83 insertions(+), 26 deletions(-) diff --git a/sysfs/net_class_aer.go b/sysfs/net_class_aer.go index 8677330f..a2a8d37a 100644 --- a/sysfs/net_class_aer.go +++ b/sysfs/net_class_aer.go @@ -133,13 +133,19 @@ func parseAerCounters(devicePath string) (*AerCounters, error) { // parseCorrectableAerCounters parses correctable error counters in // /sys/class/net//device/aer_dev_correctable. func parseCorrectableAerCounters(devicePath string, counters *CorrectableAerCounters) error { - path := filepath.Join(devicePath, "device", "aer_dev_correctable") + _, err := parseCorrectableAerFile(filepath.Join(devicePath, "device", "aer_dev_correctable"), counters) + return err +} + +// parseCorrectableAerFile parses a correctable AER counters file such as +// aer_dev_correctable. It reports whether the file was present and readable. +func parseCorrectableAerFile(path string, counters *CorrectableAerCounters) (bool, error) { value, err := parsers.SysReadFile(path) if err != nil { if canIgnoreError(err) { - return nil + return false, nil } - return fmt.Errorf("failed to read file %q: %w", path, err) + return false, fmt.Errorf("failed to read file %q: %w", path, err) } for line := range strings.SplitSeq(string(value), "\n") { @@ -148,12 +154,12 @@ func parseCorrectableAerCounters(devicePath string, counters *CorrectableAerCoun } fields := strings.Fields(line) if len(fields) != 2 { - return fmt.Errorf("unexpected number of fields: %v", fields) + return false, fmt.Errorf("unexpected number of fields: %v", fields) } counterName := fields[0] value, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { - return fmt.Errorf("error parsing value for %s: %w", counterName, err) + return false, fmt.Errorf("error parsing value for %s: %w", counterName, err) } switch counterName { @@ -178,7 +184,7 @@ func parseCorrectableAerCounters(devicePath string, counters *CorrectableAerCoun } } - return nil + return true, nil } // parseUncorrectableAerCounters parses uncorrectable error counters in @@ -186,13 +192,20 @@ func parseCorrectableAerCounters(devicePath string, counters *CorrectableAerCoun func parseUncorrectableAerCounters(devicePath, counterType string, counters *UncorrectableAerCounters, ) error { - path := filepath.Join(devicePath, "device", "aer_dev_"+counterType) + _, err := parseUncorrectableAerFile(filepath.Join(devicePath, "device", "aer_dev_"+counterType), counters) + return err +} + +// parseUncorrectableAerFile parses an uncorrectable AER counters file such as +// aer_dev_fatal or aer_dev_nonfatal. It reports whether the file was present +// and readable. +func parseUncorrectableAerFile(path string, counters *UncorrectableAerCounters) (bool, error) { value, err := parsers.ReadFileNoStat(path) if err != nil { if canIgnoreError(err) { - return nil + return false, nil } - return fmt.Errorf("failed to read file %q: %w", path, err) + return false, fmt.Errorf("failed to read file %q: %w", path, err) } for line := range strings.SplitSeq(string(value), "\n") { @@ -201,12 +214,12 @@ func parseUncorrectableAerCounters(devicePath, counterType string, } fields := strings.Fields(line) if len(fields) != 2 { - return fmt.Errorf("unexpected number of fields: %v", fields) + return false, fmt.Errorf("unexpected number of fields: %v", fields) } counterName := fields[0] value, err := strconv.ParseUint(fields[1], 10, 64) if err != nil { - return fmt.Errorf("error parsing value for %s: %w", counterName, err) + return false, fmt.Errorf("error parsing value for %s: %w", counterName, err) } switch counterName { @@ -251,5 +264,5 @@ func parseUncorrectableAerCounters(devicePath, counterType string, } } - return nil + return true, nil } diff --git a/sysfs/net_class_aer_test.go b/sysfs/net_class_aer_test.go index 07c128cc..508ebd04 100644 --- a/sysfs/net_class_aer_test.go +++ b/sysfs/net_class_aer_test.go @@ -50,7 +50,12 @@ func TestAerCounters(t *testing.T) { ac, _ := fs.AerCounters() aerCounters := AllAerCounters{ - "enp3s0f0": AerCounters{Name: "enp3s0f0"}, + "enp3s0f0": AerCounters{ + Name: "enp3s0f0", + Correctable: CorrectableAerCounters{RxErr: 2, BadTLP: 1, Timeout: 3}, + Fatal: UncorrectableAerCounters{MalfTLP: 1}, + NonFatal: UncorrectableAerCounters{CmpltTO: 1, UnsupReq: 4}, + }, "eth0": AerCounters{ Name: "eth0", Correctable: CorrectableAerCounters{ diff --git a/sysfs/pci_device.go b/sysfs/pci_device.go index f66bf66b..67f0d93d 100644 --- a/sysfs/pci_device.go +++ b/sysfs/pci_device.go @@ -97,6 +97,12 @@ type PciDevice struct { D3coldAllowed *bool // /sys/bus/pci/devices//d3cold_allowed PowerState *PciPowerState // /sys/bus/pci/devices//power_state + + // PCIe Advanced Error Reporting counters. These are nil when the kernel + // does not expose AER statistics for the device. + AerCorrectable *CorrectableAerCounters // /sys/bus/pci/devices//aer_dev_correctable + AerFatal *UncorrectableAerCounters // /sys/bus/pci/devices//aer_dev_fatal + AerNonFatal *UncorrectableAerCounters // /sys/bus/pci/devices//aer_dev_nonfatal } func (pd PciDevice) Name() string { @@ -417,5 +423,32 @@ func (fs FS) parsePciDevice(name string) (*PciDevice, error) { } } + // Parse AER counters (optional: only present on PCIe devices with AER + // support and kernels built with CONFIG_PCIEAER). + var correctable CorrectableAerCounters + found, err := parseCorrectableAerFile(filepath.Join(path, "aer_dev_correctable"), &correctable) + if err != nil { + return nil, fmt.Errorf("failed to parse AER counters %s: %w", device.Location, err) + } + if found { + device.AerCorrectable = &correctable + } + for _, f := range [...]string{"aer_dev_fatal", "aer_dev_nonfatal"} { + var uncorrectable UncorrectableAerCounters + found, err := parseUncorrectableAerFile(filepath.Join(path, f), &uncorrectable) + if err != nil { + return nil, fmt.Errorf("failed to parse AER counters %s: %w", device.Location, err) + } + if !found { + continue + } + switch f { + case "aer_dev_fatal": + device.AerFatal = &uncorrectable + case "aer_dev_nonfatal": + device.AerNonFatal = &uncorrectable + } + } + return device, nil } diff --git a/sysfs/pci_device_test.go b/sysfs/pci_device_test.go index 3ad76ffb..8b5e5539 100644 --- a/sysfs/pci_device_test.go +++ b/sysfs/pci_device_test.go @@ -106,8 +106,11 @@ func TestPciDevices(t *testing.T) { CurrentLinkSpeed: &LinkSpeed8GTs, CurrentLinkWidth: &LinkWidth4, - D3coldAllowed: &D3coldAllowed, - PowerState: &PowerState, + D3coldAllowed: &D3coldAllowed, + PowerState: &PowerState, + AerCorrectable: &CorrectableAerCounters{}, + AerFatal: &UncorrectableAerCounters{}, + AerNonFatal: &UncorrectableAerCounters{}, }, "0000:a2:00:0": PciDevice{ Location: PciDeviceLocation{ @@ -141,8 +144,11 @@ func TestPciDevices(t *testing.T) { SriovVfTotalMsix: &SriovVfTotalMsix, // Power management fields - D3coldAllowed: &D3coldAllowed, - PowerState: &PowerState, + D3coldAllowed: &D3coldAllowed, + PowerState: &PowerState, + AerCorrectable: &CorrectableAerCounters{RxErr: 2, BadTLP: 1, Timeout: 3}, + AerFatal: &UncorrectableAerCounters{MalfTLP: 1}, + AerNonFatal: &UncorrectableAerCounters{CmpltTO: 1, UnsupReq: 4}, }, } diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar index b6f18ae7..da185971 100644 --- a/testdata/fixtures.ttar +++ b/testdata/fixtures.ttar @@ -17338,15 +17338,15 @@ Mode: 755 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Path: fixtures/sys/devices/pci0000:a2/0000:a2:00.0/aer_dev_correctable Lines: 9 -RxErr 0 -BadTLP 0 +RxErr 2 +BadTLP 1 BadDLLP 0 Rollover 0 -Timeout 0 +Timeout 3 NonFatalErr 0 CorrIntErr 0 HeaderOF 0 -TOTAL_ERR_COR 0 +TOTAL_ERR_COR 6 Mode: 444 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Path: fixtures/sys/devices/pci0000:a2/0000:a2:00.0/aer_dev_fatal @@ -17360,7 +17360,7 @@ CmpltTO 0 CmpltAbrt 0 UnxCmplt 0 RxOF 0 -MalfTLP 0 +MalfTLP 1 ECRC 0 UnsupReq 0 ACSViol 0 @@ -17369,7 +17369,7 @@ BlockedTLP 0 AtomicOpBlocked 0 TLPBlockedErr 0 PoisonTLPBlocked 0 -TOTAL_ERR_FATAL 0 +TOTAL_ERR_FATAL 1 Mode: 444 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Path: fixtures/sys/devices/pci0000:a2/0000:a2:00.0/aer_dev_nonfatal @@ -17379,20 +17379,20 @@ DLP 0 SDES 0 TLP 0 FCP 0 -CmpltTO 0 +CmpltTO 1 CmpltAbrt 0 UnxCmplt 0 RxOF 0 MalfTLP 0 ECRC 0 -UnsupReq 0 +UnsupReq 4 ACSViol 0 UncorrIntErr 0 BlockedTLP 0 AtomicOpBlocked 0 TLPBlockedErr 0 PoisonTLPBlocked 0 -TOTAL_ERR_NONFATAL 0 +TOTAL_ERR_NONFATAL 5 Mode: 444 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Path: fixtures/sys/devices/pci0000:a2/0000:a2:00.0/ari_enabled From f07ed05022cdc29e24dd21a4b0adc954fbe893da Mon Sep 17 00:00:00 2001 From: Lakshmi Pradeep Date: Tue, 20 Oct 2026 11:24:32 -0700 Subject: [PATCH 2/2] sysfs: add Root Port AER totals to PciDevice Parse aer_rootport_total_err_cor, aer_rootport_total_err_fatal and aer_rootport_total_err_nonfatal into the optional AerRootPortTotalErrCor, AerRootPortTotalErrFatal and AerRootPortTotalErrNonFatal fields. The kernel creates these files only on AER-capable PCIe Root Ports, where they count every error reported to the port by the devices below it; the fields are nil elsewhere. Signed-off-by: Lakshmi Pradeep --- sysfs/pci_device.go | 36 ++++++++++++++++++++++++++++++++++++ sysfs/pci_device_test.go | 9 +++++++++ testdata/fixtures.ttar | 15 +++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/sysfs/pci_device.go b/sysfs/pci_device.go index 67f0d93d..baac9dd8 100644 --- a/sysfs/pci_device.go +++ b/sysfs/pci_device.go @@ -103,6 +103,12 @@ type PciDevice struct { AerCorrectable *CorrectableAerCounters // /sys/bus/pci/devices//aer_dev_correctable AerFatal *UncorrectableAerCounters // /sys/bus/pci/devices//aer_dev_fatal AerNonFatal *UncorrectableAerCounters // /sys/bus/pci/devices//aer_dev_nonfatal + + // Totals of the errors a PCIe Root Port has received from the devices + // below it. These are nil for devices that are not AER-capable Root Ports. + AerRootPortTotalErrCor *uint64 // /sys/bus/pci/devices//aer_rootport_total_err_cor + AerRootPortTotalErrFatal *uint64 // /sys/bus/pci/devices//aer_rootport_total_err_fatal + AerRootPortTotalErrNonFatal *uint64 // /sys/bus/pci/devices//aer_rootport_total_err_nonfatal } func (pd PciDevice) Name() string { @@ -450,5 +456,35 @@ func (fs FS) parsePciDevice(name string) (*PciDevice, error) { } } + // Parse Root Port AER totals (optional: only present on Root Ports). + for _, f := range [...]string{"aer_rootport_total_err_cor", "aer_rootport_total_err_fatal", "aer_rootport_total_err_nonfatal"} { + name := filepath.Join(path, f) + valueStr, err := parsers.SysReadFile(name) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, fmt.Errorf("failed to read AER Root Port file %q %s: %w", name, device.Location, err) + } + + valueStr = strings.TrimSpace(valueStr) + if valueStr == "" { + continue + } + value, err := strconv.ParseUint(valueStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse %s %q %s: %w", f, valueStr, device.Location, err) + } + + switch f { + case "aer_rootport_total_err_cor": + device.AerRootPortTotalErrCor = &value + case "aer_rootport_total_err_fatal": + device.AerRootPortTotalErrFatal = &value + case "aer_rootport_total_err_nonfatal": + device.AerRootPortTotalErrNonFatal = &value + } + } + return device, nil } diff --git a/sysfs/pci_device_test.go b/sysfs/pci_device_test.go index 8b5e5539..109aaa75 100644 --- a/sysfs/pci_device_test.go +++ b/sysfs/pci_device_test.go @@ -52,6 +52,11 @@ func TestPciDevices(t *testing.T) { NumaNodeNeg1 = int32(-1) D3coldAllowed = true PowerState = PciPowerStateD0 + + // Root Port AER totals on 0000:00:02.1 + RootPortErrCor = uint64(7) + RootPortErrFatal = uint64(0) + RootPortErrNonFatal = uint64(2) ) want := PciDevices{ "0000:00:02:1": PciDevice{ @@ -78,6 +83,10 @@ func TestPciDevices(t *testing.T) { D3coldAllowed: &D3coldAllowed, PowerState: &PowerState, + + AerRootPortTotalErrCor: &RootPortErrCor, + AerRootPortTotalErrFatal: &RootPortErrFatal, + AerRootPortTotalErrNonFatal: &RootPortErrNonFatal, }, "0000:01:00:0": PciDevice{ Location: PciDeviceLocation{ diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar index da185971..62ad5cab 100644 --- a/testdata/fixtures.ttar +++ b/testdata/fixtures.ttar @@ -10755,6 +10755,21 @@ Lines: 1 0xc0a9 Mode: 444 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/devices/pci0000:00/0000:00:02.1/aer_rootport_total_err_cor +Lines: 1 +7 +Mode: 444 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/devices/pci0000:00/0000:00:02.1/aer_rootport_total_err_fatal +Lines: 1 +0 +Mode: 444 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/sys/devices/pci0000:00/0000:00:02.1/aer_rootport_total_err_nonfatal +Lines: 1 +2 +Mode: 444 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Path: fixtures/sys/devices/pci0000:00/0000:00:02.1/ari_enabled Lines: 1 0