fix(libkernel): answer unknown AIO ids with ESRCH states as a PS5 does

Measured on firmware 13.60: an id that is not a live request, including
one already deleted, makes poll, wait, cancel and delete return 0 and
write 0x80020003 for it; delete makes an id unknown, and cancel leaves a
finished request's state unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
orezraeyandClaude Opus 5.5 committed 2026-10-22 16:57:51 -03:00
1 parent 2730b6abe5
commit 581a951db8
3 files changed
+63 -55

No files matched your search

+31 -36
View File
@@ -61,6 +61,23 @@ void SetState(std::int32_t id, std::int32_t state) {
g_states[id] = state;
}
std::int32_t StateLocked(std::int32_t id) {
return ValidId(id) && g_states[id] != 0 ? g_states[id] : SCE_KERNEL_ERROR_ESRCH;
}
std::int32_t CancelLocked(std::int32_t id) {
const std::int32_t state = StateLocked(id);
if (state != AioProcessing) return state;
g_states[id] = AioAborted;
return AioAborted;
}
std::int32_t DeleteLocked(std::int32_t id) {
if (StateLocked(id) == SCE_KERNEL_ERROR_ESRCH) return SCE_KERNEL_ERROR_ESRCH;
g_states[id] = 0;
return 0;
}
std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int64_t offset) {
const GuestArena::HostWrite destination(buf, nbyte);
if (!destination.Open()) {
@@ -151,9 +168,6 @@ int SubmitCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, std::int3
int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out) {
if (id == nullptr || out == nullptr) return SCE_KERNEL_ERROR_EFAULT;
if (num <= 0 || num > AioIdNumMax) return SCE_KERNEL_ERROR_EINVAL;
for (std::int32_t i = 0; i < num; ++i) {
if (!ValidId(id[i])) return SCE_KERNEL_ERROR_EINVAL;
}
return 0;
}
@@ -163,48 +177,31 @@ extern "C" {
int APS5_VABI sceKernelAioDeleteRequest(int32_t id, int32_t* ret) {
if (ret == nullptr) return SCE_KERNEL_ERROR_EFAULT;
if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL;
SetState(id, AioAborted);
*ret = 0;
std::lock_guard<std::mutex> lock(g_mutex);
*ret = DeleteLocked(id);
return 0;
}
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;
for (int32_t i = 0; i < num; ++i) {
SetState(id[i], AioAborted);
ret[i] = 0;
}
std::lock_guard<std::mutex> lock(g_mutex);
for (int32_t i = 0; i < num; ++i) ret[i] = DeleteLocked(id[i]);
return 0;
}
int APS5_VABI sceKernelAioCancelRequest(int32_t id, int32_t* state) {
if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT;
if (id == 0) {
*state = AioProcessing;
return 0;
}
if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL;
SetState(id, AioAborted);
*state = AioAborted;
std::lock_guard<std::mutex> lock(g_mutex);
*state = CancelLocked(id);
return 0;
}
int APS5_VABI sceKernelAioCancelRequests(int32_t* id, int32_t num, int32_t* state) {
if (id == nullptr || state == nullptr) return SCE_KERNEL_ERROR_EFAULT;
if (num <= 0 || num > AioIdNumMax) return SCE_KERNEL_ERROR_EINVAL;
for (int32_t i = 0; i < num; ++i) {
if (id[i] != 0 && !ValidId(id[i])) return SCE_KERNEL_ERROR_EINVAL;
}
for (int32_t i = 0; i < num; ++i) {
if (id[i] == 0) {
state[i] = AioProcessing;
continue;
}
SetState(id[i], AioAborted);
state[i] = AioAborted;
}
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] = CancelLocked(id[i]);
return 0;
}
@@ -254,9 +251,8 @@ 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;
if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL;
std::lock_guard<std::mutex> lock(g_mutex);
*state = g_states[id];
*state = StateLocked(id);
return 0;
}
@@ -271,7 +267,7 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state,
{
std::lock_guard<std::mutex> lock(g_mutex);
for (int32_t i = 0; i < num; ++i) {
state[i] = g_states[id[i]];
state[i] = StateLocked(id[i]);
if (state[i] == AioProcessing) allDone = false;
if (state[i] == AioCompleted) anyCompleted = true;
}
@@ -287,13 +283,12 @@ int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state,
int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) {
if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT;
if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL;
const auto start = std::chrono::steady_clock::now();
for (;;) {
std::int32_t current;
{
std::lock_guard<std::mutex> lock(g_mutex);
current = g_states[id];
current = StateLocked(id);
}
if (current != AioProcessing) {
*state = current;
@@ -314,7 +309,7 @@ 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] = g_states[id[i]];
for (int32_t i = 0; i < num; ++i) state[i] = StateLocked(id[i]);
return 0;
}
+31 -18
View File
@@ -239,10 +239,14 @@ int main(int argc, char** argv) {
Check(sceKernelAioSubmitReadCommands(nullptr, 1, 1, &id) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioSubmitReadCommands(requests, 0, 1, &id) == SCE_KERNEL_ERROR_EINVAL);
Check(sceKernelAioSubmitReadCommands(requests, 1, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioWaitRequest(9999, &state, nullptr) == SCE_KERNEL_ERROR_EINVAL);
state = 7;
Check(sceKernelAioWaitRequest(9999, &state, nullptr) == 0 && state == SCE_KERNEL_ERROR_ESRCH);
Check(sceKernelAioWaitRequest(id, nullptr, nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioPollRequest(id, nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioPollRequest(0, &polled) == SCE_KERNEL_ERROR_EINVAL);
for (const std::int32_t unknown : {0, -1, 511, 512, 100000}) {
polled = 7;
Check(sceKernelAioPollRequest(unknown, &polled) == 0 && polled == SCE_KERNEL_ERROR_ESRCH);
}
Check(sceKernelAioDeleteRequest(id, nullptr) == SCE_KERNEL_ERROR_EFAULT);
const int batchFd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0);
Check(batchFd >= 0);
@@ -317,24 +321,31 @@ int main(int argc, char** argv) {
Check(readStates[0] == 3 && readStates[1] == 3);
std::int32_t cancelled = 0;
Check(sceKernelAioCancelRequest(readIds[0], &cancelled) == 0);
Check(cancelled == 4);
Check(cancelled == 3);
Check(sceKernelAioPollRequest(readIds[0], &polled) == 0);
Check(polled == 4);
Check(polled == 3);
Check(sceKernelAioCancelRequest(0, &cancelled) == 0);
Check(cancelled == 2);
Check(cancelled == SCE_KERNEL_ERROR_ESRCH);
std::int32_t cancelIds[2] = {0, readIds[1]};
std::int32_t cancelStates[2] = {0, 0};
Check(sceKernelAioCancelRequests(cancelIds, 2, cancelStates) == 0);
Check(cancelStates[0] == 2 && cancelStates[1] == 4);
Check(cancelStates[0] == SCE_KERNEL_ERROR_ESRCH && cancelStates[1] == 3);
Check(sceKernelAioPollRequest(readIds[1], &polled) == 0);
Check(polled == 4);
Check(polled == 3);
Check(sceKernelAioPollRequests(readIds, 2, polledStates) == 0);
Check(polledStates[0] == 4 && polledStates[1] == 4);
Check(polledStates[0] == 3 && polledStates[1] == 3);
std::int32_t deletedRets[2] = {-1, -1};
Check(sceKernelAioDeleteRequests(writeIds, 2, deletedRets) == 0);
Check(deletedRets[0] == 0 && deletedRets[1] == 0);
Check(sceKernelAioPollRequest(writeIds[0], &polled) == 0);
Check(polled == 4);
Check(polled == SCE_KERNEL_ERROR_ESRCH);
Check(sceKernelAioDeleteRequests(writeIds, 2, deletedRets) == 0);
Check(deletedRets[0] == SCE_KERNEL_ERROR_ESRCH && deletedRets[1] == SCE_KERNEL_ERROR_ESRCH);
std::int32_t deletedRet = 7;
Check(sceKernelAioDeleteRequest(writeIds[0], &deletedRet) == 0 && deletedRet == SCE_KERNEL_ERROR_ESRCH);
std::uint32_t unknownTimeout = 1000;
Check(sceKernelAioWaitRequests(writeIds, 2, polledStates, 1, &unknownTimeout) == 0);
Check(polledStates[0] == SCE_KERNEL_ERROR_ESRCH && polledStates[1] == SCE_KERNEL_ERROR_ESRCH);
Check(sceKernelAioDeleteRequests(writeIds, 0, deletedRets) == SCE_KERNEL_ERROR_EINVAL);
Check(sceKernelClose(batchFd) == 0);
std::int32_t badIds[2] = {readIds[0], 9999};
@@ -355,13 +366,15 @@ int main(int argc, char** argv) {
Check(sceKernelAioWaitRequests(readIds, 2, nullptr, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioWaitRequests(nullptr, 2, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioWaitRequests(readIds, -1, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL);
Check(sceKernelAioWaitRequests(badIds, 2, untouched, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL);
Check(untouched[0] == 7 && untouched[1] == 7);
Check(sceKernelAioWaitRequests(badIds, 2, untouched, 1, nullptr) == 0);
Check(untouched[0] == 3 && untouched[1] == SCE_KERNEL_ERROR_ESRCH);
untouched[0] = untouched[1] = 7;
Check(sceKernelAioPollRequests(readIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioPollRequests(nullptr, 2, readStates) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioPollRequests(readIds, -1, readStates) == SCE_KERNEL_ERROR_EINVAL);
Check(sceKernelAioPollRequests(badIds, 2, untouched) == SCE_KERNEL_ERROR_EINVAL);
Check(untouched[0] == 7 && untouched[1] == 7);
Check(sceKernelAioPollRequests(badIds, 2, untouched) == 0);
Check(untouched[0] == 3 && untouched[1] == SCE_KERNEL_ERROR_ESRCH);
untouched[0] = untouched[1] = 7;
Check(sceKernelAioPollRequests(readIds, 0, untouched) == SCE_KERNEL_ERROR_EINVAL);
Check(sceKernelAioWaitRequests(readIds, 0, untouched, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL);
Check(sceKernelAioCancelRequests(readIds, 0, untouched) == SCE_KERNEL_ERROR_EINVAL);
@@ -381,13 +394,13 @@ int main(int argc, char** argv) {
Check(sceKernelAioCancelRequests(manyIds.data(), 129, manyStates.data()) == SCE_KERNEL_ERROR_EINVAL);
Check(manyStates[0] == 7);
Check(sceKernelAioPollRequests(manyIds.data(), 128, manyStates.data()) == 0 && manyStates[127] == manyStates[0]);
Check(sceKernelAioCancelRequest(9999, &cancelled) == SCE_KERNEL_ERROR_EINVAL);
Check(sceKernelAioCancelRequest(9999, &cancelled) == 0 && cancelled == SCE_KERNEL_ERROR_ESRCH);
Check(sceKernelAioCancelRequest(readIds[0], nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioCancelRequests(badIds, 2, untouched) == SCE_KERNEL_ERROR_EINVAL);
Check(untouched[0] == 7 && untouched[1] == 7);
Check(sceKernelAioCancelRequests(badIds, 2, untouched) == 0);
Check(untouched[0] == 3 && untouched[1] == SCE_KERNEL_ERROR_ESRCH);
Check(sceKernelAioCancelRequests(cancelIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT);
Check(sceKernelAioDeleteRequests(badIds, 2, untouched) == SCE_KERNEL_ERROR_EINVAL);
Check(untouched[0] == 7 && untouched[1] == 7);
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 auto shared = root / "shared.bin";
std::string sharedContents;
+1 -1
View File
@@ -63,7 +63,7 @@
- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console
- [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware
- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified.
- [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted, wait mode 2 returns once one request completed, poll copies every state without waiting). On a PS5 (firmware 13.60) an id that is not a live request (0, -1, 1, 511, 512, 100000) makes every poll, wait, cancel and delete call return 0 and write `0x80020003` as its state; here an id outside 1 to 511 returns `EINVAL` and cancel reports id 0 as processing. The console's ids are not the small numbers used here (it returned 33, 0x2021, 0x4021). Batch sizes, id counts, priorities and wait modes follow the console
- [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - one id per request for the Multiple submits and wait mode 2 returning once one request completed follow shadPS4. Requests run synchronously on submit, so cancel never finds one processing. On a PS5 (firmware 13.60) a state read after a wait has reported it carries bit `0x10000` (`0x10003` for completed); that bit is not set here. The console's ids are not the small numbers used here (it returned 33, 0x2021, 0x4021)
- [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked
- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164)
- [pthread_getprio](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) is unmeasured on the console. NID `aO+WS2xWuo4` is named in the firmware 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libkernel.c). Its return and errno contract follows FreeBSD 11 [thr_getprio.c](https://github.com/freebsd/freebsd-src/blob/671fb7df97631020c42ce55527dc49fffe0656b7/lib/libthr/thread/thr_getprio.c): return the recorded guest priority, or -1 with `ESRCH` for an unknown or finished thread. The live-thread registry lock protects the read from a concurrent join. This reports guest scheduler state, not the host scheduling priority.