seccomp: Check that programs that are too large are rejected.

This is good for three reasons:

- Linux also rejects programs larger than 4,096 (`BPF_MAXINSNS`)
  instructions.
- This avoids making an allocation of unbounded length on the next line.
- This avoids the pitfall where gVisor may spend CPU doing BPF
  bytecode optimizations, which can be worse than O(n), on a program
  which is unboundedly large. By checking the size of the BPF program
  before applying bytecode optimizations, this DoS vector is nullified.

PiperOrigin-RevId: 604523997
This commit is contained in:
Etienne Perot
2024-02-05 21:09:42 -08:00
committed by gVisor bot
parent 59b41a150e
commit 9defeeaf09
2 changed files with 73 additions and 40 deletions
+6
View File
@@ -59,6 +59,12 @@ func seccomp(t *kernel.Task, mode, flags uint64, addr hostarch.Addr) error {
if _, err := fprog.CopyIn(t, addr); err != nil {
return err
}
if fprog.Len == 0 || fprog.Len > bpf.MaxInstructions {
// If the filter is already over the maximum number of instructions,
// do not go further and attempt to optimize the bytecode to make it
// smaller.
return linuxerr.EINVAL
}
filter := make([]linux.BPFInstruction, int(fprog.Len))
if _, err := linux.CopyBPFInstructionSliceIn(t, hostarch.Addr(fprog.Filter), filter); err != nil {
return err
+67 -40
View File
@@ -71,27 +71,27 @@ void ApplySeccompFilter(uint32_t sysno, uint32_t filtered_result,
MaybeSave();
struct sock_filter filter[] = {
// A = seccomp_data.arch
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 4),
// A = seccomp_data.arch
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 4),
#if defined(__x86_64__)
// if (A != AUDIT_ARCH_X86_64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 4),
// if (A != AUDIT_ARCH_X86_64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 4),
#elif defined(__aarch64__)
// if (A != AUDIT_ARCH_AARCH64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 0, 4),
// if (A != AUDIT_ARCH_AARCH64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 0, 4),
#else
#error "Unknown architecture"
#endif
// A = seccomp_data.nr
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0),
// if (A != sysno) goto allow
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, sysno, 0, 1),
// return filtered_result
BPF_STMT(BPF_RET | BPF_K, filtered_result),
// allow: return SECCOMP_RET_ALLOW
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// kill: return SECCOMP_RET_KILL
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
// A = seccomp_data.nr
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0),
// if (A != sysno) goto allow
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, sysno, 0, 1),
// return filtered_result
BPF_STMT(BPF_RET | BPF_K, filtered_result),
// allow: return SECCOMP_RET_ALLOW
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// kill: return SECCOMP_RET_KILL
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};
struct sock_fprog prog;
prog.len = ABSL_ARRAYSIZE(filter);
@@ -119,27 +119,27 @@ void ApplyUncacheableFilter(uint32_t sysno) {
MaybeSave();
struct sock_filter filter[] = {
// A = seccomp_data.arch
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 4),
// A = seccomp_data.arch
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 4),
#if defined(__x86_64__)
// if (A != AUDIT_ARCH_X86_64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 4),
// if (A != AUDIT_ARCH_X86_64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 4),
#elif defined(__aarch64__)
// if (A != AUDIT_ARCH_AARCH64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 0, 4),
// if (A != AUDIT_ARCH_AARCH64) goto kill
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 0, 4),
#else
#error "Unknown architecture"
#endif
// A = seccomp_data.nr
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0),
// if (A != sysno) goto end
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, sysno, 0, 1),
// A = seccomp_data.args[0]
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 16),
// end: return SECCOMP_RET_ALLOW
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// kill: return SECCOMP_RET_KILL
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
// A = seccomp_data.nr
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0),
// if (A != sysno) goto end
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, sysno, 0, 1),
// A = seccomp_data.args[0]
BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 16),
// end: return SECCOMP_RET_ALLOW
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// kill: return SECCOMP_RET_KILL
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
};
struct sock_fprog prog;
prog.len = ABSL_ARRAYSIZE(filter);
@@ -169,8 +169,7 @@ TEST(SeccompTest, RetKillCausesDeathBySIGSYS) {
pid_t const pid = fork();
if (pid == 0) {
// Register a signal handler for SIGSYS that we don't expect to be invoked.
RegisterSignalHandler(
SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
RegisterSignalHandler(SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
ApplySeccompFilter(kFilteredSyscall, SECCOMP_RET_KILL);
syscall(kFilteredSyscall);
TEST_CHECK_MSG(false, "Survived invocation of test syscall");
@@ -189,8 +188,7 @@ TEST(SeccompTest, RetKillOnlyKillsOneThread) {
pid_t const pid = fork();
if (pid == 0) {
// Register a signal handler for SIGSYS that we don't expect to be invoked.
RegisterSignalHandler(
SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
RegisterSignalHandler(SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
ApplySeccompFilter(kFilteredSyscall, SECCOMP_RET_KILL);
// Pass CLONE_VFORK to block the original thread in the child process until
// the clone thread exits with SIGSYS.
@@ -296,8 +294,7 @@ TEST(SeccompTest, RetKillVsyscallCausesDeathBySIGSYS) {
pid_t const pid = fork();
if (pid == 0) {
// Register a signal handler for SIGSYS that we don't expect to be invoked.
RegisterSignalHandler(
SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
RegisterSignalHandler(SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
ApplySeccompFilter(SYS_time, SECCOMP_RET_KILL);
vsyscall_time(nullptr); // Should result in death.
TEST_CHECK_MSG(false, "Survived invocation of test syscall");
@@ -424,8 +421,7 @@ TEST(SeccompTest, LeastPermissiveFilterReturnValueApplies) {
// one that causes the kill that should be ignored.
pid_t const pid = fork();
if (pid == 0) {
RegisterSignalHandler(
SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
RegisterSignalHandler(SIGSYS, +[](int, siginfo_t*, void*) { _exit(1); });
ApplySeccompFilter(kFilteredSyscall, SECCOMP_RET_TRACE);
ApplySeccompFilter(kFilteredSyscall, SECCOMP_RET_KILL);
ApplySeccompFilter(kFilteredSyscall, SECCOMP_RET_ERRNO | ENOTNAM);
@@ -469,6 +465,37 @@ TEST(SeccompTest, FiltersPreservedAcrossForkAndExecve) {
<< "status " << status;
}
TEST(SeccompTest, EmptyProgramIsRejected) {
struct sock_fprog prog;
prog.len = 0;
prog.filter = nullptr;
ASSERT_THAT(syscall(__NR_seccomp, SECCOMP_MODE_FILTER, &prog),
SyscallFailsWithErrno(EINVAL));
MaybeSave();
ASSERT_THAT(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0, 0),
SyscallFailsWithErrno(EINVAL));
}
TEST(SeccompTest, ProgramTooLargeIsRejected) {
constexpr int kTooLargeFilterSize = 4097; // BPF_MAXINSNS + 1
TEST_PCHECK(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0);
MaybeSave();
struct sock_filter filter[kTooLargeFilterSize];
for (int i = 0; i < kTooLargeFilterSize; ++i) {
filter[i] = BPF_STMT(BPF_LD | BPF_ABS | BPF_W, 0); // A = seccomp_data.nr
}
filter[kTooLargeFilterSize - 1] =
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); // Return allow
struct sock_fprog prog;
prog.len = ABSL_ARRAYSIZE(filter);
prog.filter = filter;
ASSERT_THAT(syscall(__NR_seccomp, SECCOMP_MODE_FILTER, &prog),
SyscallFailsWithErrno(EINVAL));
MaybeSave();
ASSERT_THAT(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0, 0),
SyscallFailsWithErrno(EINVAL));
}
} // namespace
} // namespace testing