summaryrefslogtreecommitdiffhomepage
path: root/runsc
diff options
context:
space:
mode:
authorgVisor bot <gvisor-bot@google.com>2021-02-10 18:54:01 +0000
committergVisor bot <gvisor-bot@google.com>2021-02-10 18:54:01 +0000
commit156aa40e684fdf15c1e7d19212dedc01d01aa82d (patch)
tree171243932e194f68d23f663afe79d43568151032 /runsc
parenta40373b6360303daaa7cd266b2e8c42a26425508 (diff)
parent1ac58cc23e7c069ff59ddea333a89c5c5972a555 (diff)
Merge release-20210201.0-76-g1ac58cc23 (automated)
Diffstat (limited to 'runsc')
-rw-r--r--runsc/cli/main.go1
-rw-r--r--runsc/cmd/mitigate.go72
-rw-r--r--runsc/mitigate/cpu.go377
-rw-r--r--runsc/mitigate/mitigate.go78
-rw-r--r--runsc/mitigate/mitigate_conf.go37
-rw-r--r--runsc/mitigate/mitigate_state_autogen.go3
6 files changed, 568 insertions, 0 deletions
diff --git a/runsc/cli/main.go b/runsc/cli/main.go
index 6c3bf4d21..bf6928941 100644
--- a/runsc/cli/main.go
+++ b/runsc/cli/main.go
@@ -85,6 +85,7 @@ func Main(version string) {
subcommands.Register(new(cmd.Start), "")
subcommands.Register(new(cmd.Symbolize), "")
subcommands.Register(new(cmd.Wait), "")
+ subcommands.Register(new(cmd.Mitigate), "")
// Register internal commands with the internal group name. This causes
// them to be sorted below the user-facing commands with empty group.
diff --git a/runsc/cmd/mitigate.go b/runsc/cmd/mitigate.go
new file mode 100644
index 000000000..9052f091d
--- /dev/null
+++ b/runsc/cmd/mitigate.go
@@ -0,0 +1,72 @@
+// 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 cmd
+
+import (
+ "context"
+ "io/ioutil"
+
+ "github.com/google/subcommands"
+ "gvisor.dev/gvisor/pkg/log"
+ "gvisor.dev/gvisor/runsc/flag"
+ "gvisor.dev/gvisor/runsc/mitigate"
+)
+
+// Mitigate implements subcommands.Command for the "mitigate" command.
+type Mitigate struct {
+ mitigate mitigate.Mitigate
+}
+
+// Name implements subcommands.command.name.
+func (*Mitigate) Name() string {
+ return "mitigate"
+}
+
+// Synopsis implements subcommands.Command.Synopsis.
+func (*Mitigate) Synopsis() string {
+ return "mitigate mitigates the underlying system against side channel attacks"
+}
+
+// Usage implements subcommands.Command.Usage.
+func (m *Mitigate) Usage() string {
+ return m.mitigate.Usage()
+}
+
+// SetFlags implements subcommands.Command.SetFlags.
+func (m *Mitigate) SetFlags(f *flag.FlagSet) {
+ m.mitigate.SetFlags(f)
+}
+
+// Execute implements subcommands.Command.Execute.
+func (m *Mitigate) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
+ if f.NArg() != 0 {
+ f.Usage()
+ return subcommands.ExitUsageError
+ }
+
+ const path = "/proc/cpuinfo"
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ log.Warningf("Failed to read %s: %v", path, err)
+ return subcommands.ExitFailure
+ }
+
+ if err := m.mitigate.Execute(data); err != nil {
+ log.Warningf("Execute failed: %v", err)
+ return subcommands.ExitFailure
+ }
+
+ return subcommands.ExitSuccess
+}
diff --git a/runsc/mitigate/cpu.go b/runsc/mitigate/cpu.go
new file mode 100644
index 000000000..ae4ce9579
--- /dev/null
+++ b/runsc/mitigate/cpu.go
@@ -0,0 +1,377 @@
+// 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"
+ "io/ioutil"
+ "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"
+ physicalIDKey = "physical id"
+ coreIDKey = "core id"
+ bugsKey = "bugs"
+)
+
+const (
+ cpuOnlineTemplate = "/sys/devices/system/cpu/cpu%d/online"
+)
+
+// cpuSet contains a map of all CPUs on the system, mapped
+// by Physical ID and CoreIDs. threads with the same
+// Core and Physical ID are Hyperthread pairs.
+type cpuSet map[cpuID]*threadGroup
+
+// newCPUSet creates a CPUSet from data read from /proc/cpuinfo.
+func newCPUSet(data []byte, vulnerable func(*thread) bool) (cpuSet, error) {
+ processors, err := getThreads(string(data))
+ if err != nil {
+ return nil, err
+ }
+
+ set := make(cpuSet)
+ for _, p := range processors {
+ // Each ID is of the form physicalID:coreID. Hyperthread pairs
+ // have identical physical and core IDs. We need to match
+ // Hyperthread pairs so that we can shutdown all but one per
+ // pair.
+ core, ok := set[p.id]
+ if !ok {
+ core = &threadGroup{}
+ set[p.id] = core
+ }
+ core.isVulnerable = core.isVulnerable || vulnerable(p)
+ core.threads = append(core.threads, p)
+ }
+ return set, nil
+}
+
+// String implements the String method for CPUSet.
+func (c cpuSet) String() string {
+ ret := ""
+ for _, tg := range c {
+ ret += fmt.Sprintf("%s\n", tg)
+ }
+ return ret
+}
+
+// getRemainingList returns the list of threads that will remain active
+// after mitigation.
+func (c cpuSet) getRemainingList() []*thread {
+ threads := make([]*thread, 0, len(c))
+ for _, core := range c {
+ // If we're vulnerable, take only one thread from the pair.
+ if core.isVulnerable {
+ threads = append(threads, core.threads[0])
+ continue
+ }
+ // Otherwise don't shutdown anything.
+ threads = append(threads, core.threads...)
+ }
+ return threads
+}
+
+// getShutdownList returns the list of threads that will be shutdown on
+// mitigation.
+func (c cpuSet) getShutdownList() []*thread {
+ threads := make([]*thread, 0)
+ for _, core := range c {
+ // Only if we're vulnerable do shutdown anything. In this case,
+ // shutdown all but the first entry.
+ if core.isVulnerable && len(core.threads) > 1 {
+ threads = append(threads, core.threads[1:]...)
+ }
+ }
+ return threads
+}
+
+// threadGroup represents Hyperthread pairs on the same physical/core ID.
+type threadGroup struct {
+ threads []*thread
+ isVulnerable bool
+}
+
+// String implements the String method for threadGroup.
+func (c *threadGroup) String() string {
+ ret := fmt.Sprintf("ThreadGroup:\nIsVulnerable: %t\n", c.isVulnerable)
+ for _, processor := range c.threads {
+ ret += fmt.Sprintf("%s\n", processor)
+ }
+ return ret
+}
+
+// getThreads returns threads structs from reading /proc/cpuinfo.
+func getThreads(data string) ([]*thread, 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([]*thread, 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 := newThread(data[start:end])
+ if err != nil {
+ return nil, err
+ }
+ cpus = append(cpus, c)
+ }
+ return cpus, nil
+}
+
+// cpuID for each thread is defined by the physical and
+// core IDs. If equal, two threads are Hyperthread pairs.
+type cpuID struct {
+ physicalID int64
+ coreID int64
+}
+
+// type cpu represents pertinent info about a cpu.
+type thread 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).
+ id cpuID // id for this thread
+ bugs map[string]struct{} // map of vulnerabilities parsed from the 'bugs' field.
+}
+
+// newThread parses a CPU from a single cpu entry from /proc/cpuinfo.
+func newThread(data string) (*thread, 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
+ }
+
+ physicalID, err := parsePhysicalID(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 &thread{
+ processorNumber: processor,
+ vendorID: vendorID,
+ cpuFamily: cpuFamily,
+ model: model,
+ id: cpuID{
+ physicalID: physicalID,
+ coreID: coreID,
+ },
+ bugs: bugs,
+ }, nil
+}
+
+// String implements the String method for thread.
+func (t *thread) String() string {
+ template := `CPU: %d
+CPU ID: %+v
+Vendor: %s
+Family/Model: %d/%d
+Bugs: %s
+`
+ bugs := make([]string, 0)
+ for bug := range t.bugs {
+ bugs = append(bugs, bug)
+ }
+
+ return fmt.Sprintf(template, t.processorNumber, t.id, t.vendorID, t.cpuFamily, t.model, strings.Join(bugs, ","))
+}
+
+// shutdown turns off the CPU by writing 0 to /sys/devices/cpu/cpu{N}/online.
+func (t *thread) shutdown() error {
+ cpuPath := fmt.Sprintf(cpuOnlineTemplate, t.processorNumber)
+ return ioutil.WriteFile(cpuPath, []byte{'0'}, 0644)
+}
+
+// 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 (t *thread) isVulnerable() bool {
+ for _, bug := range vulnerabilities {
+ if _, ok := t.bugs[bug]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+// isActive checks if a CPU is active from /sys/devices/system/cpu/cpu{N}/online
+// If the file does not exist (ioutil returns in error), we assume the CPU is on.
+func (t *thread) isActive() bool {
+ cpuPath := fmt.Sprintf(cpuOnlineTemplate, t.processorNumber)
+ data, err := ioutil.ReadFile(cpuPath)
+ if err != nil {
+ return true
+ }
+ return len(data) > 0 && data[0] != '0'
+}
+
+// similarTo checks family/model/bugs fields for equality of two
+// processors.
+func (t *thread) similarTo(other *thread) bool {
+ if t.vendorID != other.vendorID {
+ return false
+ }
+
+ if other.cpuFamily != t.cpuFamily {
+ return false
+ }
+
+ if other.model != t.model {
+ return false
+ }
+
+ if len(other.bugs) != len(t.bugs) {
+ return false
+ }
+
+ for bug := range t.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)
+}
+
+// parsePhysicalID parses the physical id field.
+func parsePhysicalID(data string) (int64, error) {
+ return parseIntegerResult(data, physicalIDKey)
+}
+
+// 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
+}
diff --git a/runsc/mitigate/mitigate.go b/runsc/mitigate/mitigate.go
new file mode 100644
index 000000000..5be66f5f3
--- /dev/null
+++ b/runsc/mitigate/mitigate.go
@@ -0,0 +1,78 @@
+// 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
+
+import (
+ "fmt"
+
+ "gvisor.dev/gvisor/pkg/log"
+ "gvisor.dev/gvisor/runsc/flag"
+)
+
+// Mitigate handles high level mitigate operations provided to runsc.
+type Mitigate struct {
+ dryRun bool // Run the command without changing the underlying system.
+ other mitigate // Struct holds extra mitigate logic.
+}
+
+// Usage implments Usage for cmd.Mitigate.
+func (m Mitigate) Usage() string {
+ usageString := `mitigate [flags]
+
+This command mitigates an underlying system against side channel attacks.
+The command checks /proc/cpuinfo for cpus having key vulnerablilities (meltdown,
+l1tf, mds, swapgs, taa). If cpus are found to have one of the vulnerabilities,
+all but one cpu is shutdown on each core via
+/sys/devices/system/cpu/cpu{N}/online.
+`
+ return usageString + m.other.usage()
+}
+
+// SetFlags sets flags for the command Mitigate.
+func (m Mitigate) SetFlags(f *flag.FlagSet) {
+ f.BoolVar(&m.dryRun, "dryrun", false, "run the command without changing system")
+ m.other.setFlags(f)
+}
+
+// Execute executes the Mitigate command.
+func (m Mitigate) Execute(data []byte) error {
+ set, err := newCPUSet(data, m.other.vulnerable)
+ if err != nil {
+ return err
+ }
+
+ log.Infof("Mitigate found the following CPUs...")
+ log.Infof("%s", set)
+
+ shutdownList := set.getShutdownList()
+ log.Infof("Shutting down threads on thread pairs.")
+ for _, t := range shutdownList {
+ log.Infof("Shutting down thread: %s", t)
+ if m.dryRun {
+ continue
+ }
+ if err := t.shutdown(); err != nil {
+ return fmt.Errorf("error shutting down thread: %s err: %v", t, err)
+ }
+ }
+ log.Infof("Shutdown successful.")
+ m.other.execute(set, m.dryRun)
+ return nil
+}
diff --git a/runsc/mitigate/mitigate_conf.go b/runsc/mitigate/mitigate_conf.go
new file mode 100644
index 000000000..1e74f5891
--- /dev/null
+++ b/runsc/mitigate/mitigate_conf.go
@@ -0,0 +1,37 @@
+// 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 (
+ "gvisor.dev/gvisor/runsc/flag"
+)
+
+type mitigate struct {
+}
+
+// usage returns the usage string portion for the mitigate.
+func (m mitigate) usage() string { return "" }
+
+// setFlags sets additional flags for the Mitigate command.
+func (m mitigate) setFlags(f *flag.FlagSet) {}
+
+// execute performs additional parts of Execute for Mitigate.
+func (m mitigate) execute(set cpuSet, dryrun bool) error {
+ return nil
+}
+
+func (m mitigate) vulnerable(other *thread) bool {
+ return other.isVulnerable()
+}
diff --git a/runsc/mitigate/mitigate_state_autogen.go b/runsc/mitigate/mitigate_state_autogen.go
new file mode 100644
index 000000000..14bad0cd6
--- /dev/null
+++ b/runsc/mitigate/mitigate_state_autogen.go
@@ -0,0 +1,3 @@
+// automatically generated by stateify.
+
+package mitigate