Add initial mitigate code and cpu parsing.

PiperOrigin-RevId: 353274135
This commit is contained in:
Zach Koopmans
2021-01-22 10:52:57 -08:00
committed by gVisor bot
parent 9b4f4655ed
commit 16b81308cf
4 changed files with 641 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
load("//tools:defs.bzl", "go_library", "go_test")
package(licenses = ["notice"])
go_library(
name = "mitigate",
srcs = [
"cpu.go",
"mitigate.go",
],
)
go_test(
name = "mitigate_test",
size = "small",
srcs = ["cpu_test.go"],
library = ":mitigate",
)
+235
View File
@@ -0,0 +1,235 @@
// Copyright 2021 The gVisor 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 mitigate
import (
"fmt"
"regexp"
"strconv"
"strings"
)
const (
// constants of coomm
meltdown = "cpu_meltdown"
l1tf = "l1tf"
mds = "mds"
swapgs = "swapgs"
taa = "taa"
)
const (
processorKey = "processor"
vendorIDKey = "vendor_id"
cpuFamilyKey = "cpu family"
modelKey = "model"
coreIDKey = "core id"
bugsKey = "bugs"
)
// getCPUSet returns cpu structs from reading /proc/cpuinfo.
func getCPUSet(data string) ([]*cpu, error) {
// Each processor entry should start with the
// processor key. Find the beginings of each.
r := buildRegex(processorKey, `\d+`)
indices := r.FindAllStringIndex(data, -1)
if len(indices) < 1 {
return nil, fmt.Errorf("no cpus found for: %s", data)
}
// Add the ending index for last entry.
indices = append(indices, []int{len(data), -1})
// Valid cpus are now defined by strings in between
// indexes (e.g. data[index[i], index[i+1]]).
// There should be len(indicies) - 1 CPUs
// since the last index is the end of the string.
var cpus = make([]*cpu, 0, len(indices)-1)
// Find each string that represents a CPU. These begin "processor".
for i := 1; i < len(indices); i++ {
start := indices[i-1][0]
end := indices[i][0]
// Parse the CPU entry, which should be between start/end.
c, err := getCPU(data[start:end])
if err != nil {
return nil, err
}
cpus = append(cpus, c)
}
return cpus, nil
}
// type cpu represents pertinent info about a cpu.
type cpu struct {
processorNumber int64 // the processor number of this CPU.
vendorID string // the vendorID of CPU (e.g. AuthenticAMD).
cpuFamily int64 // CPU family number (e.g. 6 for CascadeLake/Skylake).
model int64 // CPU model number (e.g. 85 for CascadeLake/Skylake).
coreID int64 // This CPU's core id to match Hyperthread Pairs
bugs map[string]struct{} // map of vulnerabilities parsed from the 'bugs' field.
}
// getCPU parses a CPU from a single cpu entry from /proc/cpuinfo.
func getCPU(data string) (*cpu, error) {
processor, err := parseProcessor(data)
if err != nil {
return nil, err
}
vendorID, err := parseVendorID(data)
if err != nil {
return nil, err
}
cpuFamily, err := parseCPUFamily(data)
if err != nil {
return nil, err
}
model, err := parseModel(data)
if err != nil {
return nil, err
}
coreID, err := parseCoreID(data)
if err != nil {
return nil, err
}
bugs, err := parseBugs(data)
if err != nil {
return nil, err
}
return &cpu{
processorNumber: processor,
vendorID: vendorID,
cpuFamily: cpuFamily,
model: model,
coreID: coreID,
bugs: bugs,
}, nil
}
// List of pertinent side channel vulnerablilites.
// For mds, see: https://www.kernel.org/doc/html/latest/admin-guide/hw-vuln/mds.html.
var vulnerabilities = []string{
meltdown,
l1tf,
mds,
swapgs,
taa,
}
// isVulnerable checks if a CPU is vulnerable to pertinent bugs.
func (c *cpu) isVulnerable() bool {
for _, bug := range vulnerabilities {
if _, ok := c.bugs[bug]; ok {
return true
}
}
return false
}
// similarTo checks family/model/bugs fields for equality of two
// processors.
func (c *cpu) similarTo(other *cpu) bool {
if c.vendorID != other.vendorID {
return false
}
if other.cpuFamily != c.cpuFamily {
return false
}
if other.model != c.model {
return false
}
if len(other.bugs) != len(c.bugs) {
return false
}
for bug := range c.bugs {
if _, ok := other.bugs[bug]; !ok {
return false
}
}
return true
}
// parseProcessor grabs the processor field from /proc/cpuinfo output.
func parseProcessor(data string) (int64, error) {
return parseIntegerResult(data, processorKey)
}
// parseVendorID grabs the vendor_id field from /proc/cpuinfo output.
func parseVendorID(data string) (string, error) {
return parseRegex(data, vendorIDKey, `[\w\d]+`)
}
// parseCPUFamily grabs the cpu family field from /proc/cpuinfo output.
func parseCPUFamily(data string) (int64, error) {
return parseIntegerResult(data, cpuFamilyKey)
}
// parseModel grabs the model field from /proc/cpuinfo output.
func parseModel(data string) (int64, error) {
return parseIntegerResult(data, modelKey)
}
// parseCoreID parses the core id field.
func parseCoreID(data string) (int64, error) {
return parseIntegerResult(data, coreIDKey)
}
// parseBugs grabs the bugs field from /proc/cpuinfo output.
func parseBugs(data string) (map[string]struct{}, error) {
result, err := parseRegex(data, bugsKey, `[\d\w\s]*`)
if err != nil {
return nil, err
}
bugs := strings.Split(result, " ")
ret := make(map[string]struct{}, len(bugs))
for _, bug := range bugs {
ret[bug] = struct{}{}
}
return ret, nil
}
// parseIntegerResult parses fields expecting an integer.
func parseIntegerResult(data, key string) (int64, error) {
result, err := parseRegex(data, key, `\d+`)
if err != nil {
return 0, err
}
return strconv.ParseInt(result, 0, 64)
}
// buildRegex builds a regex for parsing each CPU field.
func buildRegex(key, match string) *regexp.Regexp {
reg := fmt.Sprintf(`(?m)^%s\s*:\s*(.*)$`, key)
return regexp.MustCompile(reg)
}
// parseRegex parses data with key inserted into a standard regex template.
func parseRegex(data, key, match string) (string, error) {
r := buildRegex(key, match)
matches := r.FindStringSubmatch(data)
if len(matches) < 2 {
return "", fmt.Errorf("failed to match key %s: %s", key, data)
}
return matches[1], nil
}
+368
View File
@@ -0,0 +1,368 @@
// Copyright 2021 The gVisor 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 mitigate
import (
"io/ioutil"
"strings"
"testing"
)
// CPU info for a Intel CascadeLake processor. Both Skylake and CascadeLake have
// the same family/model numbers, but with different bugs (e.g. skylake has
// cpu_meltdown).
var cascadeLake = &cpu{
vendorID: "GenuineIntel",
cpuFamily: 6,
model: 85,
bugs: map[string]struct{}{
"spectre_v1": struct{}{},
"spectre_v2": struct{}{},
"spec_store_bypass": struct{}{},
mds: struct{}{},
swapgs: struct{}{},
taa: struct{}{},
},
}
// TestGetCPU tests basic parsing of single CPU strings from reading
// /proc/cpuinfo.
func TestGetCPU(t *testing.T) {
data := `processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 85
core id : 0
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs taa itlb_multihit
`
want := cpu{
processorNumber: 0,
vendorID: "GenuineIntel",
cpuFamily: 6,
model: 85,
coreID: 0,
bugs: map[string]struct{}{
"cpu_meltdown": struct{}{},
"spectre_v1": struct{}{},
"spectre_v2": struct{}{},
"spec_store_bypass": struct{}{},
"l1tf": struct{}{},
"mds": struct{}{},
"swapgs": struct{}{},
"taa": struct{}{},
"itlb_multihit": struct{}{},
},
}
got, err := getCPU(data)
if err != nil {
t.Fatalf("getCpu failed with error: %v", err)
}
if !want.similarTo(got) {
t.Fatalf("Failed cpus not similar: got: %+v, want: %+v", got, want)
}
if !got.isVulnerable() {
t.Fatalf("Failed: cpu should be vulnerable.")
}
}
func TestInvalid(t *testing.T) {
result, err := getCPUSet(`something not a processor`)
if err == nil {
t.Fatalf("getCPU set didn't return an error: %+v", result)
}
if !strings.Contains(err.Error(), "no cpus") {
t.Fatalf("Incorrect error returned: %v", err)
}
}
// TestCPUSet tests getting the right number of CPUs from
// parsing full output of /proc/cpuinfo.
func TestCPUSet(t *testing.T) {
data := `processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 63
model name : Intel(R) Xeon(R) CPU @ 2.30GHz
stepping : 0
microcode : 0x1
cpu MHz : 2299.998
cache size : 46080 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 1
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs
bogomips : 4599.99
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:
processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 63
model name : Intel(R) Xeon(R) CPU @ 2.30GHz
stepping : 0
microcode : 0x1
cpu MHz : 2299.998
cache size : 46080 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 1
apicid : 1
initial apicid : 1
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs
bogomips : 4599.99
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:
`
cpuSet, err := getCPUSet(data)
if err != nil {
t.Fatalf("getCPUSet failed: %v", err)
}
wantCPULen := 2
if len(cpuSet) != wantCPULen {
t.Fatalf("Num CPU mismatch: want: %d, got: %d", wantCPULen, len(cpuSet))
}
wantCPU := cpu{
vendorID: "GenuineIntel",
cpuFamily: 6,
model: 63,
bugs: map[string]struct{}{
"cpu_meltdown": struct{}{},
"spectre_v1": struct{}{},
"spectre_v2": struct{}{},
"spec_store_bypass": struct{}{},
"l1tf": struct{}{},
"mds": struct{}{},
"swapgs": struct{}{},
},
}
for _, c := range cpuSet {
if !wantCPU.similarTo(c) {
t.Fatalf("Failed cpus not equal: got: %+v, want: %+v", c, wantCPU)
}
}
}
// TestReadFile is a smoke test for parsing methods.
func TestReadFile(t *testing.T) {
data, err := ioutil.ReadFile("/proc/cpuinfo")
if err != nil {
t.Fatalf("Failed to read cpuinfo: %v", err)
}
set, err := getCPUSet(string(data))
if err != nil {
t.Fatalf("Failed to parse CPU data %v\n%s", err, data)
}
if len(set) < 1 {
t.Fatalf("Failed to parse any CPUs: %d", len(set))
}
for _, c := range set {
t.Logf("CPU: %+v: %t", c, c.isVulnerable())
}
}
// TestVulnerable tests if the isVulnerable method is correct
// among known CPUs in GCP.
func TestVulnerable(t *testing.T) {
const haswell = `processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 63
model name : Intel(R) Xeon(R) CPU @ 2.30GHz
stepping : 0
microcode : 0x1
cpu MHz : 2299.998
cache size : 46080 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 2
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt arat md_clear arch_capabilities
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs
bogomips : 4599.99
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:`
const skylake = `processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 85
model name : Intel(R) Xeon(R) CPU @ 2.00GHz
stepping : 3
microcode : 0x1
cpu MHz : 2000.180
cache size : 39424 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 1
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves arat md_clear arch_capabilities
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs taa
bogomips : 4000.36
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:`
const cascade = `processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 85
model name : Intel(R) Xeon(R) CPU
stepping : 7
microcode : 0x1
cpu MHz : 2800.198
cache size : 33792 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 1
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2
ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmu
lqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowpr
efetch invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid r
tm mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves a
rat avx512_vnni md_clear arch_capabilities
bugs : spectre_v1 spectre_v2 spec_store_bypass mds swapgs taa
bogomips : 5600.39
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:`
const amd = `processor : 0
vendor_id : AuthenticAMD
cpu family : 23
model : 49
model name : AMD EPYC 7B12
stepping : 0
microcode : 0x1000065
cpu MHz : 2250.000
cache size : 512 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 1
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid extd_apicid tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw topoext ssbd ibrs ibpb stibp vmmcall fsgsbase tsc_adjust bmi1 avx2 smep bmi2 rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 clzero xsaveerptr arat npt nrip_save umip rdpid
bugs : sysret_ss_attrs spectre_v1 spectre_v2 spec_store_bypass
bogomips : 4500.00
TLB size : 3072 4K pages
clflush size : 64
cache_alignment : 64
address sizes : 48 bits physical, 48 bits virtual
power management:`
for _, tc := range []struct {
name string
cpuString string
vulnerable bool
}{
{
name: "haswell",
cpuString: haswell,
vulnerable: true,
}, {
name: "skylake",
cpuString: skylake,
vulnerable: true,
}, {
name: "cascadeLake",
cpuString: cascade,
vulnerable: false,
}, {
name: "amd",
cpuString: amd,
vulnerable: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
set, err := getCPUSet(tc.cpuString)
if err != nil {
t.Fatalf("Failed to getCPUSet:%v\n %s", err, tc.cpuString)
}
if len(set) < 1 {
t.Fatalf("Returned empty cpu set: %v", set)
}
for _, c := range set {
got := func() bool {
if cascadeLake.similarTo(c) {
return false
}
return c.isVulnerable()
}()
if got != tc.vulnerable {
t.Fatalf("Mismatch vulnerable for cpu %+s: got %t want: %t", tc.name, tc.vulnerable, got)
}
}
})
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2021 The gVisor 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 mitigate provides libraries for the mitigate command. The
// mitigate command mitigates side channel attacks such as MDS. Mitigate
// shuts down CPUs via /sys/devices/system/cpu/cpu{N}/online. In addition,
// the mitigate also handles computing available CPU in kubernetes kube_config
// files.
package mitigate