Files
FEX-Emu--FEX/Source/Tools/LinuxEmulation/LinuxSyscalls/ThreadManager.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

261 lines
8.1 KiB
C++

// SPDX-License-Identifier: MIT
#include "LinuxSyscalls/Syscalls.h"
#include "LinuxSyscalls/SignalDelegator.h"
#include <FEXHeaderUtils/Syscalls.h>
namespace FEX::HLE {
FEX::HLE::ThreadStateObject* ThreadManager::CreateThread(uint64_t InitialRIP, uint64_t StackPointer, FEXCore::Core::CPUState* NewThreadState,
uint64_t ParentTID, FEX::HLE::ThreadStateObject* InheritThread) {
auto ThreadStateObject = new FEX::HLE::ThreadStateObject;
ThreadStateObject->ThreadInfo.parent_tid = ParentTID;
ThreadStateObject->ThreadInfo.PID = ::getpid();
if (ParentTID == 0) {
ThreadStateObject->ThreadInfo.TID = FHU::Syscalls::gettid();
}
ThreadStateObject->Thread = CTX->CreateThread(InitialRIP, StackPointer, NewThreadState, ParentTID);
ThreadStateObject->Thread->FrontendPtr = ThreadStateObject;
if (InheritThread) {
FEX::HLE::_SyscallHandler->SeccompEmulator.InheritSeccompFilters(InheritThread, ThreadStateObject);
}
++IdleWaitRefCount;
return ThreadStateObject;
}
void ThreadManager::DestroyThread(FEX::HLE::ThreadStateObject* Thread, bool NeedsTLSUninstall) {
{
std::lock_guard lk(ThreadCreationMutex);
auto It = std::find(Threads.begin(), Threads.end(), Thread);
LOGMAN_THROW_A_FMT(It != Threads.end(), "Thread wasn't in Threads");
Threads.erase(It);
}
HandleThreadDeletion(Thread, NeedsTLSUninstall);
}
void ThreadManager::StopThread(FEX::HLE::ThreadStateObject* Thread) {
if (Thread->Thread->RunningEvents.Running.exchange(false)) {
SignalDelegation->SignalThread(Thread->Thread, FEXCore::Core::SignalEvent::Stop);
}
}
void ThreadManager::RunThread(FEX::HLE::ThreadStateObject* Thread) {
// Tell the thread to start executing
Thread->Thread->StartRunning.NotifyAll();
}
void ThreadManager::HandleThreadDeletion(FEX::HLE::ThreadStateObject* Thread, bool NeedsTLSUninstall) {
if (Thread->Thread->ExecutionThread) {
if (Thread->Thread->ExecutionThread->joinable()) {
Thread->Thread->ExecutionThread->join(nullptr);
}
if (Thread->Thread->ExecutionThread->IsSelf()) {
Thread->Thread->ExecutionThread->detach();
}
}
CTX->DestroyThread(Thread->Thread, NeedsTLSUninstall);
FEX::HLE::_SyscallHandler->SeccompEmulator.FreeSeccompFilters(Thread);
delete Thread;
--IdleWaitRefCount;
IdleWaitCV.notify_all();
}
void ThreadManager::NotifyPause() {
// Tell all the threads that they should pause
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
SignalDelegation->SignalThread(Thread->Thread, FEXCore::Core::SignalEvent::Pause);
}
}
void ThreadManager::Pause() {
NotifyPause();
WaitForIdle();
}
void ThreadManager::Run() {
// Spin up all the threads
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
Thread->Thread->SignalReason.store(FEXCore::Core::SignalEvent::Return);
}
for (auto& Thread : Threads) {
Thread->Thread->StartRunning.NotifyAll();
}
}
void ThreadManager::WaitForIdleWithTimeout() {
std::unique_lock<std::mutex> lk(IdleWaitMutex);
bool WaitResult = IdleWaitCV.wait_for(lk, std::chrono::milliseconds(1500), [this] { return IdleWaitRefCount.load() == 0; });
if (!WaitResult) {
// The wait failed, this will occur if we stepped in to a syscall
// That's okay, we just need to pause the threads manually
NotifyPause();
}
// We have sent every thread a pause signal
// Now wait again because they /will/ be going to sleep
WaitForIdle();
}
void ThreadManager::WaitForThreadsToRun() {
size_t NumThreads {};
{
std::lock_guard lk(ThreadCreationMutex);
NumThreads = Threads.size();
}
// Spin while waiting for the threads to start up
std::unique_lock<std::mutex> lk(IdleWaitMutex);
IdleWaitCV.wait(lk, [this, NumThreads] { return IdleWaitRefCount.load() >= NumThreads; });
Running = true;
}
void ThreadManager::Step() {
LogMan::Msg::AFmt("ThreadManager::Step currently not implemented");
{
std::lock_guard lk(ThreadCreationMutex);
// Walk the threads and tell them to clear their caches
// Useful when our block size is set to a large number and we need to step a single instruction
for (auto& Thread : Threads) {
CTX->ClearCodeCache(Thread->Thread);
}
}
// TODO: Set to single step mode.
Run();
WaitForThreadsToRun();
WaitForIdle();
// TODO: Set back to full running mode.
}
void ThreadManager::Stop(bool IgnoreCurrentThread) {
pid_t tid = FHU::Syscalls::gettid();
FEX::HLE::ThreadStateObject* CurrentThread {};
// Tell all the threads that they should stop
{
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
if (IgnoreCurrentThread && Thread->ThreadInfo.TID == tid) {
// If we are callign stop from the current thread then we can ignore sending signals to this thread
// This means that this thread is already gone
} else if (Thread->ThreadInfo.TID == tid) {
// We need to save the current thread for last to ensure all threads receive their stop signals
CurrentThread = Thread;
continue;
}
if (Thread->Thread->RunningEvents.Running.load()) {
StopThread(Thread);
}
// If the thread is waiting to start but immediately killed then there can be a hang
// This occurs in the case of gdb attach with immediate kill
if (Thread->Thread->RunningEvents.WaitingToStart.load()) {
Thread->Thread->RunningEvents.EarlyExit = true;
Thread->Thread->StartRunning.NotifyAll();
}
}
}
// Stop the current thread now if we aren't ignoring it
if (CurrentThread) {
StopThread(CurrentThread);
}
}
void ThreadManager::SleepThread(FEXCore::Context::Context* CTX, FEXCore::Core::CpuStateFrame* Frame) {
auto Thread = Frame->Thread;
--IdleWaitRefCount;
IdleWaitCV.notify_all();
Thread->RunningEvents.ThreadSleeping = true;
// Go to sleep
Thread->StartRunning.Wait();
Thread->RunningEvents.Running = true;
++IdleWaitRefCount;
Thread->RunningEvents.ThreadSleeping = false;
IdleWaitCV.notify_all();
}
void ThreadManager::UnlockAfterFork(FEXCore::Core::InternalThreadState* LiveThread, bool Child) {
if (!Child) {
return;
}
// This function is called after fork
// We need to cleanup some of the thread data that is dead
for (auto& DeadThread : Threads) {
if (DeadThread->Thread == LiveThread) {
continue;
}
// Setting running to false ensures that when they are shutdown we won't send signals to kill them
DeadThread->Thread->RunningEvents.Running = false;
// Despite what google searches may susgest, glibc actually has special code to handle forks
// with multiple active threads.
// It cleans up the stacks of dead threads and marks them as terminated.
// It also cleans up a bunch of internal mutexes.
// FIXME: TLS is probally still alive. Investigate
// Deconstructing the Interneal thread state should clean up most of the state.
// But if anything on the now deleted stack is holding a refrence to the heap, it will be leaked
CTX->DestroyThread(DeadThread->Thread);
delete DeadThread;
// FIXME: Make sure sure nothing gets leaked via the heap. Ideas:
// * Make sure nothing is allocated on the heap without ref in InternalThreadState
// * Surround any code that heap allocates with a per-thread mutex.
// Before forking, the the forking thread can lock all thread mutexes.
}
// Remove all threads but the live thread from Threads
Threads.clear();
auto ThreadObject = FEX::HLE::ThreadManager::GetStateObjectFromCPUState(LiveThread->CurrentFrame);
Threads.push_back(ThreadObject);
// Clean up dead stacks
FEXCore::Threads::Thread::CleanupAfterFork();
// We now only have one thread.
IdleWaitRefCount = 1;
ThreadCreationMutex.StealAndDropActiveLocks();
}
void ThreadManager::WaitForIdle() {
std::unique_lock<std::mutex> lk(IdleWaitMutex);
IdleWaitCV.wait(lk, [this] { return IdleWaitRefCount.load() == 0; });
Running = false;
}
ThreadManager::~ThreadManager() {
std::lock_guard lk(ThreadCreationMutex);
for (auto& Thread : Threads) {
HandleThreadDeletion(Thread);
}
Threads.clear();
}
} // namespace FEX::HLE