diff options
-rw-r--r-- | runsc/cli/main.go | 1 | ||||
-rw-r--r-- | runsc/cmd/mitigate.go | 72 | ||||
-rw-r--r-- | runsc/mitigate/cpu.go | 377 | ||||
-rw-r--r-- | runsc/mitigate/mitigate.go | 78 | ||||
-rw-r--r-- | runsc/mitigate/mitigate_conf.go | 37 | ||||
-rw-r--r-- | runsc/mitigate/mitigate_state_autogen.go | 3 |
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 |