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(); });