mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
## Summary This patch adds capability and permission checks that the Linux kernel enforces but gVisor currently omits. Each fix was verified against native Linux behavior using `bazel test` on both native and `runsc_ptrace` platforms. ## Changes ### 1. `SO_BINDTODEVICE`: Add `CAP_NET_RAW` check **File:** `pkg/sentry/socket/netstack/netstack.go` **Linux reference:** `net/core/sock.c:sock_setsockopt()` checks `ns_capable(sock_net(sk)->user_ns, CAP_NET_RAW)` **Evidence this is unintended:** gVisor's own test suite asserts `"CAP_NET_RAW is required to use SO_BINDTODEVICE"` (`test/syscalls/linux/socket_bind_to_device.cc:52`), and `SO_RCVBUFFORCE` in the same file already correctly checks `CAP_NET_ADMIN`. ### 2. `mknod(S_IFBLK/S_IFCHR)`: Add `CAP_MKNOD` check **File:** `pkg/sentry/syscalls/linux/sys_file.go` **Linux reference:** `fs/namei.c:vfs_mknod()` checks `capable(CAP_MKNOD)` for block/char device creation **Evidence this is unintended:** `CAP_MKNOD` is defined (`pkg/abi/linux/capability.go:56`), parsed from OCI specs (`runsc/specutils/specutils.go:491`), and has strace formatting — but is never checked anywhere. Zero `HasCapability` calls for it exist in the codebase. ### 3. `sched_setaffinity`: Add UID match / `CAP_SYS_NICE` check **File:** `pkg/sentry/syscalls/linux/sys_thread.go` **Linux reference:** `kernel/sched/core.c:check_same_owner()` requires EUID match or `CAP_SYS_NICE` **Impact:** Without this check, any unprivileged process could modify another process's CPU affinity mask. ### 4. `setpriority`: Add UID match / `CAP_SYS_NICE` check **File:** `pkg/sentry/syscalls/linux/sys_thread.go` **Linux reference:** `kernel/sys.c:set_one_prio()` requires UID match or `CAP_SYS_NICE` **Impact:** Without this check, any unprivileged process could change another process's scheduling priority. ## Testing Tests added in `test/syscalls/linux/capability_checks.cc`, verified on both native Linux and gVisor: ``` bazel test //test/syscalls:capability_checks_test_native → 6/6 passed bazel test //test/syscalls:capability_checks_test_runsc_ptrace → 4 passed, 2 skipped ``` The 2 skipped tests are the mknod positive cases (creating device nodes with `CAP_MKNOD`), which are skipped on gVisor because the sandbox does not permit device node creation regardless of capabilities. | Test | What it verifies | |------|-----------------| | `SoBindToDeviceCapTest.RequiresCapNetRaw` | `EPERM` without `CAP_NET_RAW` | | `MknodCapTest.CharDevRequiresCapMknod` | `EPERM` for `S_IFCHR` without `CAP_MKNOD` (native only) | | `MknodCapTest.BlockDevRequiresCapMknod` | `EPERM` for `S_IFBLK` without `CAP_MKNOD` (native only) | | `MknodCapTest.FifoDoesNotRequireCapMknod` | `S_IFIFO` succeeds without `CAP_MKNOD` | | `SchedSetaffinityCapTest.OtherUidRequiresCapSysNice` | `EPERM` without UID match or `CAP_SYS_NICE` | | `SetpriorityCapTest.OtherUidRequiresCapSysNice` | `EPERM` without UID match or `CAP_SYS_NICE` | COPYBARA_INTEGRATE_REVIEW=https://github.com/google/gvisor/pull/12872 from petrmarinec:fix/missing-capability-checks 70b4e0fa308ba182e7916112e90aed40cdea75f3 PiperOrigin-RevId: 903343461 Co-authored-by: Hanzo Dev <[email protected]>
305 lines
11 KiB
C++
305 lines
11 KiB
C++
// Copyright 2018 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.
|
|
|
|
#include <sched.h>
|
|
#include <sys/syscall.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
|
|
#include "gtest/gtest.h"
|
|
#include "absl/strings/str_split.h"
|
|
#include "test/util/cleanup.h"
|
|
#include "test/util/fs_util.h"
|
|
#include "test/util/linux_capability_util.h"
|
|
#include "test/util/logging.h"
|
|
#include "test/util/posix_error.h"
|
|
#include "test/util/test_util.h"
|
|
#include "test/util/thread_util.h"
|
|
|
|
namespace gvisor {
|
|
namespace testing {
|
|
namespace {
|
|
|
|
// These tests are for both the sched_getaffinity(2) and sched_setaffinity(2)
|
|
// syscalls.
|
|
class AffinityTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
EXPECT_THAT(
|
|
// Needs use the raw syscall to get the actual size.
|
|
cpuset_size_ = syscall(SYS_sched_getaffinity, /*pid=*/0,
|
|
sizeof(cpu_set_t), &mask_),
|
|
SyscallSucceeds());
|
|
// Lots of tests rely on having more than 1 logical processor available.
|
|
EXPECT_GT(CPU_COUNT(&mask_), 1);
|
|
EXPECT_GT(cpuset_size_, 0);
|
|
EXPECT_LE(cpuset_size_, sizeof(cpu_set_t));
|
|
}
|
|
|
|
static PosixError ClearLowestBit(cpu_set_t* mask, size_t cpus) {
|
|
const size_t mask_size = CPU_ALLOC_SIZE(cpus);
|
|
for (size_t n = 0; n < cpus; ++n) {
|
|
if (CPU_ISSET_S(n, mask_size, mask)) {
|
|
CPU_CLR_S(n, mask_size, mask);
|
|
return NoError();
|
|
}
|
|
}
|
|
return PosixError(EINVAL, "No bit to clear, mask is empty");
|
|
}
|
|
|
|
PosixError ClearLowestBit() { return ClearLowestBit(&mask_, CPU_SETSIZE); }
|
|
|
|
// Stores the initial cpu mask for this process.
|
|
cpu_set_t mask_ = {};
|
|
int cpuset_size_ = 0;
|
|
};
|
|
|
|
// sched_getaffinity(2) is implemented.
|
|
TEST_F(AffinityTest, SchedGetAffinityImplemented) {
|
|
EXPECT_THAT(sched_getaffinity(/*pid=*/0, sizeof(cpu_set_t), &mask_),
|
|
SyscallSucceeds());
|
|
}
|
|
|
|
// PID is not found.
|
|
TEST_F(AffinityTest, SchedGetAffinityInvalidPID) {
|
|
// Flaky, but it's tough to avoid a race condition when finding an unused pid
|
|
EXPECT_THAT(sched_getaffinity(/*pid=*/INT_MAX - 1, sizeof(cpu_set_t), &mask_),
|
|
SyscallFailsWithErrno(ESRCH));
|
|
}
|
|
|
|
// PID is not found.
|
|
TEST_F(AffinityTest, SchedSetAffinityInvalidPID) {
|
|
// Flaky, but it's tough to avoid a race condition when finding an unused pid
|
|
EXPECT_THAT(sched_setaffinity(/*pid=*/INT_MAX - 1, sizeof(cpu_set_t), &mask_),
|
|
SyscallFailsWithErrno(ESRCH));
|
|
}
|
|
|
|
TEST_F(AffinityTest, SchedSetAffinityZeroMask) {
|
|
CPU_ZERO(&mask_);
|
|
EXPECT_THAT(sched_setaffinity(/*pid=*/0, sizeof(cpu_set_t), &mask_),
|
|
SyscallFailsWithErrno(EINVAL));
|
|
}
|
|
|
|
// N.B. This test case relies on cpuset_size_ larger than the actual number of
|
|
// of all existing CPUs. Check your machine if the test fails.
|
|
TEST_F(AffinityTest, SchedSetAffinityNonexistentCPUDropped) {
|
|
// sched_setaffinity() is a no-op on platform/KVM
|
|
SKIP_IF(GvisorPlatform() == Platform::kKVM);
|
|
cpu_set_t mask = mask_;
|
|
// Add a nonexistent CPU.
|
|
//
|
|
// The number needs to be larger than the possible number of CPU available,
|
|
// but smaller than the number of the CPU that the kernel claims to support --
|
|
// it's implicitly returned by raw sched_getaffinity syscall.
|
|
CPU_SET(cpuset_size_ * 8 - 1, &mask);
|
|
EXPECT_THAT(
|
|
// Use raw syscall because it will be rejected by the libc wrapper
|
|
// otherwise.
|
|
syscall(SYS_sched_setaffinity, /*pid=*/0, sizeof(cpu_set_t), &mask),
|
|
SyscallSucceeds())
|
|
<< "failed with cpumask : " << CPUSetToString(mask)
|
|
<< ", cpuset_size_ : " << cpuset_size_;
|
|
cpu_set_t newmask;
|
|
EXPECT_THAT(sched_getaffinity(/*pid=*/0, sizeof(cpu_set_t), &newmask),
|
|
SyscallSucceeds());
|
|
EXPECT_TRUE(CPU_EQUAL(&mask_, &newmask))
|
|
<< "got: " << CPUSetToString(newmask)
|
|
<< " != expected: " << CPUSetToString(mask_);
|
|
}
|
|
|
|
TEST_F(AffinityTest, SchedSetAffinityOnlyNonexistentCPUFails) {
|
|
// sched_setaffinity() is a no-op on platform/KVM
|
|
SKIP_IF(GvisorPlatform() == Platform::kKVM);
|
|
// Make an empty cpu set.
|
|
CPU_ZERO(&mask_);
|
|
// Add a nonexistent CPU.
|
|
//
|
|
// The number needs to be larger than the possible number of CPU available,
|
|
// but smaller than the number of the CPU that the kernel claims to support --
|
|
// it's implicitly returned by raw sched_getaffinity syscall.
|
|
int cpu = cpuset_size_ * 8 - 1;
|
|
if (cpu <= NumCPUs()) {
|
|
GTEST_SKIP() << "Skipping test: cpu " << cpu << " exists";
|
|
}
|
|
CPU_SET(cpu, &mask_);
|
|
EXPECT_THAT(
|
|
// Use raw syscall because it will be rejected by the libc wrapper
|
|
// otherwise.
|
|
syscall(SYS_sched_setaffinity, /*pid=*/0, sizeof(cpu_set_t), &mask_),
|
|
SyscallFailsWithErrno(EINVAL));
|
|
}
|
|
|
|
TEST_F(AffinityTest, SchedSetAffinityInvalidSize) {
|
|
EXPECT_GT(cpuset_size_, 0);
|
|
// Not big enough.
|
|
EXPECT_THAT(sched_getaffinity(/*pid=*/0, cpuset_size_ - 1, &mask_),
|
|
SyscallFailsWithErrno(EINVAL));
|
|
// Not a multiple of word size.
|
|
EXPECT_THAT(sched_getaffinity(/*pid=*/0, cpuset_size_ + 1, &mask_),
|
|
SyscallFailsWithErrno(EINVAL));
|
|
}
|
|
|
|
TEST_F(AffinityTest, Sanity) {
|
|
// sched_setaffinity() is a no-op on platform/KVM
|
|
SKIP_IF(GvisorPlatform() == Platform::kKVM);
|
|
ASSERT_NO_ERRNO(ClearLowestBit());
|
|
EXPECT_THAT(sched_setaffinity(/*pid=*/0, sizeof(cpu_set_t), &mask_),
|
|
SyscallSucceeds());
|
|
cpu_set_t newmask;
|
|
EXPECT_THAT(sched_getaffinity(/*pid=*/0, sizeof(cpu_set_t), &newmask),
|
|
SyscallSucceeds());
|
|
EXPECT_TRUE(CPU_EQUAL(&mask_, &newmask))
|
|
<< "got: " << CPUSetToString(newmask)
|
|
<< " != expected: " << CPUSetToString(mask_);
|
|
}
|
|
|
|
TEST_F(AffinityTest, NewThread) {
|
|
// sched_setaffinity() is a no-op on platform/KVM
|
|
SKIP_IF(GvisorPlatform() == Platform::kKVM);
|
|
SKIP_IF(CPU_COUNT(&mask_) < 3);
|
|
ASSERT_NO_ERRNO(ClearLowestBit());
|
|
ASSERT_NO_ERRNO(ClearLowestBit());
|
|
EXPECT_THAT(sched_setaffinity(/*pid=*/0, sizeof(cpu_set_t), &mask_),
|
|
SyscallSucceeds());
|
|
ScopedThread([this]() {
|
|
cpu_set_t child_mask;
|
|
ASSERT_THAT(sched_getaffinity(/*pid=*/0, sizeof(cpu_set_t), &child_mask),
|
|
SyscallSucceeds());
|
|
ASSERT_TRUE(CPU_EQUAL(&child_mask, &mask_))
|
|
<< "child cpu mask: " << CPUSetToString(child_mask)
|
|
<< " != parent cpu mask: " << CPUSetToString(mask_);
|
|
});
|
|
}
|
|
|
|
TEST_F(AffinityTest, ConsistentWithProcCpuInfo) {
|
|
// Count how many cpus are shown in /proc/cpuinfo.
|
|
std::string cpuinfo = ASSERT_NO_ERRNO_AND_VALUE(GetContents("/proc/cpuinfo"));
|
|
int count = 0;
|
|
for (auto const& line : absl::StrSplit(cpuinfo, '\n')) {
|
|
if (absl::StartsWith(line, "processor")) {
|
|
count++;
|
|
}
|
|
}
|
|
EXPECT_GE(count, CPU_COUNT(&mask_));
|
|
}
|
|
|
|
TEST_F(AffinityTest, ConsistentWithProcStat) {
|
|
// Count how many cpus are shown in /proc/stat.
|
|
std::string stat = ASSERT_NO_ERRNO_AND_VALUE(GetContents("/proc/stat"));
|
|
int count = 0;
|
|
for (auto const& line : absl::StrSplit(stat, '\n')) {
|
|
if (absl::StartsWith(line, "cpu") && !absl::StartsWith(line, "cpu ")) {
|
|
count++;
|
|
}
|
|
}
|
|
EXPECT_GE(count, CPU_COUNT(&mask_));
|
|
}
|
|
|
|
TEST_F(AffinityTest, SmallCpuMask) {
|
|
const int num_cpus = NumCPUs();
|
|
const size_t mask_size = CPU_ALLOC_SIZE(num_cpus);
|
|
cpu_set_t* mask = CPU_ALLOC(num_cpus);
|
|
ASSERT_NE(mask, nullptr);
|
|
const auto free_mask = Cleanup([&] { CPU_FREE(mask); });
|
|
|
|
CPU_ZERO_S(mask_size, mask);
|
|
ASSERT_THAT(sched_getaffinity(0, mask_size, mask), SyscallSucceeds());
|
|
}
|
|
|
|
#ifndef __Fuchsia__ // Fuchsia doesn't support caps.
|
|
// Test that sched_setaffinity on another task owned by a different UID
|
|
// requires CAP_SYS_NICE.
|
|
TEST_F(AffinityTest, SetAffinityOtherUidRequiresCapSysNice) {
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_NICE)));
|
|
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
|
|
|
|
int child_ready[2];
|
|
ASSERT_THAT(pipe(child_ready), SyscallSucceeds());
|
|
|
|
pid_t child = fork();
|
|
ASSERT_THAT(child, SyscallSucceeds());
|
|
|
|
if (child == 0) {
|
|
constexpr int kUnprivilegedUid = 12345;
|
|
close(child_ready[0]);
|
|
TEST_PCHECK(
|
|
setresuid(kUnprivilegedUid, kUnprivilegedUid, kUnprivilegedUid) == 0);
|
|
|
|
char ready = 'r';
|
|
write(child_ready[1], &ready, 1);
|
|
close(child_ready[1]);
|
|
_exit(0);
|
|
}
|
|
|
|
close(child_ready[1]);
|
|
char ready;
|
|
ASSERT_THAT(read(child_ready[0], &ready, 1), SyscallSucceedsWithValue(1));
|
|
close(child_ready[0]);
|
|
|
|
EXPECT_THAT(sched_setaffinity(child, sizeof(mask_), &mask_),
|
|
SyscallSucceeds());
|
|
|
|
AutoCapability cap(CAP_SYS_NICE, false);
|
|
EXPECT_THAT(sched_setaffinity(child, sizeof(mask_), &mask_),
|
|
SyscallFailsWithErrno(EPERM));
|
|
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0),
|
|
SyscallSucceedsWithValue(child));
|
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
|
|
<< "status = " << status;
|
|
}
|
|
#endif // __Fuchsia__
|
|
|
|
TEST_F(AffinityTest, LargeCpuMask) {
|
|
// sched_setaffinity() is a no-op on platform/KVM
|
|
SKIP_IF(GvisorPlatform() == Platform::kKVM);
|
|
// Allocate mask bigger than cpu_set_t normally allocates.
|
|
const size_t cpus = CPU_SETSIZE * 8;
|
|
const size_t mask_size = CPU_ALLOC_SIZE(cpus);
|
|
|
|
cpu_set_t* large_mask = CPU_ALLOC(cpus);
|
|
auto free_mask = Cleanup([large_mask] { CPU_FREE(large_mask); });
|
|
CPU_ZERO_S(mask_size, large_mask);
|
|
|
|
// Check that get affinity with large mask works as expected.
|
|
ASSERT_THAT(sched_getaffinity(/*pid=*/0, mask_size, large_mask),
|
|
SyscallSucceeds());
|
|
EXPECT_TRUE(CPU_EQUAL(&mask_, large_mask))
|
|
<< "got: " << CPUSetToString(*large_mask, cpus)
|
|
<< " != expected: " << CPUSetToString(mask_);
|
|
|
|
// Check that set affinity with large mask works as expected.
|
|
ASSERT_NO_ERRNO(ClearLowestBit(large_mask, cpus));
|
|
EXPECT_THAT(sched_setaffinity(/*pid=*/0, mask_size, large_mask),
|
|
SyscallSucceeds());
|
|
|
|
cpu_set_t* new_mask = CPU_ALLOC(cpus);
|
|
auto free_new_mask = Cleanup([new_mask] { CPU_FREE(new_mask); });
|
|
CPU_ZERO_S(mask_size, new_mask);
|
|
EXPECT_THAT(sched_getaffinity(/*pid=*/0, mask_size, new_mask),
|
|
SyscallSucceeds());
|
|
|
|
EXPECT_TRUE(CPU_EQUAL_S(mask_size, large_mask, new_mask))
|
|
<< "got: " << CPUSetToString(*new_mask, cpus)
|
|
<< " != expected: " << CPUSetToString(*large_mask, cpus);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace testing
|
|
} // namespace gvisor
|