From 2b26d0fff552b563eb422217ca75a84df2debe2e Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sat, 10 Aug 2024 20:20:44 -0700 Subject: [PATCH] LinuxSyscalls: Implements less invasive assertion only EFAULT handlers With the previous Copy{To,From}User helpers we need to actually implement the handlers correctly. We want something that is a bit lighter so we don't need to implement the faulting path in the syscall handlers. Implements a handful of helpers that just check for readable and writable capability which can be thrown in to an assertion handler that is zero cost in release mode. Readable is checked by just attempting to read all bytes. Writable is checked by attempting to read each byte and writing it back to the same location. Uses these helpers in x64/FD.cpp to showcase how they will be used to detect EFAULT. Tested locally that they work correctly by writing some small tests for the syscalls that expect EFAULT. --- Source/Tools/LinuxEmulation/CMakeLists.txt | 2 +- .../LinuxSyscalls/FaultSafeMemcpy.cpp | 69 ------- .../LinuxSyscalls/FaultSafeUserMemAccess.cpp | 183 ++++++++++++++++++ .../LinuxSyscalls/SignalDelegator.cpp | 3 +- .../LinuxEmulation/LinuxSyscalls/Syscalls.h | 41 +++- .../LinuxEmulation/LinuxSyscalls/x32/FD.cpp | 4 +- .../LinuxEmulation/LinuxSyscalls/x64/FD.cpp | 17 ++ 7 files changed, 244 insertions(+), 75 deletions(-) delete mode 100644 Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeMemcpy.cpp create mode 100644 Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeUserMemAccess.cpp diff --git a/Source/Tools/LinuxEmulation/CMakeLists.txt b/Source/Tools/LinuxEmulation/CMakeLists.txt index 3076a3158..e276a3136 100644 --- a/Source/Tools/LinuxEmulation/CMakeLists.txt +++ b/Source/Tools/LinuxEmulation/CMakeLists.txt @@ -4,7 +4,7 @@ set (SRCS VDSO_Emulation.cpp LinuxSyscalls/GdbServer.cpp LinuxSyscalls/EmulatedFiles/EmulatedFiles.cpp - LinuxSyscalls/FaultSafeMemcpy.cpp + LinuxSyscalls/FaultSafeUserMemAccess.cpp LinuxSyscalls/FileManagement.cpp LinuxSyscalls/LinuxAllocator.cpp LinuxSyscalls/NetStream.cpp diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeMemcpy.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeMemcpy.cpp deleted file mode 100644 index f4abf5b25..000000000 --- a/Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeMemcpy.cpp +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT -#include "LinuxSyscalls/Syscalls.h" - -namespace FEX::HLE::FaultSafeMemcpy { -#ifdef _M_ARM_64 -__attribute__((naked)) size_t CopyFromUser(void* Dest, const void* Src, size_t Size) { - __asm volatile(R"( - // Early exit if a memcpy of size zero. - cbz x2, 2f; - - 1: - .globl CopyFromUser_FaultInst - CopyFromUser_FaultInst: - ldrb w3, [x1], 1; // <- This line can fault. - strb w3, [x0], 1; - sub x2, x2, 1; - cbnz x2, 1b; -2: - mov x0, 0; - ret; - )" :: - : "memory"); -} - -__attribute__((naked)) size_t CopyToUser(void* Dest, const void* Src, size_t Size) { - __asm volatile(R"( - // Early exit if a memcpy of size zero. - cbz x2, 2f; - - 1: - ldrb w3, [x1], 1; - .globl CopyToUser_FaultInst - CopyToUser_FaultInst: - strb w3, [x0], 1; // <- This line can fault. - sub x2, x2, 1; - cbnz x2, 1b; -2: - mov x0, 0; - ret; - )" :: - : "memory"); -} - -extern "C" uint64_t CopyFromUser_FaultInst; -void* const CopyFromUser_FaultLocation = &CopyFromUser_FaultInst; - -extern "C" uint64_t CopyToUser_FaultInst; -void* const CopyToUser_FaultLocation = &CopyToUser_FaultInst; - -bool IsFaultLocation(uint64_t PC) { - return reinterpret_cast(PC) == CopyFromUser_FaultLocation || reinterpret_cast(PC) == CopyToUser_FaultLocation; -} - -#else -size_t CopyFromUser(void* Dest, const void* Src, size_t Size) { - memcpy(Dest, Src, Size); - return Size; -} - -size_t CopyToUser(void* Dest, const void* Src, size_t Size) { - memcpy(Dest, Src, Size); - return Size; -} - -bool IsFaultLocation(uint64_t PC) { - return false; -} -#endif -} // namespace FEX::HLE::FaultSafeMemcpy diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeUserMemAccess.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeUserMemAccess.cpp new file mode 100644 index 000000000..8e654e9eb --- /dev/null +++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeUserMemAccess.cpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +#include "LinuxSyscalls/Syscalls.h" + +namespace FEX::HLE::FaultSafeUserMemAccess { +#ifdef _M_ARM_64 +__attribute__((naked)) size_t CopyFromUser(void* Dest, const void* Src, size_t Size) { + __asm volatile(R"( + // Early exit if a memcpy of size zero. + cbz x2, 2f; + + 1: + .globl CopyFromUser_FaultInst + CopyFromUser_FaultInst: + ldrb w3, [x1], 1; // <- This line can fault. + strb w3, [x0], 1; + sub x2, x2, 1; + cbnz x2, 1b; +2: + mov x0, 0; + ret; + )" :: + : "memory"); +} + +__attribute__((naked)) size_t CopyToUser(void* Dest, const void* Src, size_t Size) { + __asm volatile(R"( + // Early exit if a memcpy of size zero. + cbz x2, 2f; + + 1: + ldrb w3, [x1], 1; + .globl CopyToUser_FaultInst + CopyToUser_FaultInst: + strb w3, [x0], 1; // <- This line can fault. + sub x2, x2, 1; + cbnz x2, 1b; +2: + mov x0, 0; + ret; + )" :: + : "memory"); +} + +extern "C" uint64_t CopyFromUser_FaultInst; +void* const CopyFromUser_FaultLocation = &CopyFromUser_FaultInst; + +extern "C" uint64_t CopyToUser_FaultInst; +void* const CopyToUser_FaultLocation = &CopyToUser_FaultInst; + +#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED && defined(_M_ARM_64) +__attribute__((naked)) bool VerifyIsReadableImpl(const void* Src, size_t Size) { + __asm volatile(R"( + // Early exit if size is zero. + cbz x1, 2f; + + 1: + .globl UserReadable_FaultInst + UserReadable_FaultInst: + ldrb wzr, [x0], 1; // <- This line can fault. + sub x1, x1, 1; + cbnz x1, 1b; + + 2: + mov x0, 1; + ret; + )" :: + : "memory"); +} + +__attribute__((naked)) bool VerifyIsOnlyWritable(void* Src, size_t Size) { + __asm volatile(R"( + // Early exit if size is zero. + cbz x1, 2f; + + 1: + ldrb w2, [x0]; + .globl UserWritable_FaultInst + UserWritable_FaultInst: + strb w2, [x0], 1; // <- This line can fault. + + sub x1, x1, 1; + cbnz x1, 1b; + + 2: + mov x0, 1; + ret; + )" :: + : "memory"); +} + +__attribute__((naked)) bool VerifyIsStringReadableMaxSizeImpl(const char* Src, size_t MaxSize) { + __asm volatile(R"( + 1: + cbz x1, 2f; + + .globl UserStringReadable_FaultInst + UserStringReadable_FaultInst: + ldrb w2, [x0], 1; //< This line can fault. + sub x1, x1, 1; + cbnz x2, 1b; + + 2: + mov x0, 1; + ret; + )" :: + : "memory"); +} + +void VerifyIsReadable(const void* Src, size_t Size) { + LOGMAN_THROW_A_FMT(VerifyIsReadableImpl(Src, Size), "EFAULT needs readable!"); +} + +void VerifyIsStringReadable(const char* Src) { + LOGMAN_THROW_A_FMT(VerifyIsStringReadableMaxSizeImpl(Src, ~0ULL), "EFAULT needs string readable!"); +} + +void VerifyIsStringReadableMaxSize(const char* Src, size_t MaxSize) { + LOGMAN_THROW_A_FMT(VerifyIsStringReadableMaxSizeImpl(Src, MaxSize), "EFAULT needs string readable!"); +} + +void VerifyIsReadableOrNull(const void* Src, size_t Size) { + if (Src == nullptr) { + return; + } + + LOGMAN_THROW_A_FMT(VerifyIsReadableImpl(Src, Size), "EFAULT needs readable!"); +} + +void VerifyIsWritable(void* Src, size_t Size) { + ///< Checking if writable needs to check if readable first. + VerifyIsReadable(Src, Size); + + LOGMAN_THROW_A_FMT(VerifyIsOnlyWritable(Src, Size), "EFAULT needs writable!"); +} + +void VerifyIsWritableOrNull(void* Src, size_t Size) { + if (Src == nullptr) { + return; + } + + ///< Checking if writable needs to check if readable first. + VerifyIsReadable(Src, Size); + LOGMAN_THROW_A_FMT(VerifyIsOnlyWritable(Src, Size), "EFAULT needs writable!"); +} + +extern "C" uint64_t UserReadable_FaultInst; +void* const UserReadable_FaultLocation = &UserReadable_FaultInst; + +extern "C" uint64_t UserWritable_FaultInst; +void* const UserWritable_FaultLocation = &UserWritable_FaultInst; + +extern "C" uint64_t UserStringReadable_FaultInst; +void* const UserStringReadable_FaultLocation = &UserStringReadable_FaultInst; +#endif + +bool IsFaultLocation(uint64_t PC) { + bool IsMemcpyFault = false; + IsMemcpyFault |= reinterpret_cast(PC) == CopyToUser_FaultLocation; + IsMemcpyFault |= reinterpret_cast(PC) == CopyFromUser_FaultLocation; +#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED && defined(_M_ARM_64) + IsMemcpyFault |= reinterpret_cast(PC) == UserReadable_FaultLocation; + IsMemcpyFault |= reinterpret_cast(PC) == UserWritable_FaultLocation; + IsMemcpyFault |= reinterpret_cast(PC) == UserStringReadable_FaultLocation; +#endif + return IsMemcpyFault; +} + +#else +size_t CopyFromUser(void* Dest, const void* Src, size_t Size) { + memcpy(Dest, Src, Size); + return Size; +} + +size_t CopyToUser(void* Dest, const void* Src, size_t Size) { + memcpy(Dest, Src, Size); + return Size; +} + +bool IsFaultLocation(uint64_t PC) { + return false; +} +#endif +} // namespace FEX::HLE::FaultSafeUserMemAccess diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/SignalDelegator.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/SignalDelegator.cpp index e526a3028..7f31f1fc4 100644 --- a/Source/Tools/LinuxEmulation/LinuxSyscalls/SignalDelegator.cpp +++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/SignalDelegator.cpp @@ -1379,7 +1379,8 @@ void SignalDelegator::HandleGuestSignal(FEXCore::Core::InternalThreadState* Thre ERROR_AND_DIE_FMT("X86 shouldn't hit this InterruptFaultPage"); #endif } - } else if (Signal == SIGSEGV && SigInfo.si_code == SEGV_ACCERR && FaultSafeMemcpy::IsFaultLocation(ArchHelpers::Context::GetPc(UContext))) { + } else if (Signal == SIGSEGV && (SigInfo.si_code == SEGV_MAPERR || SigInfo.si_code == SEGV_ACCERR) && + FaultSafeUserMemAccess::IsFaultLocation(ArchHelpers::Context::GetPc(UContext))) { // If you want to emulate EFAULT behaviour then enable this if-statement. // Do this once we find an application that depends on this. if constexpr (false) { diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/Syscalls.h b/Source/Tools/LinuxEmulation/LinuxSyscalls/Syscalls.h index 5857b26c3..ac54f2d05 100644 --- a/Source/Tools/LinuxEmulation/LinuxSyscalls/Syscalls.h +++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/Syscalls.h @@ -594,7 +594,7 @@ static bool HasSyscallError(const void* Result) { template uint64_t GetDentsEmulation(int fd, T* dirp, uint32_t count); -namespace FaultSafeMemcpy { +namespace FaultSafeUserMemAccess { // These are little helper functions for cases when FEX needs to copy data to or from the application in a robust fashion. // CopyFromUser and CopyToUser are memcpy routines that expect to safely SIGSEGV when reading or writing application memory respectively. // Returns zero if the memcpy completed, or crashes with SIGABRT and a log message if it faults. @@ -602,8 +602,45 @@ namespace FaultSafeMemcpy { size_t CopyFromUser(void* Dest, const void* Src, size_t Size); [[nodiscard]] size_t CopyToUser(void* Dest, const void* Src, size_t Size); +#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED && defined(_M_ARM_64) + // These helpers just check if the user pointer is readable and writable. + // This is useful in an assert build that can be safely sprinkled through the syscall handler without overhead in release builds. + void VerifyIsReadable(const void* Src, size_t Size); + void VerifyIsReadableOrNull(const void* Src, size_t Size); + void VerifyIsWritable(void* Src, size_t Size); + void VerifyIsWritableOrNull(void* Src, size_t Size); + + // Iterates a null-terminated string and checks if all bytes are readable + void VerifyIsStringReadable(const char* Src); + + // Iterates a null-terminated string and checks if all bytes are readable. Up to MaxSize bytes are checked. + void VerifyIsStringReadableMaxSize(const char* Src, size_t MaxSize); +#else + inline void VerifyIsReadable(const void* Src, size_t Size) { + if (Src == nullptr) { + ERROR_AND_DIE_FMT("Unexpected nullptr syscall argument"); + } + } + inline void VerifyIsReadableOrNull(const void* Src, size_t Size) {} + inline void VerifyIsWritable(void* Src, size_t Size) { + if (Src == nullptr) { + ERROR_AND_DIE_FMT("Unexpected nullptr syscall argument"); + } + } + inline void VerifyIsWritableOrNull(void* Src, size_t Size) {} + inline void VerifyIsStringReadable(const char* Src) { + if (Src == nullptr) { + ERROR_AND_DIE_FMT("Unexpected nullptr syscall argument"); + } + } + inline void VerifyIsStringReadableMaxSize(const char* Src, size_t MaxSize) { + if (Src == nullptr) { + ERROR_AND_DIE_FMT("Unexpected nullptr syscall argument"); + } + } +#endif bool IsFaultLocation(uint64_t PC); -} // namespace FaultSafeMemcpy +} // namespace FaultSafeUserMemAccess } // namespace FEX::HLE diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/x32/FD.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/x32/FD.cpp index 07c7955cc..cd6be1ac6 100644 --- a/Source/Tools/LinuxEmulation/LinuxSyscalls/x32/FD.cpp +++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/x32/FD.cpp @@ -250,7 +250,7 @@ void RegisterFD(FEX::HLE::SyscallHandler* Handler) { struct timespec* timed_ptr {}; if (timeout_ts) { struct timespec32 timeout {}; - if (FaultSafeMemcpy::CopyFromUser(&timeout, timeout_ts, sizeof(timeout)) == EFAULT) { + if (FaultSafeUserMemAccess::CopyFromUser(&timeout, timeout_ts, sizeof(timeout)) == EFAULT) { return -EFAULT; } @@ -264,7 +264,7 @@ void RegisterFD(FEX::HLE::SyscallHandler* Handler) { struct timespec32 timeout {}; timeout = tp64; - if (FaultSafeMemcpy::CopyToUser(timeout_ts, &timeout, sizeof(timeout)) == EFAULT) { + if (FaultSafeUserMemAccess::CopyToUser(timeout_ts, &timeout, sizeof(timeout)) == EFAULT) { // Write to user memory failed, this can occur if the timeout is defined in read-only memory. // This is okay to happen, kernel continues happily. } diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/x64/FD.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/x64/FD.cpp index 7710c04ff..c7edb42e0 100644 --- a/Source/Tools/LinuxEmulation/LinuxSyscalls/x64/FD.cpp +++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/x64/FD.cpp @@ -30,12 +30,20 @@ $end_info$ namespace FEX::HLE::x64 { void RegisterFD(FEX::HLE::SyscallHandler* Handler) { REGISTER_SYSCALL_IMPL_X64(poll, [](FEXCore::Core::CpuStateFrame* Frame, struct pollfd* fds, nfds_t nfds, int timeout) -> uint64_t { + FaultSafeUserMemAccess::VerifyIsWritable(fds, sizeof(struct pollfd) * nfds); uint64_t Result = ::poll(fds, nfds, timeout); SYSCALL_ERRNO(); }); REGISTER_SYSCALL_IMPL_X64( select, [](FEXCore::Core::CpuStateFrame* Frame, int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout) -> uint64_t { + ///< All FD arrays need to be writable + FaultSafeUserMemAccess::VerifyIsWritableOrNull(readfds, sizeof(uint64_t) * nfds); + FaultSafeUserMemAccess::VerifyIsWritableOrNull(writefds, sizeof(uint64_t) * nfds); + FaultSafeUserMemAccess::VerifyIsWritableOrNull(exceptfds, sizeof(uint64_t) * nfds); + FaultSafeUserMemAccess::VerifyIsReadableOrNull(timeout, sizeof(*timeout)); + ///< timeout doesn't actually need to be writable, this is a quirk of glibc. Kernel just doesn't update timeout if not possible. + FaultSafeUserMemAccess::VerifyIsWritableOrNull(timeout, sizeof(*timeout)); uint64_t Result = ::select(nfds, readfds, writefds, exceptfds, timeout); SYSCALL_ERRNO(); }); @@ -62,6 +70,8 @@ void RegisterFD(FEX::HLE::SyscallHandler* Handler) { }); REGISTER_SYSCALL_IMPL_X64(stat, [](FEXCore::Core::CpuStateFrame* Frame, const char* pathname, FEX::HLE::x64::guest_stat* buf) -> uint64_t { + FaultSafeUserMemAccess::VerifyIsStringReadableMaxSize(pathname, PATH_MAX); + FaultSafeUserMemAccess::VerifyIsWritable(buf, sizeof(*buf)); struct stat host_stat; uint64_t Result = FEX::HLE::_SyscallHandler->FM.Stat(pathname, &host_stat); if (Result != -1) { @@ -71,6 +81,7 @@ void RegisterFD(FEX::HLE::SyscallHandler* Handler) { }); REGISTER_SYSCALL_IMPL_X64(fstat, [](FEXCore::Core::CpuStateFrame* Frame, int fd, FEX::HLE::x64::guest_stat* buf) -> uint64_t { + FaultSafeUserMemAccess::VerifyIsWritable(buf, sizeof(*buf)); struct stat host_stat; uint64_t Result = ::fstat(fd, &host_stat); if (Result != -1) { @@ -80,6 +91,8 @@ void RegisterFD(FEX::HLE::SyscallHandler* Handler) { }); REGISTER_SYSCALL_IMPL_X64(lstat, [](FEXCore::Core::CpuStateFrame* Frame, const char* path, FEX::HLE::x64::guest_stat* buf) -> uint64_t { + FaultSafeUserMemAccess::VerifyIsStringReadableMaxSize(path, PATH_MAX); + FaultSafeUserMemAccess::VerifyIsWritable(buf, sizeof(*buf)); struct stat host_stat; uint64_t Result = FEX::HLE::_SyscallHandler->FM.Lstat(path, &host_stat); if (Result != -1) { @@ -90,6 +103,8 @@ void RegisterFD(FEX::HLE::SyscallHandler* Handler) { REGISTER_SYSCALL_IMPL_X64( newfstatat, [](FEXCore::Core::CpuStateFrame* Frame, int dirfd, const char* pathname, FEX::HLE::x64::guest_stat* buf, int flag) -> uint64_t { + FaultSafeUserMemAccess::VerifyIsStringReadableMaxSize(pathname, PATH_MAX); + FaultSafeUserMemAccess::VerifyIsWritable(buf, sizeof(*buf)); struct stat host_stat; uint64_t Result = FEX::HLE::_SyscallHandler->FM.NewFSStatAt(dirfd, pathname, &host_stat, flag); if (Result != -1) { @@ -108,6 +123,8 @@ void RegisterFD(FEX::HLE::SyscallHandler* Handler) { }); REGISTER_SYSCALL_IMPL_X64(statfs, [](FEXCore::Core::CpuStateFrame* Frame, const char* path, struct statfs* buf) -> uint64_t { + FaultSafeUserMemAccess::VerifyIsStringReadableMaxSize(path, PATH_MAX); + FaultSafeUserMemAccess::VerifyIsWritable(buf, sizeof(*buf)); uint64_t Result = FEX::HLE::_SyscallHandler->FM.Statfs(path, buf); SYSCALL_ERRNO(); });