Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix CPUVulnerabilities() reporting from sysfs #532

Merged
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
81 changes: 50 additions & 31 deletions sysfs/vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
SuperQ marked this conversation as resolved.
Show resolved Hide resolved
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
Expand All @@ -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)
SuperQ marked this conversation as resolved.
Show resolved Hide resolved
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
}
61 changes: 61 additions & 0 deletions sysfs/vulnerability_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
43 changes: 43 additions & 0 deletions testdata/fixtures.ttar
Original file line number Diff line number Diff line change
Expand Up @@ -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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Expand Down