Merge pull request #14417 from roboterspinne/fix/libkernel-aio-efault

fix(libkernel): return EFAULT from AIO poll, cancel and delete for invalid memory
This commit is contained in:
Dean authored and GitHub committed 2026-11-03 11:36:00 +13:00
commit 3ade074e69
4 files changed
+121 -29

No files matched your search

+45 -28
View File
@@ -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<std::size_t>(num) * sizeof(std::int32_t);
std::array<std::int32_t, AioIdNumMax> ids{};
if (!GuestCopyIn_nid_no_patch(ids.data(), guestIds, bytes)) return SCE_KERNEL_ERROR_EFAULT;
std::array<std::int32_t, AioIdNumMax> states{};
{
std::lock_guard<std::mutex> 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<std::mutex> lock(g_mutex);
RequireFinishedLocked(id, __func__);
*ret = DeleteLocked(id);
return 0;
std::int32_t result;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(g_mutex);
RequireFinishedLocked(id, __func__);
*state = CancelLocked(id);
return 0;
std::int32_t result;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(g_mutex);
*state = ReportLocked(id);
return 0;
std::int32_t result;
{
std::lock_guard<std::mutex> 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<std::mutex> 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);
}
}
+71
View File
@@ -19,7 +19,10 @@
#include <vector>
#ifdef _WIN32
#include <io.h>
#include <windows.h>
extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler);
#else
#include <sys/mman.h>
#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<char, 4> 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<std::int32_t*>(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<void*>(unreadable), readOnly}) {
auto* badState = static_cast<std::int32_t*>(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<std::int32_t*>(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);
+4 -1
View File
@@ -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<std::uintptr_t>(&byte);
GuestArena::GuestArenaSetHostReadHook_nid_no_patch(HoldAioSource);
std::thread submitter([&] {
try { Check(sceKernelAioSubmitWriteCommands(&request, 1, 1, &submittedId) == 0); }
+1
View File
@@ -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))