// SPDX-License-Identifier: MIT /* $info$ tags: Bin|FEX desc: Glues the ELF loader, FEXCore and LinuxSyscalls to launch an elf under fex $end_info$ */ #include "Common/ArgumentLoader.h" #include "Common/FEXServerClient.h" #include "Common/Config.h" #include "Common/HostFeatures.h" #include "Common/Linux/SBRKAllocations.h" #include "PortabilityInfo.h" #include "ELFCodeLoader.h" #include "VDSO_Emulation.h" #include "LinuxSyscalls/GdbServer.h" #include "LinuxSyscalls/LinuxAllocator.h" #include "LinuxSyscalls/Syscalls.h" #include "LinuxSyscalls/Utils/Threads.h" #include "LinuxSyscalls/x32/Syscalls.h" #include "LinuxSyscalls/x64/Syscalls.h" #include "LinuxSyscalls/SignalDelegator.h" #include "Linux/Utils/ELFContainer.h" #include "Thunks.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace FEX::Logging { static bool SilentLog {}; static int OutputFD {STDERR_FILENO}; // Set an empty style to disable coloring when FEXServer output is e.g. piped to a file static bool DisableOutputColors {}; void MsgHandler(LogMan::DebugLevels Level, const char* Message) { if (SilentLog) { return; } const auto Style = DisableOutputColors ? fmt::text_style {} : LogMan::DebugLevelStyle(Level); const auto Output = fextl::fmt::format("{} {}\n", fmt::styled(LogMan::DebugLevelStr(Level), Style), Message); write(OutputFD, Output.c_str(), Output.size()); fsync(OutputFD); } void AssertHandler(const char* Message) { return MsgHandler(LogMan::ASSERT, Message); } namespace FEXServer { static int FEXServerFD {-1}; void MsgHandler(LogMan::DebugLevels Level, const char* Message) { FEXServerClient::MsgHandler(FEXServerFD, Level, Message); } void AssertHandler(const char* Message) { FEXServerClient::AssertHandler(FEXServerFD, Message); } } // namespace FEXServer void Init() { FEX_CONFIG_OPT(SilentLog, SILENTLOG); FEX_CONFIG_OPT(OutputLog, OUTPUTLOG); FEX::Logging::SilentLog = SilentLog(); if (SilentLog()) { LogMan::Throw::UnInstallHandler(); LogMan::Msg::UnInstallHandler(); } else { const auto& LogFile = OutputLog(); // If stderr or stdout then we need to dup the FD // In some cases some applications will close stderr and stdout // then redirect the FD to either a log OR some cases just not use // stderr/stdout and the FD will be reused for regular FD ops. // // We want to maintain the original output location otherwise we // can run in to problems of writing to some file auto LogFD = OutputFD; if (LogFile == "stderr") { LogFD = dup(STDERR_FILENO); } else if (LogFile == "server") { Logging::FEXServer::FEXServerFD = FEXServerClient::RequestLogFD(FEXServerClient::GetServerFD()); if (FEXServer::FEXServerFD != -1) { LogMan::Throw::InstallHandler(Logging::FEXServer::AssertHandler); LogMan::Msg::InstallHandler(Logging::FEXServer::MsgHandler); } } else if (!LogFile.empty()) { constexpr int USER_PERMS = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; LogFD = open(LogFile.c_str(), O_CREAT | O_CLOEXEC | O_WRONLY, USER_PERMS); } if (LogFD == -1) { LogMan::Msg::EFmt("Couldn't open log file. Going Silent."); Logging::SilentLog = true; } else { OutputFD = LogFD; } } DisableOutputColors = !isatty(OutputFD); } } // namespace FEX::Logging namespace FEX::Allocator { static fextl::unique_ptr Allocator; static fextl::vector Base48Bit; static fextl::vector Low4GB; void Init(bool Is64Bit) { if (Is64Bit) { // Destroy the 48th bit if it exists Base48Bit = FEXCore::Allocator::Setup48BitAllocatorIfExists(); } else { // Reserve [0x1_0000_0000, 0x2_0000_0000). // Safety net if 32-bit address calculation overflows in to 64-bit range. constexpr uint64_t First64BitAddr = 0x1'0000'0000ULL; Low4GB = FEXCore::Allocator::StealMemoryRegion(First64BitAddr, First64BitAddr + First64BitAddr); // Setup our userspace allocator FEXCore::Allocator::SetupHooks(); Allocator = FEX::HLE::CreatePassthroughAllocator(); // Now that the upper 32-bit address space is blocked for future allocations, // exhaust all of jemalloc's remaining internal allocations that it reserved before. // TODO: It's unclear how reliably this exhausts those reserves FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator glibc; void* data; do { data = malloc(0x1); } while (reinterpret_cast(data) >> 32 != 0); free(data); } } void Shutdown() { FEXCore::Allocator::ClearHooks(); FEXCore::Allocator::ReclaimMemoryRegion(Base48Bit); FEXCore::Allocator::ReclaimMemoryRegion(Low4GB); } } // namespace FEX::Allocator bool InterpreterHandler(fextl::string* Filename, const fextl::string& RootFS, fextl::vector* args) { int FD {-1}; // Attempt to open the filename from the rootfs first. FD = open(fextl::fmt::format("{}{}", RootFS, *Filename).c_str(), O_RDONLY | O_CLOEXEC); if (FD == -1) { // Failing that, attempt to open the filename directly. FD = open(Filename->c_str(), O_RDONLY | O_CLOEXEC); if (FD == -1) { return false; } } std::array Header; const auto ChunkSize = 257l; const auto ReadSize = pread(FD, Header.data(), ChunkSize, 0); close(FD); const auto Data = std::span(Header.data(), ReadSize); // Is the file large enough for shebang if (ReadSize <= 2) { return false; } // Handle shebang files if (Data[0] == '#' && Data[1] == '!') { std::string_view InterpreterLine {Data.begin() + 2, // strip off "#!" prefix std::find(Data.begin(), Data.end(), '\n')}; const auto ShebangArguments = FHU::ParseArgumentsFromString(InterpreterLine); // Executable argument *Filename = ShebangArguments.at(0); // Insert all the arguments at the start args->insert(args->begin(), ShebangArguments.begin(), ShebangArguments.end()); } return true; } /** * @brief Queries if FEX is installed as a binfmt_misc interpreter * * @param ExecutedWithFD If FEX was executed using a binfmt_misc FD handle from the kernel * @param Portable Portability information about FEX being run in portable mode * * @return true if the binfmt_misc handlers are installed and being used */ bool QueryInterpreterInstalled(bool ExecutedWithFD, const FEX::Config::PortableInformation& Portable) { if (Portable.IsPortable) { // Don't use binfmt interpreter even if it's installed return false; } // Check if FEX's binfmt_misc handlers are both installed. // The explicit check can be omitted if FEX was executed from an FD, // since this only happens if the kernel launched FEX through binfmt_misc return ExecutedWithFD || (access("/proc/sys/fs/binfmt_misc/FEX-x86", F_OK) == 0 && access("/proc/sys/fs/binfmt_misc/FEX-x86_64", F_OK) == 0); } namespace FEX::Kernel { namespace TSO { void SetupTSOEmulation(FEXCore::Context::Context* CTX) { // Check to see if this is supported. auto Result = prctl(PR_GET_MEM_MODEL, 0, 0, 0, 0); if (Result == -1) { // Unsupported, early exit. return; } FEX_CONFIG_OPT(TSOEnabled, TSOENABLED); if (!TSOEnabled()) { // TSO emulation isn't even enabled, early exit. return; } if (Result == PR_SET_MEM_MODEL_DEFAULT) { // Try to set the TSO mode if we are currently default. Result = prctl(PR_SET_MEM_MODEL, PR_SET_MEM_MODEL_TSO, 0, 0, 0); if (Result == 0) { // TSO mode successfully enabled. Tell the context to disable TSO emulation through atomics. // This flag gets inherited on thread creation, so FEX only needs to set it at the start. CTX->SetHardwareTSOSupport(true); } } } } // namespace TSO namespace CompatInput { void SetupCompatInput(bool enable) { // Check to see if this is supported. auto Result = prctl(PR_GET_COMPAT_INPUT, 0, 0, 0, 0); if (Result == -1) { // Unsupported, early exit. return; } if (enable) { prctl(PR_SET_COMPAT_INPUT, PR_SET_COMPAT_INPUT_ENABLE, 0, 0, 0); } else { prctl(PR_SET_COMPAT_INPUT, PR_SET_COMPAT_INPUT_DISABLE, 0, 0, 0); } } } // namespace CompatInput namespace GCS { void CheckForGCS() { uint64_t ShadowStackWord {}; if (prctl(PR_GET_SHADOW_STACK_STATUS, &ShadowStackWord, 0, 0, 0) == -1) { return; } // Kernel supports shadow stack. if (ShadowStackWord & PR_SHADOW_STACK_ENABLE) { // Welp. ERROR_AND_DIE_FMT("Shadow stack is enabled which FEX is incompatible with!"); } // Disable if we've gotten this far, to ensure guest can't try. prctl(PR_LOCK_SHADOW_STACK_STATUS, ~0ULL, 0, 0, 0); } } // namespace GCS namespace UnalignedAtomic { void SetupKernelUnalignedAtomics() { #ifndef PR_ARM64_SET_UNALIGN_ATOMIC #define PR_ARM64_SET_UNALIGN_ATOMIC 0x46455849 #define PR_ARM64_UNALIGN_ATOMIC_EMULATE (1UL << 0) #define PR_ARM64_UNALIGN_ATOMIC_BACKPATCH (1UL << 1) #define PR_ARM64_UNALIGN_ATOMIC_STRICT_SPLIT_LOCKS (1UL << 2) #endif // Interfaces with downstream FEX kernel patches to control unaligned atomic handling FEX_CONFIG_OPT(StrictInProcessSplitLocks, STRICTINPROCESSSPLITLOCKS); FEX_CONFIG_OPT(KernelUnalignedAtomicBackpatching, KERNELUNALIGNEDATOMICBACKPATCHING); uint64_t Flags = (StrictInProcessSplitLocks() ? PR_ARM64_UNALIGN_ATOMIC_STRICT_SPLIT_LOCKS : 0) | (KernelUnalignedAtomicBackpatching() ? PR_ARM64_UNALIGN_ATOMIC_BACKPATCH : 0) | PR_ARM64_UNALIGN_ATOMIC_EMULATE; prctl(PR_ARM64_SET_UNALIGN_ATOMIC, Flags, 0, 0, 0); } } // namespace UnalignedAtomic void Init(bool Is64Bit, FEXCore::Context::Context* CTX) { // Setup TSO hardware emulation immediately after initializing the context. TSO::SetupTSOEmulation(CTX); UnalignedAtomic::SetupKernelUnalignedAtomics(); if (!Is64Bit) { // Tell the kernel we want to use the compat input syscalls even though we're // a 64 bit process. CompatInput::SetupCompatInput(true); } else { // Our parent could be an instance running a 32 bit application, so we need // to disable compat input if we're running a 64 bit one ourselves. CompatInput::SetupCompatInput(false); } } } // namespace FEX::Kernel /** * @brief Get an FD from an environment variable and then unset the environment variable. * * @param Env The environment variable to extract the FD from. * * @return -1 if the variable didn't exist. */ static int StealFEXFDFromEnv(const char* Env) { int FEXFD {-1}; const char* FEXFDStr = getenv(Env); if (FEXFDStr) { const std::string_view FEXFDView {FEXFDStr}; std::from_chars(FEXFDView.data(), FEXFDView.data() + FEXFDView.size(), FEXFD, 10); unsetenv(Env); } return FEXFD; } int main(int argc, char** argv, char** const envp) { auto SBRKPointer = FEX::SBRKAllocations::DisableSBRKAllocations(); FEXCore::Allocator::GLIBCScopedFault GLIBFaultScope; const bool ExecutedWithFD = getauxval(AT_EXECFD) != 0; const auto PortableInfo = FEX::ReadPortabilityInformation(); const bool InterpreterInstalled = QueryInterpreterInstalled(ExecutedWithFD, PortableInfo); int FEXFD {StealFEXFDFromEnv("FEX_EXECVEFD")}; int FEXSeccompFD {StealFEXFDFromEnv("FEX_SECCOMPFD")}; // Early init trivial handlers. LogMan::Throw::InstallHandler(FEX::Logging::AssertHandler); LogMan::Msg::InstallHandler(FEX::Logging::MsgHandler); auto ArgsLoader = fextl::make_unique(argc, argv); auto Args = ArgsLoader->Get(); auto ParsedArgs = ArgsLoader->GetParsedArgs(); auto Program = FEX::Config::GetApplicationNames(Args, ExecutedWithFD, FEXFD); if (Program.ProgramPath.empty() && FEXFD == -1) { // Early exit if we weren't passed an argument return 0; } FEX::Kernel::GCS::CheckForGCS(); FEX::Config::LoadConfig(Program.ProgramName, envp, PortableInfo); // Reload the meta layer FEXCore::Config::ReloadMetaLayer(); FEXCore::Config::Set(FEXCore::Config::CONFIG_INTERPRETER_INSTALLED, InterpreterInstalled ? "1" : "0"); #ifdef VIXL_SIMULATOR // If running under the vixl simulator, ensure that indirect runtime calls are enabled. FEXCore::Config::Set(FEXCore::Config::CONFIG_DISABLE_VIXL_INDIRECT_RUNTIME_CALLS, "0"); #endif // Early check for process stall // Doesn't use CONFIG_ROOTFS and we don't want it to spin up a squashfs instance FEX_CONFIG_OPT(StallProcess, STALLPROCESS); FEX_CONFIG_OPT(StartupSleep, STARTUPSLEEP); FEX_CONFIG_OPT(StartupSleepProcName, STARTUPSLEEPPROCNAME); if (StallProcess) { while (1) { // Stall this process out forever select(0, nullptr, nullptr, nullptr, nullptr); } } // Ensure FEXServer is setup before config options try to pull CONFIG_ROOTFS auto SelfPath = FEX::GetSelfPath(); if (!FEXServerClient::SetupClient(SelfPath.value_or(argv[0]))) { LogMan::Msg::EFmt("FEXServerClient: Failure to setup client"); return -1; } FEX_CONFIG_OPT(LDPath, ROOTFS); FEX_CONFIG_OPT(Environment, ENV); FEX_CONFIG_OPT(HostEnvironment, HOSTENV); FEX::Logging::Init(); if (StartupSleep() && (StartupSleepProcName().empty() || Program.ProgramName == StartupSleepProcName())) { LogMan::Msg::IFmt("[{}][{}] Sleeping for {} seconds", ::getpid(), Program.ProgramName, StartupSleep()); std::this_thread::sleep_for(std::chrono::seconds(StartupSleep())); } FEXCore::Telemetry::Initialize(); if (!LDPath().empty() && Program.ProgramPath.starts_with(LDPath())) { // From this point on, ProgramPath needs to not have the LDPath prefixed on to it. auto RootFSLength = LDPath().size(); if (Program.ProgramPath.at(RootFSLength) != '/') { // Ensure the modified path starts as an absolute path. // This edge case can occur when ROOTFS ends with '/' and passed a path like `usr/bin/true`. --RootFSLength; } Program.ProgramPath.erase(0, RootFSLength); } bool ProgramExists = InterpreterHandler(&Program.ProgramPath, LDPath(), &Args); if (!ExecutedWithFD && FEXFD == -1 && !ProgramExists) { // Early exit if the program passed in doesn't exist // Will prevent a crash later fextl::fmt::print(stderr, "{}: command not found\n", Program.ProgramPath); return -ENOEXEC; } uint32_t KernelVersion = FEX::HLE::SyscallHandler::CalculateHostKernelVersion(); if (KernelVersion < FEX::HLE::SyscallHandler::KernelVersion(5, 15)) { LogMan::Msg::EFmt("FEX requires kernel 5.15 minimum. Expect problems."); } // Before we go any further, set all of our host environment variables that the config has provided for (auto& HostEnv : HostEnvironment.All()) { // We are going to keep these alive in memory. // No need to split the string with setenv putenv(HostEnv.data()); } ELFCodeLoader Loader {Program.ProgramPath, FEXFD, LDPath(), Args, ParsedArgs, envp, &Environment}; FEXCore::Config::Set(FEXCore::Config::CONFIG_IS64BIT_MODE, Loader.Is64BitMode() ? "1" : "0"); if (!Loader.ELFWasLoaded()) { // Loader couldn't load this program for some reason fextl::fmt::print(stderr, "Invalid or Unsupported elf file.\n"); #ifdef ARCHITECTURE_arm64 fextl::fmt::print(stderr, "This is likely due to a misconfigured x86-64 RootFS\n"); fextl::fmt::print(stderr, "Current RootFS path set to '{}'\n", LDPath()); if (LDPath().empty() || FHU::Filesystem::Exists(LDPath()) == false) { fextl::fmt::print(stderr, "RootFS path doesn't exist. This is required on AArch64 hosts\n"); fextl::fmt::print(stderr, "Use FEXRootFSFetcher to download a RootFS\n"); } #endif return -ENOEXEC; } if (ExecutedWithFD) { // Don't need to canonicalize Program.ProgramPath, Config loader will have resolved this already. FEXCore::Config::Set(FEXCore::Config::CONFIG_APP_FILENAME, Program.ProgramPath); FEXCore::Config::Set(FEXCore::Config::CONFIG_APP_CONFIG_NAME, Program.ProgramName); } else if (FEXFD != -1) { // Anonymous program. FEXCore::Config::Set(FEXCore::Config::CONFIG_APP_FILENAME, ""); FEXCore::Config::Set(FEXCore::Config::CONFIG_APP_CONFIG_NAME, ""); } else { { char ExistsTempPath[PATH_MAX]; char* RealPath = realpath(Program.ProgramPath.c_str(), ExistsTempPath); if (RealPath) { FEXCore::Config::Set(FEXCore::Config::CONFIG_APP_FILENAME, fextl::string(RealPath)); } else { // Can happen when jumping in to pressure-vessel. // `/usr/lib/pressure-vessel/from-host/libexec/steam-runtime-tools-0/pv-adverb` can't get resolved. FEXCore::Config::Set(FEXCore::Config::CONFIG_APP_FILENAME, Program.ProgramPath); } } FEXCore::Config::Set(FEXCore::Config::CONFIG_APP_CONFIG_NAME, Program.ProgramName); } // Setup Thread handlers, so FEXCore can create threads. auto StackTracker = FEX::LinuxEmulation::Threads::SetupThreadHandlers(); FEX::Allocator::Init(Loader.Is64BitMode()); FEXCore::Profiler::Init(Program.ProgramName, Program.ProgramPath); bool SupportsAVX {}; fextl::unique_ptr CTX; { auto HostFeatures = FEX::FetchHostFeatures(); CTX = FEXCore::Context::Context::CreateNewContext(HostFeatures); SupportsAVX = HostFeatures.SupportsAVX; } FEX::Kernel::Init(Loader.Is64BitMode(), CTX.get()); auto SignalDelegation = FEX::HLE::CreateSignalDelegator(CTX.get(), Program.ProgramName, SupportsAVX); auto ThunkHandler = FEX::HLE::CreateThunkHandler(); auto SyscallHandler = Loader.Is64BitMode() ? FEX::HLE::x64::CreateHandler(CTX.get(), SignalDelegation.get(), ThunkHandler.get()) : FEX::HLE::x32::CreateHandler(CTX.get(), SignalDelegation.get(), ThunkHandler.get(), std::move(FEX::Allocator::Allocator)); SyscallHandler->SetCodeLoader(&Loader); CTX->SetSignalDelegator(SignalDelegation.get()); CTX->SetSyscallHandler(SyscallHandler.get()); CTX->SetThunkHandler(ThunkHandler.get()); if (FEXCore::Config::Get_ENABLECODECACHINGWIP()) { CTX->SetCodeMapWriter(fextl::make_unique(*SyscallHandler)); } FEX_CONFIG_OPT(GdbServer, GDBSERVER); fextl::unique_ptr DebugServer; if (GdbServer) { DebugServer = fextl::make_unique(CTX.get(), SignalDelegation.get(), SyscallHandler.get()); } // Now that we have the syscall handler. Track some FDs that are FEX owned. if (FEX::Logging::OutputFD > 2) { SyscallHandler->FM.TrackFEXFD(FEX::Logging::OutputFD); } SyscallHandler->FM.TrackFEXFD(FEXServerClient::GetServerFD()); if (FEX::Logging::FEXServer::FEXServerFD != -1) { SyscallHandler->FM.TrackFEXFD(FEX::Logging::FEXServer::FEXServerFD); } if (!CTX->InitCore()) { return 1; } // Create a thread without a RIP or stack pointer setup initially. auto ParentThread = SyscallHandler->TM.CreateThread(0, 0); SyscallHandler->TM.TrackThread(ParentThread); SignalDelegation->RegisterTLSState(ParentThread); ThunkHandler->RegisterTLSState(ParentThread); SyscallHandler->DeserializeSeccompFD(ParentThread, FEXSeccompFD); // Load VDSO in to memory prior to mapping our ELFs. auto VDSOMapping = FEX::VDSO::LoadVDSOThunks(ParentThread->Thread, Loader.Is64BitMode(), SyscallHandler.get()); // Pass in our VDSO thunks ThunkHandler->AppendThunkDefinitions(FEX::VDSO::GetVDSOThunkDefinitions(Loader.Is64BitMode())); SignalDelegation->SetVDSOSymbols(); { Loader.SetVDSOBase(VDSOMapping.VDSOBase); Loader.CalculateHWCaps(CTX.get()); if (!Loader.MapMemory(SyscallHandler.get(), ParentThread->Thread)) { // failed to map LogMan::Msg::EFmt("Failed to map {}-bit elf file.", Loader.Is64BitMode() ? 64 : 32); return -ENOEXEC; } } auto BRKInfo = Loader.GetBRKInfo(); SyscallHandler->DefaultProgramBreak(BRKInfo.Base, BRKInfo.Size); // Request code cache generation if (FEXCore::Config::Get_ENABLECODECACHINGWIP()) { FEXServerClient::PopulateCodeCache(FEXServerClient::GetServerFD(), Loader.GetMainElfFD(), FEXCore::Config::Get_MULTIBLOCK()); } // Pull RIP and stack pointer from loader and set the thread data to it. ParentThread->Thread->CurrentFrame->State.rip = Loader.DefaultRIP(); ParentThread->Thread->CurrentFrame->State.gregs[FEXCore::X86State::REG_RSP] = Loader.GetStackPointer(); // Close the loader FDs after everything has been parsed and mapped. Loader.CloseFDs(); CTX->ExecuteThread(ParentThread->Thread); DebugServer.reset(); SyscallHandler->TM.Stop(); auto ProgramStatus = ParentThread->StatusCode; FEX::VDSO::UnloadVDSOMapping(ParentThread->Thread, SyscallHandler.get(), VDSOMapping); SignalDelegation->UninstallTLSState(ParentThread); SyscallHandler->TM.DestroyThread(ParentThread); DebugServer.reset(); SyscallHandler.reset(); SignalDelegation.reset(); FEX::LinuxEmulation::Threads::Shutdown(std::move(StackTracker)); Loader.FreeSections(); FEXCore::Config::Shutdown(); LogMan::Throw::UnInstallHandler(); LogMan::Msg::UnInstallHandler(); FEX::Allocator::Shutdown(); // Allocator is now original system allocator FEXCore::Telemetry::Shutdown(Program.ProgramName); FEXCore::Profiler::Shutdown(); FEX::SBRKAllocations::ReenableSBRKAllocations(SBRKPointer); return ProgramStatus; }