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.
This commit is contained in:
Ryan Houdek committed 2024-08-15 00:48:57 -07:00
1 parent 33558e63c4
commit 2b26d0fff5
7 files changed
+244 -75

No files matched your search

+1 -1
View File
@@ -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
@@ -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<void*>(PC) == CopyFromUser_FaultLocation || reinterpret_cast<void*>(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
@@ -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<void*>(PC) == CopyToUser_FaultLocation;
IsMemcpyFault |= reinterpret_cast<void*>(PC) == CopyFromUser_FaultLocation;
#if defined(ASSERTIONS_ENABLED) && ASSERTIONS_ENABLED && defined(_M_ARM_64)
IsMemcpyFault |= reinterpret_cast<void*>(PC) == UserReadable_FaultLocation;
IsMemcpyFault |= reinterpret_cast<void*>(PC) == UserWritable_FaultLocation;
IsMemcpyFault |= reinterpret_cast<void*>(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
@@ -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) {
@@ -594,7 +594,7 @@ static bool HasSyscallError(const void* Result) {
template<bool IncrementOffset, typename T>
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
@@ -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.
}
@@ -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();
});