summaryrefslogtreecommitdiffhomepage
path: root/runsc
diff options
context:
space:
mode:
Diffstat (limited to 'runsc')
-rw-r--r--runsc/container/container_test.go59
-rw-r--r--runsc/container/multi_container_test.go24
-rw-r--r--runsc/fsgofer/BUILD1
-rw-r--r--runsc/fsgofer/fsgofer.go80
-rw-r--r--runsc/fsgofer/fsgofer_amd64_unsafe.go3
-rw-r--r--runsc/fsgofer/fsgofer_arm64_unsafe.go3
-rw-r--r--runsc/fsgofer/fsgofer_test.go94
-rw-r--r--runsc/mitigate/BUILD20
-rw-r--r--runsc/mitigate/cpu.go235
-rw-r--r--runsc/mitigate/cpu_test.go368
-rw-r--r--runsc/mitigate/mitigate.go20
11 files changed, 821 insertions, 86 deletions
diff --git a/runsc/container/container_test.go b/runsc/container/container_test.go
index a92ae046d..3bbf86534 100644
--- a/runsc/container/container_test.go
+++ b/runsc/container/container_test.go
@@ -52,7 +52,7 @@ func waitForProcessList(cont *Container, want []*control.Process) error {
cb := func() error {
got, err := cont.Processes()
if err != nil {
- err = fmt.Errorf("error getting process data from container: %v", err)
+ err = fmt.Errorf("error getting process data from container: %w", err)
return &backoff.PermanentError{Err: err}
}
if !procListsEqual(got, want) {
@@ -64,11 +64,30 @@ func waitForProcessList(cont *Container, want []*control.Process) error {
return testutil.Poll(cb, 30*time.Second)
}
+// waitForProcess waits for the given process to show up in the container.
+func waitForProcess(cont *Container, want *control.Process) error {
+ cb := func() error {
+ gots, err := cont.Processes()
+ if err != nil {
+ err = fmt.Errorf("error getting process data from container: %w", err)
+ return &backoff.PermanentError{Err: err}
+ }
+ for _, got := range gots {
+ if procEqual(got, want) {
+ return nil
+ }
+ }
+ return fmt.Errorf("container got process list: %s, want: %+v", procListToString(gots), want)
+ }
+ // Gives plenty of time as tests can run slow under --race.
+ return testutil.Poll(cb, 30*time.Second)
+}
+
func waitForProcessCount(cont *Container, want int) error {
cb := func() error {
pss, err := cont.Processes()
if err != nil {
- err = fmt.Errorf("error getting process data from container: %v", err)
+ err = fmt.Errorf("error getting process data from container: %w", err)
return &backoff.PermanentError{Err: err}
}
if got := len(pss); got != want {
@@ -101,28 +120,32 @@ func procListsEqual(gots, wants []*control.Process) bool {
return false
}
for i := range gots {
- got := gots[i]
- want := wants[i]
-
- if want.UID != math.MaxUint32 && want.UID != got.UID {
- return false
- }
- if want.PID != -1 && want.PID != got.PID {
- return false
- }
- if want.PPID != -1 && want.PPID != got.PPID {
- return false
- }
- if len(want.TTY) != 0 && want.TTY != got.TTY {
- return false
- }
- if len(want.Cmd) != 0 && want.Cmd != got.Cmd {
+ if !procEqual(gots[i], wants[i]) {
return false
}
}
return true
}
+func procEqual(got, want *control.Process) bool {
+ if want.UID != math.MaxUint32 && want.UID != got.UID {
+ return false
+ }
+ if want.PID != -1 && want.PID != got.PID {
+ return false
+ }
+ if want.PPID != -1 && want.PPID != got.PPID {
+ return false
+ }
+ if len(want.TTY) != 0 && want.TTY != got.TTY {
+ return false
+ }
+ if len(want.Cmd) != 0 && want.Cmd != got.Cmd {
+ return false
+ }
+ return true
+}
+
type processBuilder struct {
process control.Process
}
diff --git a/runsc/container/multi_container_test.go b/runsc/container/multi_container_test.go
index 044eec6fe..bc802e075 100644
--- a/runsc/container/multi_container_test.go
+++ b/runsc/container/multi_container_test.go
@@ -1708,12 +1708,9 @@ func TestMultiContainerHomeEnvDir(t *testing.T) {
t.Errorf("wait on child container: %v", err)
}
- // Wait for the root container to run.
- expectedPL := []*control.Process{
- newProcessBuilder().Cmd("sh").Process(),
- newProcessBuilder().Cmd("sleep").Process(),
- }
- if err := waitForProcessList(containers[0], expectedPL); err != nil {
+ // Wait until after `env` has executed.
+ expectedProc := newProcessBuilder().Cmd("sleep").Process()
+ if err := waitForProcess(containers[0], expectedProc); err != nil {
t.Errorf("failed to wait for sleep to start: %v", err)
}
@@ -1831,7 +1828,7 @@ func TestDuplicateEnvVariable(t *testing.T) {
cmd1 := fmt.Sprintf("env > %q; sleep 1000", files[0].Name())
cmd2 := fmt.Sprintf("env > %q", files[1].Name())
cmdExec := fmt.Sprintf("env > %q", files[2].Name())
- testSpecs, ids := createSpecs([]string{"/bin/bash", "-c", cmd1}, []string{"/bin/bash", "-c", cmd2})
+ testSpecs, ids := createSpecs([]string{"/bin/sh", "-c", cmd1}, []string{"/bin/sh", "-c", cmd2})
testSpecs[0].Process.Env = append(testSpecs[0].Process.Env, "VAR=foo", "VAR=bar")
testSpecs[1].Process.Env = append(testSpecs[1].Process.Env, "VAR=foo", "VAR=bar")
@@ -1841,12 +1838,9 @@ func TestDuplicateEnvVariable(t *testing.T) {
}
defer cleanup()
- // Wait for the `env` from the root container to finish.
- expectedPL := []*control.Process{
- newProcessBuilder().Cmd("bash").Process(),
- newProcessBuilder().Cmd("sleep").Process(),
- }
- if err := waitForProcessList(containers[0], expectedPL); err != nil {
+ // Wait until after `env` has executed.
+ expectedProc := newProcessBuilder().Cmd("sleep").Process()
+ if err := waitForProcess(containers[0], expectedProc); err != nil {
t.Errorf("failed to wait for sleep to start: %v", err)
}
if ws, err := containers[1].Wait(); err != nil {
@@ -1856,8 +1850,8 @@ func TestDuplicateEnvVariable(t *testing.T) {
}
execArgs := &control.ExecArgs{
- Filename: "/bin/bash",
- Argv: []string{"/bin/bash", "-c", cmdExec},
+ Filename: "/bin/sh",
+ Argv: []string{"/bin/sh", "-c", cmdExec},
Envv: []string{"VAR=foo", "VAR=bar"},
}
if ws, err := containers[0].executeSync(execArgs); err != nil || ws.ExitStatus() != 0 {
diff --git a/runsc/fsgofer/BUILD b/runsc/fsgofer/BUILD
index c56e1d4d0..3280b74fe 100644
--- a/runsc/fsgofer/BUILD
+++ b/runsc/fsgofer/BUILD
@@ -12,7 +12,6 @@ go_library(
],
visibility = ["//runsc:__subpackages__"],
deps = [
- "//pkg/abi/linux",
"//pkg/cleanup",
"//pkg/fd",
"//pkg/log",
diff --git a/runsc/fsgofer/fsgofer.go b/runsc/fsgofer/fsgofer.go
index c3bba0973..cfa3796b1 100644
--- a/runsc/fsgofer/fsgofer.go
+++ b/runsc/fsgofer/fsgofer.go
@@ -31,7 +31,6 @@ import (
"strconv"
"golang.org/x/sys/unix"
- "gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/log"
@@ -49,13 +48,6 @@ const (
allowedOpenFlags = unix.O_TRUNC
)
-var (
- // Remember the process uid/gid to skip chown calls when file owner/group
- // doesn't need to be changed.
- processUID = p9.UID(os.Getuid())
- processGID = p9.GID(os.Getgid())
-)
-
// join is equivalent to path.Join() but skips path.Clean() which is expensive.
func join(parent, child string) string {
if child == "." || child == ".." {
@@ -374,7 +366,24 @@ func fstat(fd int) (unix.Stat_t, error) {
}
func fchown(fd int, uid p9.UID, gid p9.GID) error {
- return unix.Fchownat(fd, "", int(uid), int(gid), linux.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW)
+ return unix.Fchownat(fd, "", int(uid), int(gid), unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW)
+}
+
+func setOwnerIfNeeded(fd int, uid p9.UID, gid p9.GID) (unix.Stat_t, error) {
+ stat, err := fstat(fd)
+ if err != nil {
+ return unix.Stat_t{}, err
+ }
+
+ // Change ownership if not set accordinly.
+ if uint32(uid) != stat.Uid || uint32(gid) != stat.Gid {
+ if err := fchown(fd, uid, gid); err != nil {
+ return unix.Stat_t{}, err
+ }
+ stat.Uid = uint32(uid)
+ stat.Gid = uint32(gid)
+ }
+ return stat, nil
}
// Open implements p9.File.
@@ -457,12 +466,7 @@ func (l *localFile) Create(name string, p9Flags p9.OpenFlags, perm p9.FileMode,
})
defer cu.Clean()
- if uid != processUID || gid != processGID {
- if err := fchown(child.FD(), uid, gid); err != nil {
- return nil, nil, p9.QID{}, 0, extractErrno(err)
- }
- }
- stat, err := fstat(child.FD())
+ stat, err := setOwnerIfNeeded(child.FD(), uid, gid)
if err != nil {
return nil, nil, p9.QID{}, 0, extractErrno(err)
}
@@ -505,12 +509,7 @@ func (l *localFile) Mkdir(name string, perm p9.FileMode, uid p9.UID, gid p9.GID)
}
defer f.Close()
- if uid != processUID || gid != processGID {
- if err := fchown(f.FD(), uid, gid); err != nil {
- return p9.QID{}, extractErrno(err)
- }
- }
- stat, err := fstat(f.FD())
+ stat, err := setOwnerIfNeeded(f.FD(), uid, gid)
if err != nil {
return p9.QID{}, extractErrno(err)
}
@@ -734,15 +733,15 @@ func (l *localFile) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error {
if valid.ATime || valid.MTime {
utimes := [2]unix.Timespec{
- {Sec: 0, Nsec: linux.UTIME_OMIT},
- {Sec: 0, Nsec: linux.UTIME_OMIT},
+ {Sec: 0, Nsec: unix.UTIME_OMIT},
+ {Sec: 0, Nsec: unix.UTIME_OMIT},
}
if valid.ATime {
if valid.ATimeNotSystemTime {
utimes[0].Sec = int64(attr.ATimeSeconds)
utimes[0].Nsec = int64(attr.ATimeNanoSeconds)
} else {
- utimes[0].Nsec = linux.UTIME_NOW
+ utimes[0].Nsec = unix.UTIME_NOW
}
}
if valid.MTime {
@@ -750,7 +749,7 @@ func (l *localFile) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error {
utimes[1].Sec = int64(attr.MTimeSeconds)
utimes[1].Nsec = int64(attr.MTimeNanoSeconds)
} else {
- utimes[1].Nsec = linux.UTIME_NOW
+ utimes[1].Nsec = unix.UTIME_NOW
}
}
@@ -764,7 +763,7 @@ func (l *localFile) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error {
}
defer unix.Close(parent)
- if tErr := utimensat(parent, path.Base(l.hostPath), utimes, linux.AT_SYMLINK_NOFOLLOW); tErr != nil {
+ if tErr := utimensat(parent, path.Base(l.hostPath), utimes, unix.AT_SYMLINK_NOFOLLOW); tErr != nil {
log.Debugf("SetAttr utimens failed %q, err: %v", l.hostPath, tErr)
err = extractErrno(tErr)
}
@@ -779,15 +778,15 @@ func (l *localFile) SetAttr(valid p9.SetAttrMask, attr p9.SetAttr) error {
}
if valid.UID || valid.GID {
- uid := -1
+ uid := p9.NoUID
if valid.UID {
- uid = int(attr.UID)
+ uid = attr.UID
}
- gid := -1
+ gid := p9.NoGID
if valid.GID {
- gid = int(attr.GID)
+ gid = attr.GID
}
- if oErr := unix.Fchownat(f.FD(), "", uid, gid, linux.AT_EMPTY_PATH|linux.AT_SYMLINK_NOFOLLOW); oErr != nil {
+ if oErr := fchown(f.FD(), uid, gid); oErr != nil {
log.Debugf("SetAttr fchownat failed %q, err: %v", l.hostPath, oErr)
err = extractErrno(oErr)
}
@@ -900,12 +899,7 @@ func (l *localFile) Symlink(target, newName string, uid p9.UID, gid p9.GID) (p9.
}
defer f.Close()
- if uid != processUID || gid != processGID {
- if err := fchown(f.FD(), uid, gid); err != nil {
- return p9.QID{}, extractErrno(err)
- }
- }
- stat, err := fstat(f.FD())
+ stat, err := setOwnerIfNeeded(f.FD(), uid, gid)
if err != nil {
return p9.QID{}, extractErrno(err)
}
@@ -921,7 +915,7 @@ func (l *localFile) Link(target p9.File, newName string) error {
}
targetFile := target.(*localFile)
- if err := unix.Linkat(targetFile.file.FD(), "", l.file.FD(), newName, linux.AT_EMPTY_PATH); err != nil {
+ if err := unix.Linkat(targetFile.file.FD(), "", l.file.FD(), newName, unix.AT_EMPTY_PATH); err != nil {
return extractErrno(err)
}
return nil
@@ -959,12 +953,7 @@ func (l *localFile) Mknod(name string, mode p9.FileMode, _ uint32, _ uint32, uid
}
defer child.Close()
- if uid != processUID || gid != processGID {
- if err := fchown(child.FD(), uid, gid); err != nil {
- return p9.QID{}, extractErrno(err)
- }
- }
- stat, err := fstat(child.FD())
+ stat, err := setOwnerIfNeeded(child.FD(), uid, gid)
if err != nil {
return p9.QID{}, extractErrno(err)
}
@@ -1113,7 +1102,8 @@ func (l *localFile) Connect(flags p9.ConnectFlags) (*fd.FD, error) {
// mappings, the app path may have fit in the sockaddr, but we can't
// fit f.path in our sockaddr. We'd need to redirect through a shorter
// path in order to actually connect to this socket.
- if len(l.hostPath) > linux.UnixPathMax {
+ const UNIX_PATH_MAX = 108 // defined in afunix.h
+ if len(l.hostPath) > UNIX_PATH_MAX {
return nil, unix.ECONNREFUSED
}
diff --git a/runsc/fsgofer/fsgofer_amd64_unsafe.go b/runsc/fsgofer/fsgofer_amd64_unsafe.go
index c46958185..29ebf8500 100644
--- a/runsc/fsgofer/fsgofer_amd64_unsafe.go
+++ b/runsc/fsgofer/fsgofer_amd64_unsafe.go
@@ -20,7 +20,6 @@ import (
"unsafe"
"golang.org/x/sys/unix"
- "gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/syserr"
)
@@ -39,7 +38,7 @@ func statAt(dirFd int, name string) (unix.Stat_t, error) {
uintptr(dirFd),
uintptr(namePtr),
uintptr(statPtr),
- linux.AT_SYMLINK_NOFOLLOW,
+ unix.AT_SYMLINK_NOFOLLOW,
0,
0); errno != 0 {
diff --git a/runsc/fsgofer/fsgofer_arm64_unsafe.go b/runsc/fsgofer/fsgofer_arm64_unsafe.go
index 491460718..9fd5d0871 100644
--- a/runsc/fsgofer/fsgofer_arm64_unsafe.go
+++ b/runsc/fsgofer/fsgofer_arm64_unsafe.go
@@ -20,7 +20,6 @@ import (
"unsafe"
"golang.org/x/sys/unix"
- "gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/syserr"
)
@@ -39,7 +38,7 @@ func statAt(dirFd int, name string) (unix.Stat_t, error) {
uintptr(dirFd),
uintptr(namePtr),
uintptr(statPtr),
- linux.AT_SYMLINK_NOFOLLOW,
+ unix.AT_SYMLINK_NOFOLLOW,
0,
0); errno != 0 {
diff --git a/runsc/fsgofer/fsgofer_test.go b/runsc/fsgofer/fsgofer_test.go
index c5daebe5e..99ea9bd32 100644
--- a/runsc/fsgofer/fsgofer_test.go
+++ b/runsc/fsgofer/fsgofer_test.go
@@ -32,6 +32,9 @@ import (
"gvisor.dev/gvisor/runsc/specutils"
)
+// Nodoby is the standard UID/GID for the nobody user/group.
+const nobody = 65534
+
var allOpenFlags = []p9.OpenFlags{p9.ReadOnly, p9.WriteOnly, p9.ReadWrite}
var (
@@ -281,6 +284,92 @@ func TestCreate(t *testing.T) {
})
}
+func checkIDs(f p9.File, uid, gid int) error {
+ _, _, stat, err := f.GetAttr(p9.AttrMask{UID: true, GID: true})
+ if err != nil {
+ return fmt.Errorf("GetAttr() failed, err: %v", err)
+ }
+ if want := p9.UID(uid); stat.UID != want {
+ return fmt.Errorf("Wrong UID, want: %v, got: %v", want, stat.UID)
+ }
+ if want := p9.GID(gid); stat.GID != want {
+ return fmt.Errorf("Wrong GID, want: %v, got: %v", want, stat.GID)
+ }
+ return nil
+}
+
+// TestCreateSetGID checks files/dirs/symlinks are created with the proper
+// owner when the parent directory has setgid set,
+func TestCreateSetGID(t *testing.T) {
+ if !specutils.HasCapabilities(capability.CAP_CHOWN) {
+ t.Skipf("Test requires CAP_CHOWN")
+ }
+
+ runCustom(t, []uint32{unix.S_IFDIR}, rwConfs, func(t *testing.T, s state) {
+ // Change group and set setgid to the parent dir.
+ if err := unix.Chown(s.file.hostPath, os.Getuid(), nobody); err != nil {
+ t.Fatalf("Chown() failed: %v", err)
+ }
+ if err := unix.Chmod(s.file.hostPath, 02777); err != nil {
+ t.Fatalf("Chmod() failed: %v", err)
+ }
+
+ t.Run("create", func(t *testing.T) {
+ _, l, _, _, err := s.file.Create("test", p9.ReadOnly, 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid()))
+ if err != nil {
+ t.Fatalf("WriteAt() failed: %v", err)
+ }
+ defer l.Close()
+ if err := checkIDs(l, os.Getuid(), os.Getgid()); err != nil {
+ t.Error(err)
+ }
+ })
+
+ t.Run("mkdir", func(t *testing.T) {
+ _, err := s.file.Mkdir("test-dir", 0777, p9.UID(os.Getuid()), p9.GID(os.Getgid()))
+ if err != nil {
+ t.Fatalf("WriteAt() failed: %v", err)
+ }
+ _, l, err := s.file.Walk([]string{"test-dir"})
+ if err != nil {
+ t.Fatalf("Walk() failed: %v", err)
+ }
+ defer l.Close()
+ if err := checkIDs(l, os.Getuid(), os.Getgid()); err != nil {
+ t.Error(err)
+ }
+ })
+
+ t.Run("symlink", func(t *testing.T) {
+ if _, err := s.file.Symlink("/some/target", "symlink", p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != nil {
+ t.Fatalf("Symlink() failed: %v", err)
+ }
+ _, l, err := s.file.Walk([]string{"symlink"})
+ if err != nil {
+ t.Fatalf("Walk() failed, err: %v", err)
+ }
+ defer l.Close()
+ if err := checkIDs(l, os.Getuid(), os.Getgid()); err != nil {
+ t.Error(err)
+ }
+ })
+
+ t.Run("mknod", func(t *testing.T) {
+ if _, err := s.file.Mknod("nod", p9.ModeRegular|0777, 0, 0, p9.UID(os.Getuid()), p9.GID(os.Getgid())); err != nil {
+ t.Fatalf("Mknod() failed: %v", err)
+ }
+ _, l, err := s.file.Walk([]string{"nod"})
+ if err != nil {
+ t.Fatalf("Walk() failed, err: %v", err)
+ }
+ defer l.Close()
+ if err := checkIDs(l, os.Getuid(), os.Getgid()); err != nil {
+ t.Error(err)
+ }
+ })
+ })
+}
+
// TestReadWriteDup tests that a file opened in any mode can be dup'ed and
// reopened in any other mode.
func TestReadWriteDup(t *testing.T) {
@@ -458,7 +547,7 @@ func TestSetAttrTime(t *testing.T) {
}
func TestSetAttrOwner(t *testing.T) {
- if os.Getuid() != 0 {
+ if !specutils.HasCapabilities(capability.CAP_CHOWN) {
t.Skipf("SetAttr(owner) test requires CAP_CHOWN, running as %d", os.Getuid())
}
@@ -477,7 +566,7 @@ func TestSetAttrOwner(t *testing.T) {
}
func TestLink(t *testing.T) {
- if os.Getuid() != 0 {
+ if !specutils.HasCapabilities(capability.CAP_DAC_READ_SEARCH) {
t.Skipf("Link test requires CAP_DAC_READ_SEARCH, running as %d", os.Getuid())
}
runCustom(t, allTypes, rwConfs, func(t *testing.T, s state) {
@@ -995,7 +1084,6 @@ func BenchmarkCreateDiffOwner(b *testing.B) {
files := make([]p9.File, 0, 500)
fds := make([]*fd.FD, 0, 500)
gid := p9.GID(os.Getgid())
- const nobody = 65534
b.ResetTimer()
for i := 0; i < b.N; i++ {
diff --git a/runsc/mitigate/BUILD b/runsc/mitigate/BUILD
new file mode 100644
index 000000000..3b0342d18
--- /dev/null
+++ b/runsc/mitigate/BUILD
@@ -0,0 +1,20 @@
+load("//tools:defs.bzl", "go_library", "go_test")
+
+package(licenses = ["notice"])
+
+go_library(
+ name = "mitigate",
+ srcs = [
+ "cpu.go",
+ "mitigate.go",
+ ],
+ deps = ["@in_gopkg_yaml_v2//:go_default_library"],
+)
+
+go_test(
+ name = "mitigate_test",
+ size = "small",
+ srcs = ["cpu_test.go"],
+ library = ":mitigate",
+ deps = ["@com_github_google_go_cmp//cmp:go_default_library"],
+)
diff --git a/runsc/mitigate/cpu.go b/runsc/mitigate/cpu.go
new file mode 100644
index 000000000..113b98159
--- /dev/null
+++ b/runsc/mitigate/cpu.go
@@ -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
+}
diff --git a/runsc/mitigate/cpu_test.go b/runsc/mitigate/cpu_test.go
new file mode 100644
index 000000000..77b714a02
--- /dev/null
+++ b/runsc/mitigate/cpu_test.go
@@ -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)
+ }
+ }
+ })
+ }
+}
diff --git a/runsc/mitigate/mitigate.go b/runsc/mitigate/mitigate.go
new file mode 100644
index 000000000..51d5449b6
--- /dev/null
+++ b/runsc/mitigate/mitigate.go
@@ -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