Merge pull request #1686 from Cloud-Multiplayer/feat/macos-write-watch
feat(libc): write watch on macOS hosts
This commit is contained in:
commit
e0526f57cd
|
|
@ -175,6 +175,19 @@ jobs:
|
|||
- run: cmake --build build/register-context --parallel
|
||||
- run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error
|
||||
|
||||
write-watch:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-15-intel, macos-15]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release
|
||||
- run: cmake --build build/write-watch --parallel
|
||||
- run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error
|
||||
|
||||
linux:
|
||||
needs: eligibility
|
||||
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,10 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
if(APPLE)
|
||||
add_subdirectory(tests/write_watch)
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
|
||||
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes)
|
|||
bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t bytes);
|
||||
bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes);
|
||||
bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context);
|
||||
void GuestWriteWatchBeginHostWrite_nid_postfix(const void* pointer, std::size_t bytes);
|
||||
void GuestWriteWatchEndHostWrite_nid_postfix(const void* pointer, std::size_t bytes);
|
||||
void GuestWriteWatchProtectionChanged_nid_postfix(const void* pointer, std::size_t bytes, int protection);
|
||||
int GuestWriteWatchProtection_nid_postfix(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libc/include/WindowsMappings.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
|
|
@ -276,8 +277,7 @@ bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes) {
|
|||
#ifdef _WIN32
|
||||
return WindowsMappings::Get().BeginHostWrite(reinterpret_cast<std::uintptr_t>(pointer), bytes);
|
||||
#else
|
||||
(void)pointer;
|
||||
(void)bytes;
|
||||
GuestWriteWatch::GuestWriteWatchBeginHostWrite_nid_postfix(pointer, bytes);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
|
@ -286,8 +286,7 @@ void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes) {
|
|||
#ifdef _WIN32
|
||||
WindowsMappings::Get().EndHostWrite(reinterpret_cast<std::uintptr_t>(pointer), bytes);
|
||||
#else
|
||||
(void)pointer;
|
||||
(void)bytes;
|
||||
GuestWriteWatch::GuestWriteWatchEndHostWrite_nid_postfix(pointer, bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,22 @@
|
|||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#include <sched.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if (defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC)) || defined(__APPLE__)
|
||||
#define APS5_GUEST_WRITE_WATCH 1
|
||||
#endif
|
||||
|
||||
namespace GuestWriteWatch {
|
||||
namespace {
|
||||
|
||||
|
|
@ -249,13 +265,294 @@ private:
|
|||
std::map<std::uintptr_t, std::uintptr_t> _fresh;
|
||||
};
|
||||
|
||||
const bool g_opened = (Watch::Get(), true);
|
||||
#elif defined(__APPLE__)
|
||||
class Watch {
|
||||
public:
|
||||
static Watch& Get() {
|
||||
static Watch watch;
|
||||
return watch;
|
||||
}
|
||||
|
||||
bool Available() const {
|
||||
return _available;
|
||||
}
|
||||
|
||||
void Register(std::uintptr_t begin, std::uintptr_t end) {
|
||||
begin = pageStart(begin);
|
||||
end = pageEnd(end);
|
||||
if (!_available || end <= begin) return;
|
||||
std::vector<std::uint8_t> pages((end - begin) / _page, Written);
|
||||
for (auto cursor = begin; cursor < end;) {
|
||||
mach_vm_address_t address = cursor;
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_basic_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
mach_port_t object = MACH_PORT_NULL;
|
||||
if (mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object) != KERN_SUCCESS || address >= end) break;
|
||||
const auto first = std::max<std::uintptr_t>(cursor, address);
|
||||
const auto last = std::min<std::uintptr_t>(end, address + size);
|
||||
const std::uint8_t protection = ((info.protection & VM_PROT_READ) ? PROT_READ : 0) | ((info.protection & VM_PROT_WRITE) ? PROT_WRITE : 0) | ((info.protection & VM_PROT_EXECUTE) ? PROT_EXEC : 0);
|
||||
for (auto page = first; page < last; page += _page) pages[(page - begin) / _page] = Written | protection;
|
||||
cursor = last;
|
||||
}
|
||||
const Lock lock(_lock);
|
||||
forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) {
|
||||
pages[(page - begin) / _page] = static_cast<std::uint8_t>(Written | (state & ProtectionMask));
|
||||
});
|
||||
erase(begin, end);
|
||||
_ranges.emplace(begin, Range{end, std::move(pages)});
|
||||
}
|
||||
|
||||
bool Unregister(std::uintptr_t begin, std::uintptr_t end) {
|
||||
begin = pageStart(begin);
|
||||
end = pageEnd(end);
|
||||
if (!_available || end <= begin) return false;
|
||||
const Lock lock(_lock);
|
||||
return erase(begin, end);
|
||||
}
|
||||
|
||||
bool Covers(std::uintptr_t begin, std::uintptr_t end) {
|
||||
if (!_available) return false;
|
||||
const Lock lock(_lock);
|
||||
return covers(begin, end);
|
||||
}
|
||||
|
||||
bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context) {
|
||||
if (!_available) return false;
|
||||
begin = pageStart(begin);
|
||||
end = pageEnd(end);
|
||||
if (end <= begin) return true;
|
||||
std::vector<std::pair<std::uintptr_t, std::uintptr_t>> runs;
|
||||
{
|
||||
const Lock lock(_lock);
|
||||
if (!covers(begin, end)) return false;
|
||||
forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) {
|
||||
if ((state & Written) == 0) return;
|
||||
if (!runs.empty() && runs.back().second == page) runs.back().second = page + _page;
|
||||
else runs.emplace_back(page, page + _page);
|
||||
for (const auto& [first, last] : _hostWrites)
|
||||
if (page >= first && page < last) return;
|
||||
state &= static_cast<std::uint8_t>(~Written);
|
||||
if ((state & PROT_WRITE) != 0) state |= Protected;
|
||||
});
|
||||
for (const auto& [first, last] : runs) protect(first, last);
|
||||
}
|
||||
for (const auto& [first, last] : runs) written(context, first, last);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProtectionChanged(std::uintptr_t begin, std::uintptr_t end, int protection) {
|
||||
begin = pageStart(begin);
|
||||
end = pageEnd(end);
|
||||
if (!_available || end <= begin) return;
|
||||
const Lock lock(_lock);
|
||||
forEachPage(begin, end, [&](std::uintptr_t, std::uint8_t& state) {
|
||||
state = static_cast<std::uint8_t>(Written | (protection & ProtectionMask));
|
||||
});
|
||||
}
|
||||
|
||||
void BeginHostWrite(std::uintptr_t begin, std::uintptr_t end) {
|
||||
begin = pageStart(begin);
|
||||
end = pageEnd(end);
|
||||
if (!_available || end <= begin) return;
|
||||
const Lock lock(_lock);
|
||||
_hostWrites.emplace_back(begin, end);
|
||||
forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) {
|
||||
if ((state & Protected) != 0) ::mprotect(reinterpret_cast<void*>(page), _page, state & ProtectionMask);
|
||||
state = static_cast<std::uint8_t>((state & ProtectionMask) | Written);
|
||||
});
|
||||
}
|
||||
|
||||
void EndHostWrite(std::uintptr_t begin, std::uintptr_t end) {
|
||||
begin = pageStart(begin);
|
||||
end = pageEnd(end);
|
||||
if (!_available || end <= begin) return;
|
||||
const Lock lock(_lock);
|
||||
const auto found = std::find(_hostWrites.begin(), _hostWrites.end(), std::pair{begin, end});
|
||||
if (found != _hostWrites.end()) _hostWrites.erase(found);
|
||||
}
|
||||
|
||||
int Protection(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd) {
|
||||
const auto page = pageStart(address);
|
||||
const Lock lock(_lock);
|
||||
auto it = _ranges.upper_bound(page);
|
||||
if (_available && it != _ranges.begin() && page < std::prev(it)->second.end) {
|
||||
--it;
|
||||
const auto& pages = it->second.pages;
|
||||
const std::uint8_t protection = pages[(page - it->first) / _page] & ProtectionMask;
|
||||
auto end = page + _page;
|
||||
while (end < it->second.end && end < limit && (pages[(end - it->first) / _page] & ProtectionMask) == protection) end += _page;
|
||||
*runEnd = std::min(end, limit);
|
||||
return protection;
|
||||
}
|
||||
*runEnd = it != _ranges.end() ? std::min(it->first, limit) : limit;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::uint8_t ProtectionMask = PROT_READ | PROT_WRITE | PROT_EXEC;
|
||||
static constexpr std::uint8_t Written = 0x10;
|
||||
static constexpr std::uint8_t Protected = 0x20;
|
||||
|
||||
struct Range {
|
||||
std::uintptr_t end;
|
||||
std::vector<std::uint8_t> pages;
|
||||
};
|
||||
|
||||
struct Turns {
|
||||
std::atomic<std::uint64_t> next{0};
|
||||
std::atomic<std::uint64_t> serving{0};
|
||||
};
|
||||
|
||||
struct Lock {
|
||||
explicit Lock(Turns& turns) : _turns(turns), _turn(turns.next.fetch_add(1, std::memory_order_relaxed)) {
|
||||
while (_turns.serving.load(std::memory_order_acquire) != _turn) ::sched_yield();
|
||||
}
|
||||
~Lock() {
|
||||
_turns.serving.store(_turn + 1, std::memory_order_release);
|
||||
}
|
||||
Turns& _turns;
|
||||
std::uint64_t _turn;
|
||||
};
|
||||
|
||||
Watch() {
|
||||
if (std::getenv("APS5_NO_WRITE_WATCH") != nullptr) return;
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = &Watch::fault;
|
||||
action.sa_flags = SA_SIGINFO | SA_RESTART;
|
||||
sigemptyset(&action.sa_mask);
|
||||
if (sigaction(SIGBUS, &action, &_previousBus) != 0 || sigaction(SIGSEGV, &action, &_previousSegv) != 0) {
|
||||
std::fprintf(stderr, "[memory] write watch: cannot install the fault handler (%s); guest buffers are fully re-read every use\n", std::strerror(errno));
|
||||
return;
|
||||
}
|
||||
_available = true;
|
||||
}
|
||||
|
||||
static void fault(int signal, siginfo_t* info, void* context) {
|
||||
auto& watch = Get();
|
||||
if (watch.record(reinterpret_cast<std::uintptr_t>(info->si_addr))) return;
|
||||
const auto& previous = signal == SIGBUS ? watch._previousBus : watch._previousSegv;
|
||||
if ((previous.sa_flags & SA_SIGINFO) != 0 && previous.sa_sigaction != nullptr) {
|
||||
previous.sa_sigaction(signal, info, context);
|
||||
return;
|
||||
}
|
||||
if (previous.sa_handler != SIG_DFL && previous.sa_handler != SIG_IGN && previous.sa_handler != nullptr) {
|
||||
previous.sa_handler(signal);
|
||||
return;
|
||||
}
|
||||
::signal(signal, SIG_DFL);
|
||||
}
|
||||
|
||||
bool record(std::uintptr_t address) {
|
||||
const auto page = pageStart(address);
|
||||
const Lock lock(_lock);
|
||||
auto it = _ranges.upper_bound(page);
|
||||
if (it == _ranges.begin()) return false;
|
||||
--it;
|
||||
if (page >= it->second.end) return false;
|
||||
auto& state = it->second.pages[(page - it->first) / _page];
|
||||
if ((state & Protected) != 0) {
|
||||
state = static_cast<std::uint8_t>((state & ProtectionMask) | Written);
|
||||
::mprotect(reinterpret_cast<void*>(page), _page, state & ProtectionMask);
|
||||
return true;
|
||||
}
|
||||
return (state & PROT_WRITE) != 0;
|
||||
}
|
||||
|
||||
std::uintptr_t pageStart(std::uintptr_t address) const {
|
||||
return address & ~(_page - 1);
|
||||
}
|
||||
|
||||
std::uintptr_t pageEnd(std::uintptr_t address) const {
|
||||
return (address + _page - 1) & ~(_page - 1);
|
||||
}
|
||||
|
||||
template <typename TFunction>
|
||||
void forEachPage(std::uintptr_t begin, std::uintptr_t end, TFunction function) {
|
||||
auto it = _ranges.upper_bound(begin);
|
||||
if (it != _ranges.begin()) --it;
|
||||
for (; it != _ranges.end() && it->first < end; ++it) {
|
||||
const auto first = std::max(begin, it->first);
|
||||
const auto last = std::min(end, it->second.end);
|
||||
for (auto page = first; page < last; page += _page) function(page, it->second.pages[(page - it->first) / _page]);
|
||||
}
|
||||
}
|
||||
|
||||
void protect(std::uintptr_t begin, std::uintptr_t end) {
|
||||
std::uintptr_t runStart = 0;
|
||||
std::uintptr_t previous = begin;
|
||||
int runProtection = -1;
|
||||
const auto flush = [&](std::uintptr_t runEnd) {
|
||||
if (runProtection >= 0) ::mprotect(reinterpret_cast<void*>(runStart), runEnd - runStart, runProtection);
|
||||
runProtection = -1;
|
||||
};
|
||||
forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) {
|
||||
const int wanted = (state & Protected) != 0 ? (state & ProtectionMask & ~PROT_WRITE) : -1;
|
||||
if (wanted != runProtection || page != previous) {
|
||||
flush(previous);
|
||||
runStart = page;
|
||||
runProtection = wanted;
|
||||
}
|
||||
previous = page + _page;
|
||||
});
|
||||
flush(previous);
|
||||
}
|
||||
|
||||
bool covers(std::uintptr_t begin, std::uintptr_t end) const {
|
||||
auto it = _ranges.upper_bound(begin);
|
||||
if (it == _ranges.begin()) return false;
|
||||
--it;
|
||||
auto cursor = begin;
|
||||
for (; it != _ranges.end() && it->first <= cursor; ++it) {
|
||||
if (it->second.end > cursor) cursor = it->second.end;
|
||||
if (cursor >= end) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool erase(std::uintptr_t begin, std::uintptr_t end) {
|
||||
bool removed = false;
|
||||
auto it = _ranges.upper_bound(begin);
|
||||
if (it != _ranges.begin()) --it;
|
||||
while (it != _ranges.end() && it->first < end) {
|
||||
const auto rangeBegin = it->first;
|
||||
const auto rangeEnd = it->second.end;
|
||||
if (rangeEnd <= begin) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
removed = true;
|
||||
auto pages = std::move(it->second.pages);
|
||||
it = _ranges.erase(it);
|
||||
const auto cutBegin = std::max(begin, rangeBegin);
|
||||
const auto cutEnd = std::min(end, rangeEnd);
|
||||
for (auto page = cutBegin; page < cutEnd; page += _page) {
|
||||
const auto state = pages[(page - rangeBegin) / _page];
|
||||
if ((state & Protected) != 0) ::mprotect(reinterpret_cast<void*>(page), _page, state & ProtectionMask);
|
||||
}
|
||||
if (rangeBegin < cutBegin) _ranges.emplace(rangeBegin, Range{cutBegin, std::vector<std::uint8_t>(pages.begin(), pages.begin() + (cutBegin - rangeBegin) / _page)});
|
||||
if (cutEnd < rangeEnd) it = _ranges.emplace(cutEnd, Range{rangeEnd, std::vector<std::uint8_t>(pages.begin() + (cutEnd - rangeBegin) / _page, pages.end())}).first;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
const std::uintptr_t _page = static_cast<std::uintptr_t>(::getpagesize());
|
||||
bool _available = false;
|
||||
Turns _lock;
|
||||
std::map<std::uintptr_t, Range> _ranges;
|
||||
std::vector<std::pair<std::uintptr_t, std::uintptr_t>> _hostWrites;
|
||||
struct sigaction _previousBus {};
|
||||
struct sigaction _previousSegv {};
|
||||
};
|
||||
|
||||
const bool g_opened = (Watch::Get(), true);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
bool GuestWriteWatchAvailable_nid_postfix() {
|
||||
#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC)
|
||||
#ifdef APS5_GUEST_WRITE_WATCH
|
||||
return Watch::Get().Available();
|
||||
#else
|
||||
return false;
|
||||
|
|
@ -263,7 +560,7 @@ bool GuestWriteWatchAvailable_nid_postfix() {
|
|||
}
|
||||
|
||||
void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes) {
|
||||
#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC)
|
||||
#ifdef APS5_GUEST_WRITE_WATCH
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(pointer);
|
||||
Watch::Get().Register(begin, begin + bytes);
|
||||
#else
|
||||
|
|
@ -273,7 +570,7 @@ void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes)
|
|||
}
|
||||
|
||||
bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t bytes) {
|
||||
#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC)
|
||||
#ifdef APS5_GUEST_WRITE_WATCH
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(pointer);
|
||||
return Watch::Get().Unregister(begin, begin + bytes);
|
||||
#else
|
||||
|
|
@ -284,7 +581,7 @@ bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t byte
|
|||
}
|
||||
|
||||
bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes) {
|
||||
#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC)
|
||||
#ifdef APS5_GUEST_WRITE_WATCH
|
||||
return bytes != 0 && address + bytes > address && Watch::Get().Covers(address, address + bytes);
|
||||
#else
|
||||
static_cast<void>(address);
|
||||
|
|
@ -294,7 +591,7 @@ bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes
|
|||
}
|
||||
|
||||
bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context) {
|
||||
#if defined(__linux__) && defined(PAGEMAP_SCAN) && defined(UFFD_FEATURE_WP_ASYNC)
|
||||
#ifdef APS5_GUEST_WRITE_WATCH
|
||||
if (bytes == 0 || address + bytes < address) return false;
|
||||
return Watch::Get().Collect(address, address + bytes, written, context);
|
||||
#else
|
||||
|
|
@ -306,4 +603,45 @@ bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t byte
|
|||
#endif
|
||||
}
|
||||
|
||||
void GuestWriteWatchBeginHostWrite_nid_postfix(const void* pointer, std::size_t bytes) {
|
||||
#if defined(__APPLE__)
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(pointer);
|
||||
Watch::Get().BeginHostWrite(begin, begin + bytes);
|
||||
#else
|
||||
static_cast<void>(pointer);
|
||||
static_cast<void>(bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuestWriteWatchEndHostWrite_nid_postfix(const void* pointer, std::size_t bytes) {
|
||||
#if defined(__APPLE__)
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(pointer);
|
||||
Watch::Get().EndHostWrite(begin, begin + bytes);
|
||||
#else
|
||||
static_cast<void>(pointer);
|
||||
static_cast<void>(bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuestWriteWatchProtectionChanged_nid_postfix(const void* pointer, std::size_t bytes, int protection) {
|
||||
#if defined(__APPLE__)
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(pointer);
|
||||
Watch::Get().ProtectionChanged(begin, begin + bytes, protection);
|
||||
#else
|
||||
static_cast<void>(pointer);
|
||||
static_cast<void>(bytes);
|
||||
static_cast<void>(protection);
|
||||
#endif
|
||||
}
|
||||
|
||||
int GuestWriteWatchProtection_nid_postfix(std::uintptr_t address, std::uintptr_t limit, std::uintptr_t* runEnd) {
|
||||
#if defined(__APPLE__)
|
||||
return Watch::Get().Protection(address, limit, runEnd);
|
||||
#else
|
||||
static_cast<void>(address);
|
||||
*runEnd = limit;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
#error The page protection write watch is the macOS backend
|
||||
#endif
|
||||
|
||||
using namespace GuestWriteWatch;
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct Region {
|
||||
explicit Region(std::size_t pageCount) : page(static_cast<std::size_t>(::getpagesize())), bytes(pageCount * page) {
|
||||
base = static_cast<std::uint8_t*>(::mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0));
|
||||
Require(base != MAP_FAILED, "cannot map the test region");
|
||||
}
|
||||
~Region() {
|
||||
GuestWriteWatchUnregister_nid_postfix(base, bytes);
|
||||
::munmap(base, bytes);
|
||||
}
|
||||
std::uintptr_t Address(std::size_t index) const {
|
||||
return reinterpret_cast<std::uintptr_t>(base) + index * page;
|
||||
}
|
||||
std::set<std::size_t> Collect() {
|
||||
std::set<std::size_t> pages;
|
||||
struct Sink {
|
||||
Region* region;
|
||||
std::set<std::size_t>* pages;
|
||||
} sink{this, &pages};
|
||||
const bool complete = GuestWriteWatchCollect_nid_postfix(Address(0), bytes, [](void* context, std::uintptr_t begin, std::uintptr_t end) {
|
||||
auto& target = *static_cast<Sink*>(context);
|
||||
for (auto at = begin; at < end; at += target.region->page) target.pages->insert((at - target.region->Address(0)) / target.region->page);
|
||||
}, &sink);
|
||||
Require(complete, "collection of a watched range is incomplete");
|
||||
return pages;
|
||||
}
|
||||
std::size_t page;
|
||||
std::size_t bytes;
|
||||
std::uint8_t* base = nullptr;
|
||||
};
|
||||
|
||||
std::set<std::size_t> All(std::size_t count) {
|
||||
std::set<std::size_t> pages;
|
||||
for (std::size_t index = 0; index < count; ++index) pages.insert(index);
|
||||
return pages;
|
||||
}
|
||||
|
||||
void CheckCollect() {
|
||||
Region region(8);
|
||||
Require(GuestWriteWatchAvailable_nid_postfix(), "write watch is unavailable");
|
||||
Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "unregistered range is covered");
|
||||
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
|
||||
Require(GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "registered range is not covered");
|
||||
Require(region.Collect() == All(8), "a new range is not reported in full");
|
||||
Require(region.Collect().empty(), "an untouched range reports writes");
|
||||
region.base[3 * region.page + 17] = 1;
|
||||
region.base[5 * region.page] = 2;
|
||||
region.base[5 * region.page + 9] = 3;
|
||||
Require(region.Collect() == std::set<std::size_t>({3, 5}), "written pages are not reported exactly");
|
||||
Require(region.Collect().empty(), "collected writes are reported again");
|
||||
Require(region.base[3 * region.page + 17] == 1 && region.base[5 * region.page + 9] == 3, "watched writes were lost");
|
||||
std::memset(region.base + region.page - 4, 7, 8);
|
||||
Require(region.Collect() == std::set<std::size_t>({0, 1}), "a write across a page boundary is not reported on both pages");
|
||||
}
|
||||
|
||||
void CheckRegisterAgain() {
|
||||
Region region(4);
|
||||
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
|
||||
Require(region.Collect() == All(4), "a new range is not reported in full");
|
||||
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
|
||||
Require(region.Collect() == All(4), "a range registered again is not reported in full");
|
||||
region.base[2 * region.page] = 1;
|
||||
Require(region.Collect() == std::set<std::size_t>({2}), "a range registered while protected stops reporting writes");
|
||||
GuestWriteWatchRegister_nid_postfix(region.base + region.page, region.page);
|
||||
Require(region.Collect() == std::set<std::size_t>({1}), "registering part of a range does not report that part alone");
|
||||
region.base[0] = 1;
|
||||
region.base[3 * region.page] = 1;
|
||||
Require(region.Collect() == std::set<std::size_t>({0, 3}), "the rest of a split range stops reporting writes");
|
||||
}
|
||||
|
||||
void CheckHostWrite() {
|
||||
Region region(4);
|
||||
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
|
||||
Require(region.Collect() == All(4), "a new range is not reported in full");
|
||||
int descriptors[2];
|
||||
Require(::pipe(descriptors) == 0, "cannot create a pipe");
|
||||
const std::array<char, 8> payload{'w', 'a', 't', 'c', 'h', 'e', 'd', '!'};
|
||||
Require(::write(descriptors[1], payload.data(), payload.size()) == static_cast<ssize_t>(payload.size()), "cannot fill the pipe");
|
||||
errno = 0;
|
||||
Require(::read(descriptors[0], region.base + region.page, payload.size()) == -1 && errno == EFAULT, "a system call wrote a protected page without a host write");
|
||||
GuestWriteWatchBeginHostWrite_nid_postfix(region.base + region.page, payload.size());
|
||||
Require(region.Collect() == std::set<std::size_t>({1}), "a page under a host write is not reported");
|
||||
Require(::read(descriptors[0], region.base + region.page, payload.size()) == static_cast<ssize_t>(payload.size()), "a collection protected a page during a host write");
|
||||
GuestWriteWatchEndHostWrite_nid_postfix(region.base + region.page, payload.size());
|
||||
Require(std::memcmp(region.base + region.page, payload.data(), payload.size()) == 0, "the host write did not reach the page");
|
||||
Require(region.Collect() == std::set<std::size_t>({1}), "a host write is not reported after it ends");
|
||||
Require(region.Collect().empty(), "a finished host write is reported again");
|
||||
::close(descriptors[0]);
|
||||
::close(descriptors[1]);
|
||||
}
|
||||
|
||||
void CheckProtection() {
|
||||
Region region(6);
|
||||
std::uintptr_t runEnd = 0;
|
||||
Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(6), &runEnd) == -1 && runEnd == region.Address(6), "an unwatched range reports a protection");
|
||||
GuestWriteWatchRegister_nid_postfix(region.base + 2 * region.page, 3 * region.page);
|
||||
Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(6), &runEnd) == -1 && runEnd == region.Address(2), "the run before a watched range does not end at it");
|
||||
static_cast<void>(GuestWriteWatchCollect_nid_postfix(region.Address(2), 3 * region.page, [](void*, std::uintptr_t, std::uintptr_t) {}, nullptr));
|
||||
Require(GuestWriteWatchProtection_nid_postfix(region.Address(2), region.Address(6), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(5), "a protected page does not report the protection it was given");
|
||||
Require(::mprotect(region.base + 3 * region.page, region.page, PROT_READ) == 0, "cannot change the protection");
|
||||
GuestWriteWatchProtectionChanged_nid_postfix(region.base + 3 * region.page, region.page, PROT_READ);
|
||||
Require(GuestWriteWatchProtection_nid_postfix(region.Address(2), region.Address(6), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(3), "a run does not end where the protection changes");
|
||||
Require(GuestWriteWatchProtection_nid_postfix(region.Address(3), region.Address(6), &runEnd) == PROT_READ && runEnd == region.Address(4), "a changed protection is not reported");
|
||||
std::set<std::size_t> pages;
|
||||
static_cast<void>(GuestWriteWatchCollect_nid_postfix(region.Address(2), 3 * region.page, [](void* context, std::uintptr_t begin, std::uintptr_t) { static_cast<std::set<std::size_t>*>(context)->insert(begin); }, &pages));
|
||||
Require(pages == std::set<std::size_t>({region.Address(3)}), "a protection change is not reported as a write");
|
||||
Require(::mprotect(region.base + 3 * region.page, region.page, PROT_READ | PROT_WRITE) == 0, "cannot restore the protection");
|
||||
GuestWriteWatchProtectionChanged_nid_postfix(region.base + 3 * region.page, region.page, PROT_READ | PROT_WRITE);
|
||||
region.base[3 * region.page] = 1;
|
||||
GuestWriteWatchUnregister_nid_postfix(region.base + 2 * region.page, 3 * region.page);
|
||||
region.base[2 * region.page] = 1;
|
||||
region.base[4 * region.page] = 1;
|
||||
Require(!GuestWriteWatchCovers_nid_postfix(region.Address(2), region.page), "an unregistered range is covered");
|
||||
}
|
||||
|
||||
void CheckThreads() {
|
||||
constexpr std::size_t Threads = 4;
|
||||
constexpr std::size_t PagesPerThread = 64;
|
||||
constexpr int Rounds = 200;
|
||||
Region region(Threads * PagesPerThread);
|
||||
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
|
||||
Require(region.Collect() == All(Threads * PagesPerThread), "a new range is not reported in full");
|
||||
std::atomic<bool> start{false};
|
||||
std::atomic<std::size_t> finished{0};
|
||||
std::array<std::thread, Threads> writers;
|
||||
for (std::size_t thread = 0; thread < Threads; ++thread) {
|
||||
writers[thread] = std::thread([&, thread] {
|
||||
while (!start.load()) std::this_thread::yield();
|
||||
for (int round = 1; round <= Rounds; ++round)
|
||||
for (std::size_t index = 0; index < PagesPerThread; ++index) region.base[(thread * PagesPerThread + index) * region.page] = static_cast<std::uint8_t>(round);
|
||||
finished.fetch_add(1);
|
||||
});
|
||||
}
|
||||
std::vector<std::uint8_t> seen(Threads * PagesPerThread, 0);
|
||||
const auto take = [&] {
|
||||
for (const auto index : region.Collect()) seen[index] = region.base[index * region.page];
|
||||
};
|
||||
start.store(true);
|
||||
while (finished.load() != Threads) take();
|
||||
for (auto& writer : writers) writer.join();
|
||||
take();
|
||||
for (std::size_t index = 0; index < seen.size(); ++index)
|
||||
Require(seen[index] == static_cast<std::uint8_t>(Rounds), "a write that raced a collection was not reported");
|
||||
Require(region.Collect().empty(), "writes are reported after the writers stopped");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
Require(argc == 2, "expected collect, register, host, protection or threads");
|
||||
const std::string_view mode(argv[1]);
|
||||
if (mode == "collect") CheckCollect();
|
||||
else if (mode == "register") CheckRegisterAgain();
|
||||
else if (mode == "host") CheckHostWrite();
|
||||
else if (mode == "protection") CheckProtection();
|
||||
else if (mode == "threads") CheckThreads();
|
||||
else throw std::runtime_error("unknown write watch test");
|
||||
std::printf("write watch %s passed\n", argv[1]);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
project(WriteWatchTests LANGUAGES CXX)
|
||||
enable_testing()
|
||||
set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
|
||||
endif()
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
add_executable(write_watch_tests ../WriteWatch.cpp ../../prx/libc/src/GuestWriteWatch.cpp)
|
||||
target_include_directories(write_watch_tests PRIVATE ../..)
|
||||
target_compile_features(write_watch_tests PRIVATE cxx_std_20)
|
||||
target_link_libraries(write_watch_tests PRIVATE Threads::Threads)
|
||||
set_target_properties(write_watch_tests PROPERTIES
|
||||
CXX_EXTENSIONS OFF
|
||||
RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>")
|
||||
|
||||
foreach(mode collect register host protection threads)
|
||||
add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode})
|
||||
set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30)
|
||||
endforeach()
|
||||
|
|
@ -74,6 +74,18 @@ Build configuration parameters:
|
|||
|
||||
SDL and FreeType settings forced by the root `CMakeLists.txt` cannot be overridden with `-D`.
|
||||
|
||||
## Write watch tests
|
||||
|
||||
The macOS write watch can be built and tested separately, without submodules or the prx libraries:
|
||||
|
||||
```sh
|
||||
cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64
|
||||
cmake --build build/write-watch
|
||||
ctest --test-dir build/write-watch --output-on-failure
|
||||
```
|
||||
|
||||
With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Rosetta, which is how relinked titles run; without it they run natively. The full `BUILD_TESTING` build on macOS also registers them.
|
||||
|
||||
## Pipeline statistics
|
||||
|
||||
Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newly created graphics or compute pipeline. This requires `VK_KHR_pipeline_executable_properties` and `pipelineExecutableInfo`; an unsupported device fails with an error. Statistic names and units are driver-specific. Capturing statistics can increase pipeline compilation cost. The setting is disabled by default.
|
||||
|
|
|
|||
|
|
@ -266,6 +266,10 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown
|
||||
- [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still passes guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, it misreads and miswrites guest strings. The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported
|
||||
- [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe.
|
||||
- On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so:
|
||||
- a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (the file reads); a host call that writes guest memory without it (sockets, `clock_gettime` and the like into a watched page) fails;
|
||||
- code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree;
|
||||
- the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same.
|
||||
- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented.
|
||||
- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported
|
||||
- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries
|
||||
|
|
|
|||
Loading…
Reference in New Issue