diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index e228a480e5..90b425bcc1 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -252,6 +252,28 @@ int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out) { return 0; } +int CopyStateOut(std::int32_t* out, std::int32_t state) { + return GuestCopyOut_nid_no_patch(out, &state, sizeof(state)) ? 0 : SCE_KERNEL_ERROR_EFAULT; +} + +int ApplyToIds(const std::int32_t* guestIds, std::int32_t num, std::int32_t* out, const char* function, bool requireFinished, + std::int32_t (*operation)(std::int32_t)) { + const int error = ValidateIds(guestIds, num, out); + if (error != 0) return error; + const std::size_t bytes = static_cast(num) * sizeof(std::int32_t); + std::array ids{}; + if (!GuestCopyIn_nid_no_patch(ids.data(), guestIds, bytes)) return SCE_KERNEL_ERROR_EFAULT; + std::array states{}; + { + std::lock_guard lock(g_mutex); + if (requireFinished) { + for (std::int32_t i = 0; i < num; ++i) RequireFinishedLocked(ids[i], function); + } + for (std::int32_t i = 0; i < num; ++i) states[i] = operation(ids[i]); + } + return GuestCopyOut_nid_no_patch(out, states.data(), bytes) ? 0 : SCE_KERNEL_ERROR_EFAULT; +} + } extern "C" { @@ -271,36 +293,32 @@ void AioAdvanceIdentityForTest_nid_no_patch(std::int64_t next) { int APS5_VABI sceKernelAioDeleteRequest(int32_t id, int32_t* ret) { if (ret == nullptr) return SCE_KERNEL_ERROR_EFAULT; - std::lock_guard lock(g_mutex); - RequireFinishedLocked(id, __func__); - *ret = DeleteLocked(id); - return 0; + std::int32_t result; + { + std::lock_guard lock(g_mutex); + RequireFinishedLocked(id, __func__); + result = DeleteLocked(id); + } + return CopyStateOut(ret, result); } int APS5_VABI sceKernelAioDeleteRequests(int32_t* id, int32_t num, int32_t* ret) { - const int error = ValidateIds(id, num, ret); - if (error != 0) return error; - std::lock_guard lock(g_mutex); - for (int32_t i = 0; i < num; ++i) RequireFinishedLocked(id[i], __func__); - for (int32_t i = 0; i < num; ++i) ret[i] = DeleteLocked(id[i]); - return 0; + return ApplyToIds(id, num, ret, __func__, true, DeleteLocked); } int APS5_VABI sceKernelAioCancelRequest(int32_t id, int32_t* state) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; - std::lock_guard lock(g_mutex); - RequireFinishedLocked(id, __func__); - *state = CancelLocked(id); - return 0; + std::int32_t result; + { + std::lock_guard lock(g_mutex); + RequireFinishedLocked(id, __func__); + result = CancelLocked(id); + } + return CopyStateOut(state, result); } int APS5_VABI sceKernelAioCancelRequests(int32_t* id, int32_t num, int32_t* state) { - const int error = ValidateIds(id, num, state); - if (error != 0) return error; - std::lock_guard lock(g_mutex); - for (int32_t i = 0; i < num; ++i) RequireFinishedLocked(id[i], __func__); - for (int32_t i = 0; i < num; ++i) state[i] = CancelLocked(id[i]); - return 0; + return ApplyToIds(id, num, state, __func__, true, CancelLocked); } int APS5_VABI sceKernelAioInitializeImpl(void* param, int32_t size) { @@ -349,9 +367,12 @@ int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest* req, i int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; - std::lock_guard lock(g_mutex); - *state = ReportLocked(id); - return 0; + std::int32_t result; + { + std::lock_guard lock(g_mutex); + result = ReportLocked(id); + } + return CopyStateOut(state, result); } int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, uint32_t mode, uint32_t* usec) { @@ -423,11 +444,7 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec } int APS5_VABI sceKernelAioPollRequests(int32_t* id, int32_t num, int32_t* state) { - const int error = ValidateIds(id, num, state); - if (error != 0) return error; - std::lock_guard lock(g_mutex); - for (int32_t i = 0; i < num; ++i) state[i] = ReportLocked(id[i]); - return 0; + return ApplyToIds(id, num, state, __func__, false, ReportLocked); } } diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 83fc668a6e..dff172fd1a 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -19,7 +19,10 @@ #include #ifdef _WIN32 #include +#include extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#else +#include #endif static void Require(bool value, int line) { @@ -35,6 +38,25 @@ static void DeleteAio(std::int32_t id) { Check(sceKernelAioDeleteRequest(id, &removed) == 0 && removed == 0); } +static void* MapReadOnlyPage() { +#ifdef _WIN32 + void* page = VirtualAlloc(nullptr, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READONLY); + Check(page != nullptr); +#else + void* page = mmap(nullptr, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Check(page != MAP_FAILED); +#endif + return page; +} + +static void UnmapReadOnlyPage(void* page) { +#ifdef _WIN32 + Check(VirtualFree(page, 0, MEM_RELEASE) != 0); +#else + Check(munmap(page, 4096) == 0); +#endif +} + #ifdef _WIN32 static int g_parameterReports = 0; static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { @@ -400,6 +422,55 @@ int main(int argc, char** argv) { Check(sceKernelAioDeleteRequests(badIds, 2, untouched) == 0); Check(untouched[0] == 0 && untouched[1] == SCE_KERNEL_ERROR_ESRCH); Check(sceKernelAioDeleteRequests(writeIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); + const int faultFd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Check(faultFd >= 0); + std::array faultBuffer{}; + KernelAioResult faultResult{-1, 0}; + KernelAioRwRequest faultRequest{0, faultBuffer.size(), faultBuffer.data(), &faultResult, faultFd}; + const auto finished = [&] { + std::int32_t requestId = 0; + Check(sceKernelAioSubmitReadCommands(&faultRequest, 1, 1, &requestId) == 0 && faultResult.state == 3); + return requestId; + }; + auto* unreadable = reinterpret_cast(0x10); + std::int32_t kept[2] = {7, 7}; + Check(sceKernelAioPollRequests(unreadable, 0, kept) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelAioPollRequests(unreadable, 2, kept) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioCancelRequests(unreadable, 2, kept) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioDeleteRequests(unreadable, 2, kept) == SCE_KERNEL_ERROR_EFAULT); + Check(kept[0] == 7 && kept[1] == 7); + void* readOnly = MapReadOnlyPage(); + for (void* bad : {static_cast(unreadable), readOnly}) { + auto* badState = static_cast(bad); + const std::int32_t pollId = finished(); + Check(sceKernelAioPollRequest(pollId, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioPollRequest(pollId, &polled) == 0 && polled == 0x10003); + const std::int32_t cancelId = finished(); + Check(sceKernelAioCancelRequest(cancelId, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioPollRequest(cancelId, &polled) == 0 && polled == 3); + const std::int32_t deleteId = finished(); + Check(sceKernelAioDeleteRequest(deleteId, badState) == SCE_KERNEL_ERROR_EFAULT); + std::int32_t removed = 7; + Check(sceKernelAioDeleteRequest(deleteId, &removed) == 0 && removed == SCE_KERNEL_ERROR_ESRCH); + std::int32_t batchIds[2] = {finished(), finished()}; + std::int32_t batchStates[2] = {7, 7}; + Check(sceKernelAioPollRequests(batchIds, 2, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioCancelRequests(batchIds, 2, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioPollRequests(batchIds, 2, batchStates) == 0 && batchStates[0] == 0x10003 && batchStates[1] == 0x10003); + Check(sceKernelAioDeleteRequests(batchIds, 2, badState) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioDeleteRequests(batchIds, 2, batchStates) == 0); + Check(batchStates[0] == SCE_KERNEL_ERROR_ESRCH && batchStates[1] == SCE_KERNEL_ERROR_ESRCH); + DeleteAio(pollId); + DeleteAio(cancelId); + } + Check(*static_cast(readOnly) == 0); + UnmapReadOnlyPage(readOnly); + const std::int32_t twiceId = finished(); + std::int32_t twice[2] = {twiceId, twiceId}; + std::int32_t twiceStates[2] = {7, 7}; + Check(sceKernelAioPollRequests(twice, 2, twiceStates) == 0 && twiceStates[0] == 3 && twiceStates[1] == 0x10003); + Check(sceKernelAioDeleteRequests(twice, 2, twiceStates) == 0 && twiceStates[0] == 0 && twiceStates[1] == SCE_KERNEL_ERROR_ESRCH); + Check(sceKernelClose(faultFd) == 0); const auto shared = root / "shared.bin"; std::string sharedContents; for (char letter : {'A', 'B', 'C', 'D'}) sharedContents.append(1024, letter); diff --git a/core/libs/tests/GuestAioRegistry.cpp b/core/libs/tests/GuestAioRegistry.cpp index 1b165ec7ca..e6e6324361 100644 --- a/core/libs/tests/GuestAioRegistry.cpp +++ b/core/libs/tests/GuestAioRegistry.cpp @@ -160,8 +160,10 @@ static std::condition_variable g_aioGateChanged; static bool g_aioGateEntered = false; static bool g_aioGateReleased = false; static bool g_aioGateFails = false; +static std::uintptr_t g_aioGateSource = 0; -static void HoldAioSource(std::uintptr_t, std::size_t) { +static void HoldAioSource(std::uintptr_t address, std::size_t) { + if (address != g_aioGateSource) return; std::unique_lock lock(g_aioGateMutex); g_aioGateEntered = true; g_aioGateChanged.notify_all(); @@ -180,6 +182,7 @@ static void CheckAioProcessingOwnership(bool fail) { KernelAioRwRequest request{0, 1, &byte, &result, -1}; std::int32_t submittedId = -7; bool submitFailed = false; + g_aioGateSource = reinterpret_cast(&byte); GuestArena::GuestArenaSetHostReadHook_nid_no_patch(HoldAioSource); std::thread submitter([&] { try { Check(sceKernelAioSubmitWriteCommands(&request, 1, 1, &submittedId) == 0); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c14a6b9447..433d490e42 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -77,6 +77,7 @@ - [sceKernelGetExecutableModuleHandle](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - returns the main program's handle, the one `sceKernelGetModuleList` lists first and `sceKernelGetModuleInfo` accepts. The `int(void)` signature and that use follow reSOT's [`load_code_copy`](https://github.com/bystritskiy/rePOP-ps5/blob/4e6fe79ea83b5e3814e1be27c7d2b85c9e1ad22c/port/ps5/src/mmio_ps5.c#L123-L137), which passes the handle to `sceKernelGetModuleInfo` to find its own code segment. The [PS5 3.20 export lists](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c#L3922) have it in libkernel as well as libkernel_sys, so it is exported from libkernel only. The number is AnyPS5's own: a payload process on PS5 firmware 13.60 got its main program as handle 0 from `sceKernelGetModuleList`, and what this call returns on a console was not measured - [sceKernelAvailableFlexibleMemorySize, sceKernelConfiguredFlexibleMemorySize, sceKernelGetPrtAperture](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) validate destination pointers with `GuestCopyOut`: a null pointer returns `SCE_KERNEL_ERROR_EINVAL`, while an unmapped or unwritable pointer returns `SCE_KERNEL_ERROR_EFAULT` (in `sceKernelGetPrtAperture`, sequential copyout leaves the address written if length faults). Console fault behaviour on bad memory is unmeasured. - [sceKernelQueryMemoryProtection and sceKernelIsStack](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) copy each optional output with `GuestCopyOut`: a null output is skipped, while an unmapped or unwritable one returns `SCE_KERNEL_ERROR_EFAULT` as a host-safety fallback. Outputs are written in order (`start`, `end`, then the protection), so when a later one faults the earlier ones are already written. Console fault behaviour on bad memory is unmeasured. +- [sceKernelAioPollRequest, sceKernelAioPollRequests, sceKernelAioCancelRequest, sceKernelAioCancelRequests, sceKernelAioDeleteRequest and sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) copy the ids in with `GuestCopyIn` and the states out with `GuestCopyOut`: a null output still returns `SCE_KERNEL_ERROR_EFAULT`, and an unreadable id array or an unmapped or unwritable output returns it too, as a host-safety fallback. The states are copied out after the registry changed, so when the copy faults a poll has already marked the request reported and a delete has already removed it; a cancel changes nothing. Console fault behaviour on bad memory is unmeasured. `sceKernelAioWaitRequest`, `sceKernelAioWaitRequests` and the submit calls still access the ids, states, requests and results directly. - [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [Anonymous mmap and the flexible budget](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - whether a PS5 charges a plain `MAP_PRIVATE | MAP_ANON` map to the flexible-memory budget is not measured. Here it is not charged: it is placed through `DoMapAnon` and is not added to the flexible ranges, so `sceKernelAvailableFlexibleMemorySize` does not drop and a map is never refused for the flexible budget. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/memory.cpp#L784-L790) maps an anonymous map without `MAP_SYSTEM` as flexible memory on the PS4, which [checks it against and charges it to](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) the flexible budget ([#7120](https://github.com/boykopovar/AnyPS5/issues/7120))