Files
FEX-Emu--FEX/Source/Tools/LinuxEmulation/LinuxSyscalls/Seccomp/BPFEmitter.cpp
T
Ryan Houdek ac32876e4e LinuxEmulation: Implement support for seccomp
Seccomp is a relatively complex feature that was added to Linux back in
2005, and was further extended in 2013 to support BPF based protections.
Once seccomp is enabled, you can no longer disable seccomp but
additional protections can be placed on top of existing seccomp filters.
Additionally seccomp filters are inherited in child processes, which
ensures the process tree can't escape from the secure computing
environment through child processes.

The basis of this feature is a shim that lives between userspace and the
kernel at the syscall entrypoint.
In "strict" mode, seccomp only allows read, write, exit, exit_group, and {rt_,}sigreturn to function.
When in "filter" mode, a BPF filter is run on syscall entrypoint and
returns state about if the syscall should be allowed or not. Multiple
filters can be installed in this mode, all of which get executed. The
result that is the most restricted is the action that occurs at the end.

There are some significant limitations in filter mode that must be
adhered to which makes executing this code inside of kernel space a
non-issue and effectively limits how much cpu time is spent in the filters.
Although these filters are free to do basically anything with the
provided data, just can't do any loops.

FEX needs to implement seccomp because there are multiple applications
using the feature, the primary one being Chromium which some games embed
without disabling the sandbox. WINE also uses seccomp for capturing
games that do raw Windows system calls. Apparently Red Dead Redemption
is one of the games that requires this.

While FEX implements seccomp, it is not yet all encompassing, which is
one of the reasons why it isn't enabled by default and requires a config
option.

**seccomp_unotify is not implemented**
This is a relatively new feature for seccomp which lets the seccomp
filter signal an FD for multiple things. Luckily Chromium and WINE don't
use this. This will be tricky to implement under FEX since it
requires ioctl trapping and some other behaviour

**ptrace isn't supported**
One feature of seccomp is that it can raise ptrace events. Since FEX
doesn't support ptrace at all, this isn't handled. Again Chromium and
WINE don't use this.

**kill-thread not quite correct**
This isn't directly related to seccomp but more about how we do thread
shutdown in FEX. This will require some more changes around thread state
tracking before fully supporting this. Chromium and WINE don't use this.
kill-process also falls under this

Features that are supported:
- Strict mode and seccomp-bpf mode supported
- All BFP instructions that seccomp-bpf understands
- Inheriting seccomp through execve
   - This means we serialize and deserialize the calling thread's
     seccomp filters
   - An execve that escapes FEX will also escape seccomp. Not much we
     can do about it
- TSync - Allowing post-mortem seccomp insertion which allows threads to
  synchronize seccomp filters after the fact

Features that are not supported:
- Different arch qualifiers depending on syscall entrypoint
  - Just like our syscall handler, we are hardcoded to the arch that the
    application starts with
- user_notif
- ptrace
- Runtime code cache invalidation when seccomp is installed
  - Currently we must ensure all syscalls go through the frontend
    syscall handler
  - Runtime invalidation of code cache with inline syscalls will get
    fixed in the future.

This currently isn't enabled by default because of the minor feature
problems that haven't been resolved. Currently the Linux Kernel's test
application works for the features that FEX supports, and WINE's usage
can be handled by FEX. Chromium's sandbox doesn't yet work with this PR,
but it only fails due to features unrelated to seccomp.

Having this open for merging now so we can work to resolve the remaining
issues without this bitrotting.
2024-09-02 14:07:53 -07:00

382 lines
11 KiB
C++

// SPDX-License-Identifier: MIT
/*
$info$
tags: LinuxSyscalls|syscalls-shared
$end_info$
*/
#include "LinuxSyscalls/Seccomp/BPFEmitter.h"
#include "LinuxSyscalls/Seccomp/SeccompEmulator.h"
#include <linux/bpf_common.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#define VALIDATE(cond) \
do { \
if (!(cond)) { \
RETURN_ERROR(-EINVAL) \
} \
} while (0)
namespace FEX::HLE {
template<bool CalculateSize>
uint64_t BPFEmitter::HandleLoad(uint32_t BPFIP, const sock_filter* Inst) {
VALIDATE(BPF_SIZE(Inst->code) == BPF_W);
[[maybe_unused]] size_t OpSize {};
const auto DestReg = BPF_CLASS(Inst->code) == BPF_LD ? REG_A : REG_X;
switch (BPF_MODE(Inst->code)) {
case BPF_IMM: {
auto Const = ConstPool.try_emplace(Inst->k, ARMEmitter::ForwardLabel {});
EMIT_INST(ldr(DestReg, &Const.first->second));
break;
}
case BPF_ABS: {
// ABS has some restrictions
// - Must be 4-byte aligned
// - Must be less than the size of seccomp_data
const auto Offset = Inst->k;
// Need to be 4-byte aligned.
VALIDATE((Offset & 0b11) == 0);
// Ensure accessing inside of seccomp_data.
VALIDATE(Offset < sizeof(seccomp_data));
EMIT_INST(ldr(DestReg, REG_SECCOMP_DATA, Offset));
break;
}
case BPF_MEM:
// Must be smaller than scratch space size.
VALIDATE(Inst->k < 16);
EMIT_INST(ldr(DestReg, REG_SECCOMP_DATA, offsetof(WorkingBuffer, ScratchMemory[Inst->k])));
break;
case BPF_LEN:
// Just returns the length of seccomp_data.
EMIT_INST(movz(DestReg, sizeof(seccomp_data)));
break;
case BPF_IND:
case BPF_MSH:
default: RETURN_ERROR(-EINVAL); // Unsupported
}
RETURN_SUCCESS();
}
template<bool CalculateSize>
uint64_t BPFEmitter::HandleStore(uint32_t BPFIP, const sock_filter* Inst) {
VALIDATE(BPF_SIZE(Inst->code) == BPF_W);
[[maybe_unused]] size_t OpSize {};
const auto SrcReg = BPF_CLASS(Inst->code) == BPF_LD ? REG_A : REG_X;
// Must be smaller than scratch space size.
VALIDATE(Inst->k < 16);
EMIT_INST(str(SrcReg, REG_SECCOMP_DATA, offsetof(WorkingBuffer, ScratchMemory[Inst->k])));
RETURN_SUCCESS();
}
template<bool CalculateSize>
uint64_t BPFEmitter::HandleALU(uint32_t BPFIP, const sock_filter* Inst) {
[[maybe_unused]] size_t OpSize {};
const auto SrcType = BPF_SRC(Inst->code);
const auto Op = BPF_OP(Inst->code);
switch (Op) {
case BPF_ADD:
case BPF_SUB:
case BPF_MUL:
case BPF_DIV:
case BPF_OR:
case BPF_AND:
case BPF_LSH:
case BPF_RSH:
case BPF_MOD:
case BPF_XOR: {
auto SrcReg = REG_X;
if (SrcType == BPF_K) {
SrcReg = REG_TMP;
auto Const = ConstPool.try_emplace(Inst->k, ARMEmitter::ForwardLabel {});
EMIT_INST(ldr(SrcReg, &Const.first->second));
}
switch (Op) {
case BPF_ADD: EMIT_INST(add(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
case BPF_SUB: EMIT_INST(sub(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
case BPF_MUL: EMIT_INST(mul(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
case BPF_DIV:
// Specifically unsigned.
EMIT_INST(udiv(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg));
break;
case BPF_OR: EMIT_INST(orr(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
case BPF_AND: EMIT_INST(and_(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
case BPF_LSH: EMIT_INST(lslv(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
case BPF_RSH: EMIT_INST(lsrv(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
case BPF_MOD:
// Specifically unsigned.
EMIT_INST(udiv(ARMEmitter::Size::i32Bit, REG_TMP2, REG_A, SrcReg));
EMIT_INST(msub(ARMEmitter::Size::i32Bit, REG_A, REG_TMP2, SrcReg, REG_A));
break;
case BPF_XOR: EMIT_INST(eor(ARMEmitter::Size::i32Bit, REG_A, REG_A, SrcReg)); break;
default: RETURN_ERROR(-EINVAL);
}
break;
}
case BPF_NEG:
// Only BPF_K supported on NEG.
VALIDATE(SrcType == BPF_K);
EMIT_INST(neg(ARMEmitter::Size::i32Bit, REG_A, REG_A));
break;
default: RETURN_ERROR(-EINVAL);
}
RETURN_SUCCESS();
}
template<bool CalculateSize>
uint64_t BPFEmitter::HandleJmp(uint32_t BPFIP, uint32_t NumInst, const sock_filter* Inst) {
[[maybe_unused]] size_t OpSize {};
const auto SrcType = BPF_SRC(Inst->code);
const auto Op = BPF_OP(Inst->code);
switch (Op) {
case BPF_JA: {
// Only BPF_K supported on JA.
VALIDATE(SrcType == BPF_K);
// BPF IP register is effectively only 32-bit. Treat k constant like a signed integer.
// This allows it to jump anywhere in the program.
// But! Loops are EXPLICITLY disallowed inside of BPF programs.
// This is to prevent DOS style attacks through BPF programs.
uint64_t Target = BPFIP + Inst->k + 1;
// Must not jump past the end.
VALIDATE(Target < NumInst);
fextl::unordered_map<uint32_t, ARMEmitter::ForwardLabel>::iterator TargetLabel {};
if constexpr (!CalculateSize) {
TargetLabel = JumpLabels.try_emplace(Target, ARMEmitter::ForwardLabel {}).first;
}
EMIT_INST(b(&TargetLabel->second));
break;
}
case BPF_JEQ:
case BPF_JGT:
case BPF_JGE:
case BPF_JSET: {
auto CompareSrcReg = REG_X;
if (SrcType == BPF_K) {
CompareSrcReg = REG_TMP;
auto Const = ConstPool.try_emplace(Inst->k, ARMEmitter::ForwardLabel {});
EMIT_INST(ldr(CompareSrcReg, &Const.first->second));
}
uint32_t TargetTrue = BPFIP + Inst->jt + 1;
uint32_t TargetFalse = BPFIP + Inst->jf + 1;
// Must not jump past the end.
VALIDATE(TargetTrue < NumInst && TargetFalse < NumInst);
ARMEmitter::Condition CompareResultOp;
if (Op == BPF_JEQ) {
CompareResultOp = ARMEmitter::Condition::CC_EQ;
EMIT_INST(cmp(ARMEmitter::Size::i32Bit, REG_A, CompareSrcReg));
} else if (Op == BPF_JGT) {
CompareResultOp = ARMEmitter::Condition::CC_HI;
EMIT_INST(cmp(ARMEmitter::Size::i32Bit, REG_A, CompareSrcReg));
} else if (Op == BPF_JGE) {
CompareResultOp = ARMEmitter::Condition::CC_HS;
EMIT_INST(cmp(ARMEmitter::Size::i32Bit, REG_A, CompareSrcReg));
} else if (Op == BPF_JSET) {
CompareResultOp = ARMEmitter::Condition::CC_NE;
EMIT_INST(tst(ARMEmitter::Size::i32Bit, REG_A, CompareSrcReg));
} else {
RETURN_ERROR(-EINVAL);
}
fextl::unordered_map<uint32_t, ARMEmitter::ForwardLabel>::iterator TargetTrueLabel {};
fextl::unordered_map<uint32_t, ARMEmitter::ForwardLabel>::iterator TargetFalseLabel {};
if constexpr (!CalculateSize) {
TargetTrueLabel = JumpLabels.try_emplace(TargetTrue, ARMEmitter::ForwardLabel {}).first;
TargetFalseLabel = JumpLabels.try_emplace(TargetFalse, ARMEmitter::ForwardLabel {}).first;
}
EMIT_INST(b(CompareResultOp, &TargetTrueLabel->second));
EMIT_INST(b(&TargetFalseLabel->second));
break;
}
default: RETURN_ERROR(-EINVAL); // Unknown jump type
}
RETURN_SUCCESS();
}
template<bool CalculateSize>
uint64_t BPFEmitter::HandleRet(uint32_t BPFIP, const sock_filter* Inst) {
[[maybe_unused]] size_t OpSize {};
const auto RValSrc = BPF_RVAL(Inst->code);
switch (RValSrc) {
case BPF_K: {
auto Const = ConstPool.try_emplace(Inst->k, ARMEmitter::ForwardLabel {});
EMIT_INST(ldr(ARMEmitter::WReg::w0, &Const.first->second));
break;
}
case BPF_X: EMIT_INST(mov(ARMEmitter::WReg::w0, REG_X)); break;
case BPF_A:
// w0 is already REG_A
static_assert(REG_A == ARMEmitter::WReg::w0, "This is expected to be the same");
break;
default: RETURN_ERROR(-EINVAL);
}
EMIT_INST(ret());
RETURN_SUCCESS();
}
template<bool CalculateSize>
uint64_t BPFEmitter::HandleMisc(uint32_t BPFIP, const sock_filter* Inst) {
[[maybe_unused]] size_t OpSize {};
const auto MiscOp = BPF_MISCOP(Inst->code);
switch (MiscOp) {
case BPF_TAX: EMIT_INST(mov(REG_X, REG_A)); break;
case BPF_TXA: EMIT_INST(mov(REG_A, REG_X)); break;
default: RETURN_ERROR(-EINVAL) // Unsupported misc operation.
}
RETURN_SUCCESS();
}
template<bool CalculateSize, class Pred>
uint64_t BPFEmitter::HandleEmission(uint32_t flags, const sock_fprog* prog) {
constexpr Pred PredFunc;
uint64_t CalculatedSize {};
for (uint32_t i = 0; i < prog->len; ++i) {
if constexpr (!CalculateSize) {
auto jump_label = JumpLabels.find(i);
if (jump_label != JumpLabels.end()) {
Bind(&jump_label->second);
}
}
bool HadError {};
uint64_t Result {};
const sock_filter* Inst = &prog->filter[i];
const uint16_t Code = Inst->code;
const uint16_t Class = BPF_CLASS(Code);
switch (Class) {
case BPF_LD:
case BPF_LDX: {
Result = HandleLoad<CalculateSize>(i, Inst);
break;
}
case BPF_ST:
case BPF_STX: {
Result = HandleStore<CalculateSize>(i, Inst);
break;
}
case BPF_ALU: {
Result = HandleALU<CalculateSize>(i, Inst);
break;
}
case BPF_JMP: {
Result = HandleJmp<CalculateSize>(i, prog->len, Inst);
break;
}
case BPF_RET: {
Result = HandleRet<CalculateSize>(i, Inst);
break;
}
case BPF_MISC: {
Result = HandleMisc<CalculateSize>(i, Inst);
break;
}
default:
// We handle all instruction classes.
FEX_UNREACHABLE;
}
HadError = PredFunc(Result);
if constexpr (CalculateSize) {
CalculatedSize += Result;
}
if (HadError) {
if constexpr (!CalculateSize) {
// Had error, early return and free the memory.
FEXCore::Allocator::munmap(GetBufferBase(), FuncSize);
}
return Result;
}
}
if constexpr (CalculateSize) {
// Add the constant pool size.
CalculatedSize += ConstPool.size() * 4;
// Size calculation could have added constants and jump labels. Erase them now.
ConstPool.clear();
JumpLabels.clear();
return CalculatedSize;
}
return 0;
}
uint64_t BPFEmitter::JITFilter(uint32_t flags, const sock_fprog* prog) {
FuncSize = HandleEmission<true, SizeErrorCheck>(flags, prog);
if (FuncSize == ~0ULL) {
// Buffer size calculation found invalid code.
return -EINVAL;
}
SetBuffer((uint8_t*)FEXCore::Allocator::mmap(nullptr, FuncSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), FuncSize);
const auto CodeBegin = GetCursorAddress<uint8_t*>();
uint64_t Result = HandleEmission<false, EmissionErrorCheck>(flags, prog);
if (Result != 0) {
// Had error, early return and free the memory.
FEXCore::Allocator::munmap(GetBufferBase(), FuncSize);
return Result;
}
const uint64_t CodeOnlySize = GetCursorAddress<uint8_t*>() - CodeBegin;
// Emit the constant pool.
Align();
for (auto& Const : ConstPool) {
Bind(&Const.second);
dc32(Const.first);
}
ClearICache(CodeBegin, CodeOnlySize);
::mprotect(CodeBegin, AllocationSize(), PROT_READ | PROT_EXEC);
Func = CodeBegin;
if constexpr (false) {
// Useful for debugging seccomp filters.
LogMan::Msg::DFmt("JITFilter: disas 0x{:x},+{}", (uint64_t)CodeBegin, CodeOnlySize);
}
ConstPool.clear();
JumpLabels.clear();
return 0;
}
} // namespace FEX::HLE