diff options
Diffstat (limited to 'runsc')
-rw-r--r-- | runsc/container/container_test.go | 59 | ||||
-rw-r--r-- | runsc/container/multi_container_test.go | 24 | ||||
-rw-r--r-- | runsc/fsgofer/BUILD | 1 | ||||
-rw-r--r-- | runsc/fsgofer/fsgofer.go | 80 | ||||
-rw-r--r-- | runsc/fsgofer/fsgofer_amd64_unsafe.go | 3 | ||||
-rw-r--r-- | runsc/fsgofer/fsgofer_arm64_unsafe.go | 3 | ||||
-rw-r--r-- | runsc/fsgofer/fsgofer_test.go | 94 | ||||
-rw-r--r-- | runsc/mitigate/BUILD | 20 | ||||
-rw-r--r-- | runsc/mitigate/cpu.go | 235 | ||||
-rw-r--r-- | runsc/mitigate/cpu_test.go | 368 | ||||
-rw-r--r-- | runsc/mitigate/mitigate.go | 20 |
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 |