Skip to content

Commit

Permalink
Merge pull request #3375 from Sonicadvance1/implement_efault_support
Browse files Browse the repository at this point in the history
Linux: Implements a fault safe memcpy routine
  • Loading branch information
Sonicadvance1 authored Jan 26, 2024
2 parents 750b0b7 + 0913741 commit 8e3d4a3
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 3 deletions.
1 change: 1 addition & 0 deletions Source/Tools/LinuxEmulation/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ set (SRCS
VDSO_Emulation.cpp
LinuxSyscalls/GdbServer.cpp
LinuxSyscalls/EmulatedFiles/EmulatedFiles.cpp
LinuxSyscalls/FaultSafeMemcpy.cpp
LinuxSyscalls/FileManagement.cpp
LinuxSyscalls/LinuxAllocator.cpp
LinuxSyscalls/NetStream.cpp
Expand Down
72 changes: 72 additions & 0 deletions Source/Tools/LinuxEmulation/LinuxSyscalls/FaultSafeMemcpy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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
}
16 changes: 16 additions & 0 deletions Source/Tools/LinuxEmulation/LinuxSyscalls/SignalDelegator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ desc: Handles host -> host and host -> guest signal routing, emulates procmask &
*/

#include "LinuxSyscalls/SignalDelegator.h"
#include "LinuxSyscalls/Syscalls.h"

#include <FEXCore/Core/Context.h>
#include <FEXCore/Core/CoreState.h>
Expand Down Expand Up @@ -1467,6 +1468,21 @@ namespace FEX::HLE {
#endif
}
}
else if (Signal == SIGSEGV &&
SigInfo.si_code == SEGV_ACCERR &&
FaultSafeMemcpy::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) {
// Return from the subroutine, returning EFAULT.
ArchHelpers::Context::SetArmReg(UContext, 0, EFAULT);
ArchHelpers::Context::SetPc(UContext, ArchHelpers::Context::GetArmReg(UContext, 30));
return;
}
else {
LogMan::Msg::AFmt("Received invalid data to syscall. Crashing now!");
}
}
else {
if (IsAsyncSignal(&SigInfo, Signal) && MustDeferSignal) {
// If the signal is asynchronous (as determined by si_code) and FEX is in a state of needing
Expand Down
10 changes: 10 additions & 0 deletions Source/Tools/LinuxEmulation/LinuxSyscalls/Syscalls.h
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,16 @@ static bool HasSyscallError(const void* Result) {

template<bool IncrementOffset, typename T>
uint64_t GetDentsEmulation(int fd, T *dirp, uint32_t count);

namespace FaultSafeMemcpy {
// 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.
[[nodiscard]] size_t CopyFromUser(void *Dest, const void* Src, size_t Size);
[[nodiscard]] size_t CopyToUser(void *Dest, const void* Src, size_t Size);
bool IsFaultLocation(uint64_t PC);
}

}

// Registers syscall for both 32bit and 64bit
Expand Down
15 changes: 13 additions & 2 deletions Source/Tools/LinuxEmulation/LinuxSyscalls/x32/FD.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,12 @@ namespace FEX::HLE::x32 {
struct timespec tp64{};
struct timespec *timed_ptr{};
if (timeout_ts) {
tp64 = *timeout_ts;
struct timespec32 timeout{};
if (FaultSafeMemcpy::CopyFromUser(&timeout, timeout_ts, sizeof(timeout)) == EFAULT) {
return -EFAULT;
}

tp64 = timeout;
timed_ptr = &tp64;
}

Expand All @@ -275,7 +280,13 @@ namespace FEX::HLE::x32 {
sigsetsize);

if (timeout_ts) {
*timeout_ts = tp64;
struct timespec32 timeout{};
timeout = tp64;

if (FaultSafeMemcpy::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.
}
}

SYSCALL_ERRNO();
Expand Down
2 changes: 1 addition & 1 deletion Source/Tools/LinuxEmulation/LinuxSyscalls/x32/Types.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ timespec32 {
int32_t tv_sec;
int32_t tv_nsec;

timespec32() = delete;
timespec32() = default;

operator timespec() const {
timespec spec{};
Expand Down
3 changes: 3 additions & 0 deletions unittests/FEXLinuxTests/Known_Failures
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ sigtest_samask.32
sigtest_samask.64
sigtest_sigmask.32
sigtest_sigmask.64

# Disabled since FEX's FaultSafeMemcpy is intentionally stub-implemented
syscalls_efault.32
169 changes: 169 additions & 0 deletions unittests/FEXLinuxTests/tests/syscalls/syscalls_efault.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#include <catch2/catch.hpp>

#include <cstdint>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <poll.h>
#include <signal.h>

TEST_CASE("poll") {
// poll can return EFAULT if first argument is pointed to invalid pointer.
// Using mmap specifically for allocating with PROT_NONE.
struct pollfd *invalid_fds = reinterpret_cast<struct pollfd *>(mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
auto ret = ::syscall(SYS_poll, invalid_fds, 1, 0);
REQUIRE(ret == -1);
CHECK(errno == EFAULT);
}

TEST_CASE("ppoll") {
// ppoll can return EFAULT for arguments 1, 3, 4.
// Using mmap specifically for allocating with PROT_NONE.
struct pollfd *invalid_fds = reinterpret_cast<struct pollfd *>(mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
struct timespec *invalid_timespec = reinterpret_cast<struct timespec*>(mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
sigset_t *invalid_sigset = reinterpret_cast<sigset_t*>(mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));

SECTION("invalid fds") {
auto ret = ::syscall(SYS_ppoll, invalid_fds, 1, 0, nullptr, nullptr);
REQUIRE(ret == -1);
CHECK(errno == EFAULT);
}

SECTION("invalid timespec") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};
auto ret = ::syscall(SYS_ppoll, &valid_fds, 1, invalid_timespec, nullptr, sizeof(uint64_t));
REQUIRE(ret == -1);
CHECK(errno == EFAULT);
}

SECTION("invalid sigset") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};

struct timespec valid_ts{};
auto ret = ::syscall(SYS_ppoll, &valid_fds, 1, &valid_ts, invalid_sigset, sizeof(uint64_t));
REQUIRE(ret == -1);
CHECK(errno == EFAULT);
}

SECTION("valid configuration") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};

struct timespec valid_ts{};
sigset_t valid_sigset{};
sigemptyset(&valid_sigset);
auto ret = ::syscall(SYS_ppoll, &valid_fds, 1, &valid_ts, &valid_sigset, sizeof(uint64_t));
REQUIRE(ret == 0);
}

SECTION("invalid timespec write-back") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};

// Kernel will read timespec, but it then can't write the result back.
mprotect(invalid_timespec, sysconf(_SC_PAGESIZE), PROT_READ | PROT_WRITE);
invalid_timespec->tv_sec = 1;
mprotect(invalid_timespec, sysconf(_SC_PAGESIZE), PROT_READ);

sigset_t valid_sigset{};
sigemptyset(&valid_sigset);
auto ret = ::syscall(SYS_ppoll, &valid_fds, 1, invalid_timespec, &valid_sigset, sizeof(uint64_t));
REQUIRE(ret == 0);
CHECK(invalid_timespec->tv_sec == 1);
}
}

struct timespec64 {
uint64_t tv_sec, tv_nsec;
};

static const timespec64 readonly_ts {
.tv_sec = 1,
.tv_nsec = 0,
};


TEST_CASE("ppoll_64") {
#ifndef SYS_ppoll_time64
#define SYS_ppoll_time64 SYS_ppoll
#endif
// ppoll can return EFAULT for arguments 1, 3, 4
// Using mmap specifically for allocating with PROT_NONE.
struct pollfd *invalid_fds = reinterpret_cast<struct pollfd *>(mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
timespec64 *invalid_timespec = reinterpret_cast<timespec64*>(mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
sigset_t *invalid_sigset = reinterpret_cast<sigset_t*>(mmap(nullptr, sysconf(_SC_PAGESIZE), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));

SECTION("invalid fds") {
auto ret = ::syscall(SYS_ppoll_time64, invalid_fds, 1, 0, nullptr, nullptr);
REQUIRE(ret == -1);
CHECK(errno == EFAULT);
}

SECTION("invalid timespec") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};
auto ret = ::syscall(SYS_ppoll_time64, &valid_fds, 1, invalid_timespec, nullptr, sizeof(uint64_t));
REQUIRE(ret == -1);
CHECK(errno == EFAULT);
}

SECTION("invalid sigset") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};

timespec64 valid_ts{};
auto ret = ::syscall(SYS_ppoll_time64, &valid_fds, 1, &valid_ts, invalid_sigset, sizeof(uint64_t));
REQUIRE(ret == -1);
CHECK(errno == EFAULT);
}

SECTION("valid configuration") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};

timespec64 valid_ts{};
sigset_t valid_sigset{};
sigemptyset(&valid_sigset);
auto ret = ::syscall(SYS_ppoll_time64, &valid_fds, 1, &valid_ts, &valid_sigset, sizeof(uint64_t));
REQUIRE(ret == 0);
}

SECTION("invalid timespec write-back") {
struct pollfd valid_fds {
.fd = STDOUT_FILENO,
.events = 0,
.revents = 0,
};

// Kernel will read timespec, but it then can't write the result back.
sigset_t valid_sigset{};
sigemptyset(&valid_sigset);
auto ret = ::syscall(SYS_ppoll_time64, &valid_fds, 1, &readonly_ts, &valid_sigset, sizeof(uint64_t));
REQUIRE(ret == 0);
CHECK(readonly_ts.tv_sec == 1);
}
}

0 comments on commit 8e3d4a3

Please sign in to comment.