Repository: prometheus/procfs (go)

Issue:
CPU vulnerability reporting from sysfs is broken
I was working on: https://github.com/prometheus/node_exporter/issues/1046 and got a panic when trying to get cpu vulnerabilities:

```
panic: Couldn't create metrics handler: couldn't create collector: failed to get vulnerabilities: unknown vulnerability state for itlb_multihit: Not affected
```

Turns out that the parser is looking for `"Not Affected"` rather than `"Not affected"`.  I'm not sure where the capital letter came from. I don't see it in the sysfs code. I suspect it was copied from the comment in the relevant PR: https://github.com/prometheus/procfs/pull/106#issuecomment-414981407 and this actually never worked.

Pull request description:
Fix CPUVulnerabilities() reporting from sysfs
closes #531 

This PR contains a few changes (each in a separate commit):
- adds fixtures and tests for parsing cpu vulnerabilities from sysfs, fixes the typo in the vulnerability string
- switches to using a map of vulnerabilities instead of a slice (makes working with the result easier, e.g. when testing)
- switches to using encoding of the state
- adds license in the test file
- adds linux build flags
- adds test cases for vulnerable state, brings state string to lower cases (to try and make it case insensitive)

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 25a6795..d53e455 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -35,15 +35,6 @@ jobs:
         steps:
         - run: make test
 
-  codespell:
-    docker:
-    - image: circleci/python
-
-    steps:
-    - checkout
-    - run: sudo pip install codespell
-    - run: codespell --skip=".git,./vendor,ttar,fixtures.ttar,./fixtures,go.mod,go.sum" -I scripts/codespell_ignore.txt
-
 workflows:
   version: 2
   procfs:
@@ -66,4 +57,3 @@ workflows:
             go_version:
             - "1.19"
             - "1.20"
-    - codespell
diff --git a/.golangci.yml b/.golangci.yml
index a197699..c24864a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -2,6 +2,7 @@
 linters:
   enable:
   - godot
+  - misspell
   - revive
 
 linter-settings:
@@ -10,3 +11,5 @@ linter-settings:
     exclude:
     # Ignore "See: URL"
     - 'See:'
+  misspell:
+    locale: US
diff --git a/README.md b/README.md
index 43c3773..1224816 100644
--- a/README.md
+++ b/README.md
@@ -51,11 +51,11 @@ ensure the `fixtures` directory is up to date by removing the existing directory
 extracting the ttar file using `make fixtures/.unpacked` or just `make test`.
 
 ```bash
-rm -rf fixtures
+rm -rf testdata/fixtures
 make test
 ```
 
 Next, make the required changes to the extracted files in the `fixtures` directory.  When
 the changes are complete, run `make update_fixtures` to create a new `fixtures.ttar` file
 based on the updated `fixtures` directory.  And finally, verify the changes using
-`git diff fixtures.ttar`.
+`git diff testdata/fixtures.ttar`.
diff --git a/net_route.go b/net_route.go
new file mode 100644
index 0000000..deb7029
--- /dev/null
+++ b/net_route.go
@@ -0,0 +1,143 @@
+// Copyright 2023 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package procfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+const (
+	blackholeRepresentation string = "*"
+	blackholeIfaceName      string = "blackhole"
+	routeLineColumns        int    = 11
+)
+
+// A NetRouteLine represents one line from net/route.
+type NetRouteLine struct {
+	Iface       string
+	Destination uint32
+	Gateway     uint32
+	Flags       uint32
+	RefCnt      uint32
+	Use         uint32
+	Metric      uint32
+	Mask        uint32
+	MTU         uint32
+	Window      uint32
+	IRTT        uint32
+}
+
+func (fs FS) NetRoute() ([]NetRouteLine, error) {
+	return readNetRoute(fs.proc.Path("net", "route"))
+}
+
+func readNetRoute(path string) ([]NetRouteLine, error) {
+	b, err := util.ReadFileNoStat(path)
+	if err != nil {
+		return nil, err
+	}
+
+	routelines, err := parseNetRoute(bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("failed to read net route from %s: %w", path, err)
+	}
+	return routelines, nil
+}
+
+func parseNetRoute(r io.Reader) ([]NetRouteLine, error) {
+	var routelines []NetRouteLine
+
+	scanner := bufio.NewScanner(r)
+	scanner.Scan()
+	for scanner.Scan() {
+		fields := strings.Fields(scanner.Text())
+		routeline, err := parseNetRouteLine(fields)
+		if err != nil {
+			return nil, err
+		}
+		routelines = append(routelines, *routeline)
+	}
+	return routelines, nil
+}
+
+func parseNetRouteLine(fields []string) (*NetRouteLine, error) {
+	if len(fields) != routeLineColumns {
+		return nil, fmt.Errorf("invalid routeline, num of digits: %d", len(fields))
+	}
+	iface := fields[0]
+	if iface == blackholeRepresentation {
+		iface = blackholeIfaceName
+	}
+	destination, err := strconv.ParseUint(fields[1], 16, 32)
+	if err != nil {
+		return nil, err
+	}
+	gateway, err := strconv.ParseUint(fields[2], 16, 32)
+	if err != nil {
+		return nil, err
+	}
+	flags, err := strconv.ParseUint(fields[3], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	refcnt, err := strconv.ParseUint(fields[4], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	use, err := strconv.ParseUint(fields[5], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	metric, err := strconv.ParseUint(fields[6], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	mask, err := strconv.ParseUint(fields[7], 16, 32)
+	if err != nil {
+		return nil, err
+	}
+	mtu, err := strconv.ParseUint(fields[8], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	window, err := strconv.ParseUint(fields[9], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	irtt, err := strconv.ParseUint(fields[10], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	routeline := &NetRouteLine{
+		Iface:       iface,
+		Destination: uint32(destination),
+		Gateway:     uint32(gateway),
+		Flags:       uint32(flags),
+		RefCnt:      uint32(refcnt),
+		Use:         uint32(use),
+		Metric:      uint32(metric),
+		Mask:        uint32(mask),
+		MTU:         uint32(mtu),
+		Window:      uint32(window),
+		IRTT:        uint32(irtt),
+	}
+	return routeline, nil
+}
diff --git a/scripts/codespell_ignore.txt b/scripts/codespell_ignore.txt
deleted file mode 100644
index 4b41a23..0000000
--- a/scripts/codespell_ignore.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-alo
-als
-ded
-hart
-hda
-inflight
-packages\'
-ro
-sie
-te
-uint
diff --git a/sysfs/vulnerability.go b/sysfs/vulnerability.go
index 87e7016..08927a2 100644
--- a/sysfs/vulnerability.go
+++ b/sysfs/vulnerability.go
@@ -24,33 +24,49 @@ import (
 )
 
 const (
-	notAffected = "Not Affected"
-	vulnerable  = "Vulnerable"
-	mitigation  = "Mitigation"
+	notAffected = "not affected" // based on: https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu
+	vulnerable  = "vulnerable"
+	mitigation  = "mitigation"
+)
+
+const (
+	VulnerabilityStateNotAffected = iota
+	VulnerabilityStateVulnerable
+	VulnerabilityStateMitigation
+)
+
+var (
+	// VulnerabilityHumanEncoding allows mapping the vulnerability state (encoded as an int) onto a human friendly
+	// string. It can be used by consumers of this library to expose to the user the state of the vulnerability.
+	VulnerabilityHumanEncoding = map[int]string{
+		VulnerabilityStateNotAffected: notAffected,
+		VulnerabilityStateVulnerable:  vulnerable,
+		VulnerabilityStateMitigation:  mitigation,
+	}
 )
 
 // CPUVulnerabilities retrieves a map of vulnerability names to their mitigations.
-func (fs FS) CPUVulnerabilities() ([]Vulnerability, error) {
-	matches, err := filepath.Glob(fs.sys.Path("devices/system/cpu/vulnerabilities/*"))
+func (fs FS) CPUVulnerabilities() (map[string]*Vulnerability, error) {
+	matchingFilepaths, err := filepath.Glob(fs.sys.Path("devices/system/cpu/vulnerabilities/*"))
 	if err != nil {
 		return nil, err
 	}
 
-	vulnerabilities := make([]Vulnerability, 0, len(matches))
-	for _, match := range matches {
-		name := filepath.Base(match)
+	vulnerabilities := make(map[string]*Vulnerability, len(matchingFilepaths))
+	for _, path := range matchingFilepaths {
+		filename := filepath.Base(path)
 
-		value, err := os.ReadFile(match)
+		rawContent, err := os.ReadFile(path)
 		if err != nil {
 			return nil, err
 		}
 
-		v, err := parseVulnerability(name, string(value))
+		v, err := parseVulnerability(filename, string(rawContent))
 		if err != nil {
 			return nil, err
 		}
 
-		vulnerabilities = append(vulnerabilities, v)
+		vulnerabilities[filename] = v
 	}
 
 	return vulnerabilities, nil
@@ -59,29 +75,32 @@ func (fs FS) CPUVulnerabilities() ([]Vulnerability, error) {
 // Vulnerability represents a single vulnerability extracted from /sys/devices/system/cpu/vulnerabilities/.
 type Vulnerability struct {
 	CodeName   string
-	State      string
+	State      int
 	Mitigation string
 }
 
-func parseVulnerability(name, value string) (Vulnerability, error) {
-	v := Vulnerability{CodeName: name}
-	value = strings.TrimSpace(value)
-	if value == notAffected {
-		v.State = notAffected
-		return v, nil
-	}
-
-	if strings.HasPrefix(value, vulnerable) {
-		v.State = vulnerable
-		v.Mitigation = strings.TrimPrefix(strings.TrimPrefix(value, vulnerable), ": ")
-		return v, nil
-	}
+func parseVulnerability(name, rawContent string) (*Vulnerability, error) {
+	v := &Vulnerability{CodeName: name}
+	rawContent = strings.TrimSpace(rawContent)
+	rawContentLower := strings.ToLower(rawContent)
+	switch {
+	case strings.HasPrefix(rawContentLower, notAffected):
+		v.State = VulnerabilityStateNotAffected
+	case strings.HasPrefix(rawContentLower, vulnerable):
+		v.State = VulnerabilityStateVulnerable
+		m := strings.Fields(rawContent)
+		if len(m) > 1 {
+			v.Mitigation = strings.Join(m[1:], " ")
+		}
+	case strings.HasPrefix(rawContentLower, mitigation):
+		v.State = VulnerabilityStateMitigation
+		m := strings.Fields(rawContent)
+		if len(m) > 1 {
+			v.Mitigation = strings.Join(m[1:], " ")
+		}
+	default:
+		return nil, fmt.Errorf("unknown vulnerability state for %s: %s", name, rawContent)
 
-	if strings.HasPrefix(value, mitigation) {
-		v.State = mitigation
-		v.Mitigation = strings.TrimPrefix(strings.TrimPrefix(value, mitigation), ": ")
-		return v, nil
 	}
-
-	return v, fmt.Errorf("unknown vulnerability state for %s: %s", name, value)
+	return v, nil
 }


Test patch:
diff --git a/net_route_test.go b/net_route_test.go
new file mode 100644
index 0000000..0464036
--- /dev/null
+++ b/net_route_test.go
@@ -0,0 +1,60 @@
+// Copyright 2023 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package procfs
+
+import (
+	"bytes"
+	"reflect"
+	"testing"
+)
+
+func TestParseNetRoute(t *testing.T) {
+	var netRoute = []byte(`Iface            Destination  Gateway   Flags  RefCnt  Use  Metric  Mask      MTU  Window  IRTT
+eno16780032      00000000     9503A8C0  0003   0       0    100     00000000  0    0       0
+eno16780032      0000A8C0     00000000  0001   0       0    100     0000FFFF  0    0       0`)
+
+	r := bytes.NewReader(netRoute)
+	parsed, _ := parseNetRoute(r)
+	want := []NetRouteLine{
+		{
+			Iface:       "eno16780032",
+			Destination: 0,
+			Gateway:     2500044992,
+			Flags:       3,
+			RefCnt:      0,
+			Use:         0,
+			Metric:      100,
+			Mask:        0,
+			MTU:         0,
+			Window:      0,
+			IRTT:        0,
+		},
+		{
+			Iface:       "eno16780032",
+			Destination: 43200,
+			Gateway:     0,
+			Flags:       1,
+			RefCnt:      0,
+			Use:         0,
+			Metric:      100,
+			Mask:        65535,
+			MTU:         0,
+			Window:      0,
+			IRTT:        0,
+		},
+	}
+	if !reflect.DeepEqual(want, parsed) {
+		t.Errorf("want %v, parsed %v", want, parsed)
+	}
+}
diff --git a/sysfs/vulnerability_test.go b/sysfs/vulnerability_test.go
new file mode 100644
index 0000000..255586d
--- /dev/null
+++ b/sysfs/vulnerability_test.go
@@ -0,0 +1,61 @@
+// Copyright 2023 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build linux
+// +build linux
+
+package sysfs
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestFS_CPUVulnerabilities(t *testing.T) {
+	sysFs, err := NewFS(sysTestFixtures)
+	if err != nil {
+		t.Fatal(fmt.Errorf("failed to get sysfs FS: %w", err))
+	}
+	got, err := sysFs.CPUVulnerabilities()
+	if err != nil {
+		t.Fatal(fmt.Errorf("failed to parse sysfs vulnerabilities files: %w", err))
+	}
+
+	tests := []struct {
+		name              string
+		vulnerabilityName string
+		want              *Vulnerability
+		wantErr           bool
+	}{
+		{"Not affected", "itlb_multihit", &Vulnerability{CodeName: "itlb_multihit", State: VulnerabilityStateNotAffected, Mitigation: ""}, false},
+		{"Not affected with underscores", "tsx_async_abort", &Vulnerability{CodeName: "tsx_async_abort", State: VulnerabilityStateNotAffected, Mitigation: ""}, false},
+		{"Mitigation simple string", "spec_store_bypass", &Vulnerability{CodeName: "spec_store_bypass", State: VulnerabilityStateMitigation, Mitigation: "Speculative Store Bypass disabled via prctl"}, false},
+		{"Mitigation special chars", "retbleed", &Vulnerability{CodeName: "retbleed", State: VulnerabilityStateMitigation, Mitigation: "untrained return thunk; SMT enabled with STIBP protection"}, false},
+		{"Mitigation more special chars", "spectre_v1", &Vulnerability{CodeName: "spectre_v1", State: VulnerabilityStateMitigation, Mitigation: "usercopy/swapgs barriers and __user pointer sanitization"}, false},
+		{"Mitigation with multiple subsections", "spectre_v2", &Vulnerability{CodeName: "spectre_v2", State: VulnerabilityStateMitigation, Mitigation: "Retpolines, IBPB: conditional, STIBP: always-on, RSB filling, PBRSB-eIBRS: Not affected"}, false},
+		{"Vulnerable", "mds", &Vulnerability{CodeName: "mds", State: VulnerabilityStateVulnerable, Mitigation: ""}, false},
+		{"Vulnerable with mitigation available", "mmio_stale_data", &Vulnerability{CodeName: "mmio_stale_data", State: VulnerabilityStateVulnerable, Mitigation: "Clear CPU buffers attempted, no microcode"}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotVulnerability, ok := got[tt.vulnerabilityName]
+			if !ok && !tt.wantErr {
+				t.Errorf("CPUVulnerabilities() vulnerability %s not found", tt.vulnerabilityName)
+			}
+			if !reflect.DeepEqual(gotVulnerability, tt.want) {
+				t.Errorf("CPUVulnerabilities() gotVulnerability = %v, want %v", gotVulnerability, tt.want)
+			}
+		})
+	}
+}
diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar
index cc2ff45..decfe16 100644
--- a/testdata/fixtures.ttar
+++ b/testdata/fixtures.ttar
@@ -13234,6 +13234,49 @@ Lines: 1
 2
 Mode: 664
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/cpu/vulnerabilities
+Mode: 755
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/itlb_multihit
+Lines: 1
+Not affected
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/mds
+Lines: 1
+Vulnerable
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/mmio_stale_data
+Lines: 1
+Vulnerable: Clear CPU buffers attempted, no microcode
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/retbleed
+Lines: 1
+Mitigation: untrained return thunk; SMT enabled with STIBP protection
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/spec_store_bypass
+Lines: 1
+Mitigation: Speculative Store Bypass disabled via prctl
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/spectre_v1
+Lines: 1
+Mitigation: usercopy/swapgs barriers and __user pointer sanitization
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/spectre_v2
+Lines: 1
+Mitigation: Retpolines, IBPB: conditional, STIBP: always-on, RSB filling, PBRSB-eIBRS: Not affected
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/devices/system/cpu/vulnerabilities/tsx_async_abort
+Lines: 1
+Not affected
+Mode: 444
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/devices/system/node
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -