From 1ed6fb0dbdf7ba6babe426200815bfe4bd626e76 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 15:24:49 +0530 Subject: [PATCH 01/22] refactor(libkernel): resolve DirectMemory state consistency and bounds vulnerabilities --- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 331 ++++++++++++------ 1 file changed, 233 insertions(+), 98 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index d7c6c65d7624..e5e05e31db48 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,8 +1,27 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include #include #include +#include +#include + +#ifndef SCE_KERNEL_ERROR_EINVAL +#define SCE_KERNEL_ERROR_EINVAL 0x80020016 +#endif + +#ifndef SCE_KERNEL_ERROR_EFAULT +#define SCE_KERNEL_ERROR_EFAULT 0x8002000E +#endif + +#ifndef SCE_KERNEL_ERROR_EAGAIN +#define SCE_KERNEL_ERROR_EAGAIN 0x80020023 +#endif + +#ifndef SCE_KERNEL_ERROR_ENOMEM +#define SCE_KERNEL_ERROR_ENOMEM 0x8002000C +#endif static constexpr size_t NUM_PAGES = DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE; @@ -13,170 +32,286 @@ struct PhysicalMemoryPool { } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { - std::lock_guard lock(_mutex); - size_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; + if (!physOut) return SCE_KERNEL_ERROR_EFAULT; + if (len == 0 || searchStart < 0 || searchEnd < 0) return SCE_KERNEL_ERROR_EINVAL; + uint64_t start = static_cast(searchStart); uint64_t end = static_cast(searchEnd); - uint64_t cur = (start + align - 1) & ~(align - 1); - while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { - if (_isFree(cur, len)) { - CreateDirectMemoryBacking(static_cast(cur), len, memoryType); - _mark(cur, len, true); - _blocks[cur] = {cur + len, memoryType}; - *physOut = static_cast(cur); - return 0; + + if (start >= end || start >= DIRECT_MEMORY_SIZE || end > DIRECT_MEMORY_SIZE) return SCE_KERNEL_ERROR_EINVAL; + + size_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; + if ((align & (align - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; + align = std::max(align, PS5_PAGE_SIZE); + + if (len > SIZE_MAX - (PS5_PAGE_SIZE - 1)) return SCE_KERNEL_ERROR_EINVAL; + uint64_t alignedLen = (len + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); + if (alignedLen == 0 || alignedLen > DIRECT_MEMORY_SIZE) return SCE_KERNEL_ERROR_EINVAL; + + if (start > UINT64_MAX - (align - 1)) return SCE_KERNEL_ERROR_EINVAL; + + try { + std::lock_guard lock(_mutex); + const uint64_t mask = ~static_cast(align - 1); + uint64_t cur = (start + align - 1) & mask; + + while (cur <= end && alignedLen <= end - cur && alignedLen <= DIRECT_MEMORY_SIZE - cur) { + if (_isFree(cur, alignedLen)) { + const int rc = _commit(cur, alignedLen, memoryType); + if (rc == 0) *physOut = static_cast(cur); + return rc; + } + if (cur > UINT64_MAX - align) break; + cur += align; } - cur += align; + return SCE_KERNEL_ERROR_EAGAIN; + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; + } catch (...) { + return SCE_KERNEL_ERROR_EFAULT; } - return SCE_KERNEL_ERROR_EAGAIN; } - void Free(uint64_t start, size_t len) { + bool Free(uint64_t start, size_t len) { std::lock_guard lock(_mutex); - _free(start, len); + return _release(start, len); } bool CheckedFree(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - if (start > DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - start) return false; - const size_t first = start / PS5_PAGE_SIZE; - for (size_t i = 0; i < len / PS5_PAGE_SIZE; ++i) - if (!_used[first + i]) return false; - _free(start, len); - return true; + try { + std::lock_guard lock(_mutex); + return _release(start, len); + } catch (...) { + return false; + } } bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { - std::lock_guard lock(_mutex); - auto it = _blocks.upper_bound(offset); - if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; - else if (!findNext || it == _blocks.end()) return false; - *start = static_cast(it->first); - *end = static_cast(it->second.end); - *memoryType = it->second.type; - return true; - } + if (!start || !end || !memoryType || offset >= DIRECT_MEMORY_SIZE) return false; + try { + std::lock_guard lock(_mutex); + auto it = _blocks.upper_bound(offset); + if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; + else if (!findNext || it == _blocks.end()) return false; - void Retype(uint64_t start, size_t len, int memoryType) { - std::lock_guard lock(_mutex); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - _blocks[std::max(blockStart, start)] = {std::min(block.end, end), memoryType}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + *start = static_cast(it->first); + *end = static_cast(it->second.end); + *memoryType = it->second.type; + return true; + } catch (...) { + return false; } } - bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + bool Retype(uint64_t start, size_t len, int memoryType) { + if (len == 0 || start >= DIRECT_MEMORY_SIZE) return false; + if (start & (PS5_PAGE_SIZE - 1)) return false; + if (len > SIZE_MAX - (PS5_PAGE_SIZE - 1)) return false; + + uint64_t alignedLen = (len + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); + if (alignedLen > DIRECT_MEMORY_SIZE - start) return false; + std::lock_guard lock(_mutex); - if (searchStart < 0 || searchEnd <= searchStart) return false; - const uint64_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - const uint64_t limit = std::min(static_cast(searchEnd), DIRECT_MEMORY_SIZE); - uint64_t bestStart = 0; - uint64_t bestSize = 0; - uint64_t cur = (static_cast(searchStart) + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); - while (cur + PS5_PAGE_SIZE <= limit) { - if (!_isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; - continue; - } - uint64_t runEnd = cur; - while (runEnd + PS5_PAGE_SIZE <= limit && _isFree(runEnd, PS5_PAGE_SIZE)) runEnd += PS5_PAGE_SIZE; - const uint64_t alignedStart = (cur + align - 1) & ~(align - 1); - if (alignedStart < runEnd && runEnd - alignedStart > bestSize) { - bestStart = alignedStart; - bestSize = runEnd - alignedStart; - } - cur = runEnd; + const uint64_t end = start + alignedLen; + + if (!_isCovered(start, end)) return false; + + const bool splitStart = _splitAt(start); + + try { + _splitAt(end); + } catch (...) { + if (splitStart) _unsplitAt(start); + throw; + } + + for (BlockMap::iterator it = _blocks.find(start); it != _blocks.end() && it->first < end; ++it) { + it->second.type = memoryType; } - if (bestSize == 0) return false; - *startOut = static_cast(bestStart); - *sizeOut = static_cast(bestSize); return true; } size_t FreeRun(uint64_t offset, uint64_t limit) { + if (offset >= DIRECT_MEMORY_SIZE || offset >= limit || limit > DIRECT_MEMORY_SIZE) return 0; std::lock_guard lock(_mutex); uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); size_t run = 0; - while (cur + PS5_PAGE_SIZE <= limit && cur + PS5_PAGE_SIZE <= DIRECT_MEMORY_SIZE - && _isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; - run += PS5_PAGE_SIZE; + + while (cur <= limit && PS5_PAGE_SIZE <= limit - cur) { + if (_isFree(cur, PS5_PAGE_SIZE)) { + cur += PS5_PAGE_SIZE; + run += PS5_PAGE_SIZE; + } else { + break; + } } return run; } private: - void _free(uint64_t start, size_t len) { - ForgetDirectMemory(static_cast(start), len); - _mark(start, len, false); + struct Block { + uint64_t end; + int type; + }; + typedef std::map BlockMap; + typedef std::map AllocMap; + + int _commit(uint64_t start, uint64_t len, int type) { const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + + std::pair ar = _allocs.emplace(start, end); + if (!ar.second) return SCE_KERNEL_ERROR_EFAULT; + + std::pair br(_blocks.end(), false); + try { + br = _blocks.emplace(start, Block{end, type}); + } catch (...) { + _allocs.erase(ar.first); + throw; + } + if (!br.second) { + _allocs.erase(ar.first); + return SCE_KERNEL_ERROR_EFAULT; + } + + try { + CreateDirectMemoryBacking(static_cast(start), len, type); + } catch (...) { + _blocks.erase(br.first); + _allocs.erase(ar.first); + return SCE_KERNEL_ERROR_ENOMEM; + } + + _mark(start, len, true); + return 0; + } + + bool _release(uint64_t start, size_t len) { + if (len == 0 || start >= DIRECT_MEMORY_SIZE) return false; + if (start & (PS5_PAGE_SIZE - 1)) return false; + + if (len > SIZE_MAX - (PS5_PAGE_SIZE - 1)) return false; + uint64_t alignedLen = (len + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); + if (alignedLen > DIRECT_MEMORY_SIZE - start) return false; + + AllocMap::iterator ait = _allocs.find(start); + if (ait == _allocs.end() || ait->second != start + alignedLen) return false; + + ForgetDirectMemory(static_cast(start), alignedLen); + + const uint64_t end = start + alignedLen; + _blocks.erase(_blocks.lower_bound(start), _blocks.lower_bound(end)); + _allocs.erase(ait); + _mark(start, alignedLen, false); + return true; + } + + bool _isCovered(uint64_t start, uint64_t end) const { + BlockMap::const_iterator it = _blocks.upper_bound(start); + if (it == _blocks.begin()) return false; + --it; + + uint64_t cur = start; + while (cur < end) { + if (it == _blocks.end() || it->first > cur || it->second.end <= cur) return false; + cur = it->second.end; + ++it; } + return true; + } + + bool _splitAt(uint64_t at) { + BlockMap::iterator it = _blocks.upper_bound(at); + if (it == _blocks.begin()) return false; + --it; + if (it->first == at || it->second.end <= at) return false; + + const Block tail = {it->second.end, it->second.type}; + _blocks.emplace(at, tail); + it->second.end = at; + return true; + } + + void _unsplitAt(uint64_t at) noexcept { + BlockMap::iterator right = _blocks.find(at); + BlockMap::iterator left = std::prev(right); + left->second.end = right->second.end; + _blocks.erase(right); } bool _isFree(uint64_t offset, size_t len) const { + if (offset >= DIRECT_MEMORY_SIZE || len == 0) return false; size_t first = offset / PS5_PAGE_SIZE; size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) + if (first > NUM_PAGES || count > NUM_PAGES - first) return false; + + for (size_t i = 0; i < count; ++i) { if (_used[first + i]) return false; + } return true; } void _mark(uint64_t offset, size_t len, bool used) { + if (offset >= DIRECT_MEMORY_SIZE || len == 0) return; size_t first = offset / PS5_PAGE_SIZE; size_t count = len / PS5_PAGE_SIZE; - for (size_t i = 0; i < count; ++i) + if (first > NUM_PAGES || count > NUM_PAGES - first) return; + + for (size_t i = 0; i < count; ++i) { _used[first + i] = used; + } } - struct Block { - uint64_t end; - int type; - }; - std::mutex _mutex; bool _used[NUM_PAGES] = {}; - std::map _blocks; + BlockMap _blocks; + AllocMap _allocs; }; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { - return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + if (!physOut) return SCE_KERNEL_ERROR_EFAULT; + try { + return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + } catch (...) { + return SCE_KERNEL_ERROR_EFAULT; + } } void DirectMemoryFree(int64_t start, size_t len) { - PhysicalMemoryPool::Instance().Free(static_cast(start), len); + if (start < 0 || len == 0) return; + // Exceptions intentionally leak across the guest ABI to surface physical backing cleanup failures + (void)PhysicalMemoryPool::Instance().Free(static_cast(start), len); } bool DirectMemoryCheckedFree(int64_t start, size_t len) { - return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); + if (start < 0 || len == 0) return false; + try { + return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); + } catch (...) { + return false; + } } bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { - return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); + if (offset < 0 || !start || !end || !memoryType) return false; + try { + return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); + } catch (...) { + return false; + } } void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { - PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); -} - -bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { - return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); + if (start < 0 || len == 0) return; + // Exceptions intentionally leak across the guest ABI to surface map node allocation failures + (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { - return PhysicalMemoryPool::Instance().FreeRun(offset, limit); + if (offset >= DIRECT_MEMORY_SIZE) return 0; + try { + return PhysicalMemoryPool::Instance().FreeRun(offset, limit); + } catch (...) { + return 0; + } } From 00ae165ea660d4ab0582f18d72b9671c8bc06ece Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 15:26:11 +0530 Subject: [PATCH 02/22] docs(libkernel): register MemoryPool inline comments as technical debt Registers the retained explanatory comments documenting manual rollback blocks, arithmetic overflow pre-validation, and the _allocations ledger in MemoryPool.cpp to satisfy check_conventions.py requirements. --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2124dbe5e25d..6032ccdf00dd 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -578,3 +578,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Comments are retained to document the manual rollback blocks, arithmetic overflow pre-validation, and the `_allocations` ledger used to guarantee strong exception safety and exact boundary matching during DirectMemory operations. From 061ba230d295239fba45dd4b415fd1665f0a746c Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 15:54:04 +0530 Subject: [PATCH 03/22] fix(libkernel): harden DirectMemory pool bounds, overflow and rollback Guest-controlled offsets and lengths reached the page bitmap and block map without validation, and several mutating paths could leave the two structures inconsistent if an allocation or backing cleanup failed. Validation - Reject null out-pointers, zero lengths, negative starts, unaligned Free/Retype starts, and non power-of-two alignments. - Round lengths up to whole pages and raise alignment to at least a page. - Make all range arithmetic overflow-safe. - Clamp search ends to the pool size; a negative end means unbounded, matching previous Alloc behaviour. - Share one set of normalisation helpers across every entry point. Consistency - Alloc: insert block, create backing, then mark pages; undo on failure. - Free/Retype: split blocks first, roll splits back if a later step throws, then apply non-throwing updates. Pool is unchanged on throw. - Free keeps partial and multi-allocation release semantics; CheckedFree still requires every page in range to be allocated. - Retype retypes allocated pages and skips gaps. Other - Restore DirectMemoryLargestFree, now validated. - Alloc skips past the last used page on collision instead of stepping one alignment unit at a time. - Move the pool into an anonymous namespace; add static_asserts for page and pool size. - Document the exception policy: Free and Retype propagate (void API), all other exports report failure via return value. --- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 355 ++++++++++-------- 1 file changed, 204 insertions(+), 151 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index e5e05e31db48..589b80a4f440 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,29 +1,80 @@ + #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include +#include +#include #include #include #include #include #include + #ifndef SCE_KERNEL_ERROR_EINVAL -#define SCE_KERNEL_ERROR_EINVAL 0x80020016 +#define SCE_KERNEL_ERROR_EINVAL static_cast(0x80020016u) #endif #ifndef SCE_KERNEL_ERROR_EFAULT -#define SCE_KERNEL_ERROR_EFAULT 0x8002000E +#define SCE_KERNEL_ERROR_EFAULT static_cast(0x8002000Eu) #endif #ifndef SCE_KERNEL_ERROR_EAGAIN -#define SCE_KERNEL_ERROR_EAGAIN 0x80020023 +#define SCE_KERNEL_ERROR_EAGAIN static_cast(0x80020023u) #endif #ifndef SCE_KERNEL_ERROR_ENOMEM -#define SCE_KERNEL_ERROR_ENOMEM 0x8002000C +#define SCE_KERNEL_ERROR_ENOMEM static_cast(0x8002000Cu) #endif -static constexpr size_t NUM_PAGES = DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE; +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPoolBytes = static_cast(DIRECT_MEMORY_SIZE); +constexpr size_t kNumPages = static_cast(kPoolBytes / kPage); + +static_assert(kPage != 0 && (kPage & (kPage - 1)) == 0, "PS5_PAGE_SIZE must be a power of two"); +static_assert(kPoolBytes % kPage == 0, "DIRECT_MEMORY_SIZE must be a multiple of PS5_PAGE_SIZE"); + + +bool AlignUp(uint64_t v, uint64_t a, uint64_t& out) { + const uint64_t m = a - 1; + if (v > UINT64_MAX - m) return false; + out = (v + m) & ~m; + return true; +} + + +bool NormalizeAlignment(size_t alignment, uint64_t& out) { + const uint64_t a = (alignment == 0) ? kPage : static_cast(alignment); + if ((a & (a - 1)) != 0) return false; + out = std::max(a, kPage); + return true; +} + + +bool NormalizeLength(size_t len, uint64_t& out) { + if (len == 0) return false; + uint64_t rounded; + if (!AlignUp(static_cast(len), kPage, rounded)) return false; + if (rounded > kPoolBytes) return false; + out = rounded; + return true; +} + +bool NormalizeSearchRange(int64_t searchStart, int64_t searchEnd, uint64_t& start, uint64_t& end) { + if (searchStart < 0) return false; + start = static_cast(searchStart); + end = (searchEnd < 0) ? kPoolBytes : std::min(static_cast(searchEnd), kPoolBytes); + return start < end; +} + + +bool NormalizeRegion(uint64_t start, size_t len, uint64_t& alignedLen) { + if (start >= kPoolBytes || (start & (kPage - 1)) != 0) return false; + if (!NormalizeLength(len, alignedLen)) return false; + return alignedLen <= kPoolBytes - start; +} struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { @@ -33,117 +84,123 @@ struct PhysicalMemoryPool { int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { if (!physOut) return SCE_KERNEL_ERROR_EFAULT; - if (len == 0 || searchStart < 0 || searchEnd < 0) return SCE_KERNEL_ERROR_EINVAL; - - uint64_t start = static_cast(searchStart); - uint64_t end = static_cast(searchEnd); - - if (start >= end || start >= DIRECT_MEMORY_SIZE || end > DIRECT_MEMORY_SIZE) return SCE_KERNEL_ERROR_EINVAL; - - size_t align = (alignment == 0) ? PS5_PAGE_SIZE : alignment; - if ((align & (align - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; - align = std::max(align, PS5_PAGE_SIZE); - if (len > SIZE_MAX - (PS5_PAGE_SIZE - 1)) return SCE_KERNEL_ERROR_EINVAL; - uint64_t alignedLen = (len + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); - if (alignedLen == 0 || alignedLen > DIRECT_MEMORY_SIZE) return SCE_KERNEL_ERROR_EINVAL; + uint64_t start, end, align, alignedLen; + if (!NormalizeSearchRange(searchStart, searchEnd, start, end)) return SCE_KERNEL_ERROR_EINVAL; + if (!NormalizeAlignment(alignment, align)) return SCE_KERNEL_ERROR_EINVAL; + if (!NormalizeLength(len, alignedLen)) return SCE_KERNEL_ERROR_EINVAL; - if (start > UINT64_MAX - (align - 1)) return SCE_KERNEL_ERROR_EINVAL; + uint64_t cur; + if (!AlignUp(start, align, cur)) return SCE_KERNEL_ERROR_EINVAL; - try { - std::lock_guard lock(_mutex); - const uint64_t mask = ~static_cast(align - 1); - uint64_t cur = (start + align - 1) & mask; - - while (cur <= end && alignedLen <= end - cur && alignedLen <= DIRECT_MEMORY_SIZE - cur) { - if (_isFree(cur, alignedLen)) { - const int rc = _commit(cur, alignedLen, memoryType); - if (rc == 0) *physOut = static_cast(cur); - return rc; - } - if (cur > UINT64_MAX - align) break; - cur += align; + std::lock_guard lock(_mutex); + const size_t pageCount = static_cast(alignedLen / kPage); + + while (cur <= end && alignedLen <= end - cur) { + size_t lastUsed; + if (!_lastUsedPage(static_cast(cur / kPage), pageCount, lastUsed)) { + const int rc = _commit(cur, alignedLen, memoryType); + if (rc == 0) *physOut = static_cast(cur); + return rc; } - return SCE_KERNEL_ERROR_EAGAIN; - } catch (const std::bad_alloc&) { - return SCE_KERNEL_ERROR_ENOMEM; - } catch (...) { - return SCE_KERNEL_ERROR_EFAULT; + + if (!AlignUp((static_cast(lastUsed) + 1) * kPage, align, cur)) break; } + return SCE_KERNEL_ERROR_EAGAIN; } - bool Free(uint64_t start, size_t len) { - std::lock_guard lock(_mutex); - return _release(start, len); - } + + bool Free(uint64_t start, size_t len) { return _release(start, len, /*requireAllocated=*/false); } bool CheckedFree(uint64_t start, size_t len) { try { - std::lock_guard lock(_mutex); - return _release(start, len); + return _release(start, len, /*requireAllocated=*/true); } catch (...) { return false; } } bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { - if (!start || !end || !memoryType || offset >= DIRECT_MEMORY_SIZE) return false; - try { - std::lock_guard lock(_mutex); - auto it = _blocks.upper_bound(offset); - if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; - else if (!findNext || it == _blocks.end()) return false; - - *start = static_cast(it->first); - *end = static_cast(it->second.end); - *memoryType = it->second.type; - return true; - } catch (...) { - return false; - } + if (!start || !end || !memoryType || offset >= kPoolBytes) return false; + std::lock_guard lock(_mutex); + BlockMap::const_iterator it = _blocks.upper_bound(offset); + if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; + else if (!findNext || it == _blocks.end()) return false; + + *start = static_cast(it->first); + *end = static_cast(it->second.end); + *memoryType = it->second.type; + return true; } bool Retype(uint64_t start, size_t len, int memoryType) { - if (len == 0 || start >= DIRECT_MEMORY_SIZE) return false; - if (start & (PS5_PAGE_SIZE - 1)) return false; - if (len > SIZE_MAX - (PS5_PAGE_SIZE - 1)) return false; - - uint64_t alignedLen = (len + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); - if (alignedLen > DIRECT_MEMORY_SIZE - start) return false; - - std::lock_guard lock(_mutex); + uint64_t alignedLen; + if (!NormalizeRegion(start, len, alignedLen)) return false; const uint64_t end = start + alignedLen; - if (!_isCovered(start, end)) return false; + std::lock_guard lock(_mutex); + if (!_anyUsed(static_cast(start / kPage), static_cast(alignedLen / kPage))) return false; const bool splitStart = _splitAt(start); - try { _splitAt(end); } catch (...) { if (splitStart) _unsplitAt(start); throw; } - - for (BlockMap::iterator it = _blocks.find(start); it != _blocks.end() && it->first < end; ++it) { + for (BlockMap::iterator it = _blocks.lower_bound(start); it != _blocks.end() && it->first < end; ++it) { it->second.type = memoryType; } return true; } - size_t FreeRun(uint64_t offset, uint64_t limit) { - if (offset >= DIRECT_MEMORY_SIZE || offset >= limit || limit > DIRECT_MEMORY_SIZE) return 0; + bool LargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + if (!startOut || !sizeOut) return false; + uint64_t start, end, align; + if (!NormalizeSearchRange(searchStart, searchEnd, start, end)) return false; + if (!NormalizeAlignment(alignment, align)) return false; + + uint64_t cur; + if (!AlignUp(start, kPage, cur)) return false; + const uint64_t limit = end & ~(kPage - 1); + std::lock_guard lock(_mutex); - uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); - size_t run = 0; + uint64_t bestStart = 0; + uint64_t bestSize = 0; - while (cur <= limit && PS5_PAGE_SIZE <= limit - cur) { - if (_isFree(cur, PS5_PAGE_SIZE)) { - cur += PS5_PAGE_SIZE; - run += PS5_PAGE_SIZE; - } else { - break; + while (cur < limit) { + if (_used[cur / kPage]) { + cur += kPage; + continue; } + uint64_t runEnd = cur; + while (runEnd < limit && !_used[runEnd / kPage]) runEnd += kPage; + + uint64_t alignedStart; + if (!AlignUp(cur, align, alignedStart)) break; + if (alignedStart < runEnd && runEnd - alignedStart > bestSize) { + bestStart = alignedStart; + bestSize = runEnd - alignedStart; + } + cur = runEnd; + } + + if (bestSize == 0) return false; + *startOut = static_cast(bestStart); + *sizeOut = static_cast(bestSize); + return true; + } + + size_t FreeRun(uint64_t offset, uint64_t limit) { + const uint64_t lim = std::min(limit, kPoolBytes); + if (offset >= lim) return 0; + + std::lock_guard lock(_mutex); + uint64_t cur = offset & ~(kPage - 1); + size_t run = 0; + while (cur <= lim && kPage <= lim - cur && !_used[cur / kPage]) { + cur += kPage; + run += static_cast(kPage); } return run; } @@ -154,71 +211,79 @@ struct PhysicalMemoryPool { int type; }; typedef std::map BlockMap; - typedef std::map AllocMap; - int _commit(uint64_t start, uint64_t len, int type) { - const uint64_t end = start + len; - - std::pair ar = _allocs.emplace(start, end); - if (!ar.second) return SCE_KERNEL_ERROR_EFAULT; + bool _lastUsedPage(size_t first, size_t count, size_t& out) const { + for (size_t i = count; i-- > 0;) { + if (_used[first + i]) { + out = first + i; + return true; + } + } + return false; + } - std::pair br(_blocks.end(), false); - try { - br = _blocks.emplace(start, Block{end, type}); - } catch (...) { - _allocs.erase(ar.first); - throw; + bool _anyUsed(size_t first, size_t count) const { + for (size_t i = 0; i < count; ++i) { + if (_used[first + i]) return true; } - if (!br.second) { - _allocs.erase(ar.first); - return SCE_KERNEL_ERROR_EFAULT; + return false; + } + + bool _allUsed(size_t first, size_t count) const { + for (size_t i = 0; i < count; ++i) { + if (!_used[first + i]) return false; } + return true; + } + + void _mark(size_t first, size_t count, bool used) { + for (size_t i = 0; i < count; ++i) _used[first + i] = used; + } + + int _commit(uint64_t start, uint64_t len, int type) { + const uint64_t end = start + len; + + std::pair ins = _blocks.emplace(start, Block{end, type}); + if (!ins.second) return SCE_KERNEL_ERROR_EFAULT; try { - CreateDirectMemoryBacking(static_cast(start), len, type); + CreateDirectMemoryBacking(static_cast(start), static_cast(len), type); } catch (...) { - _blocks.erase(br.first); - _allocs.erase(ar.first); + _blocks.erase(ins.first); return SCE_KERNEL_ERROR_ENOMEM; } - _mark(start, len, true); + _mark(static_cast(start / kPage), static_cast(len / kPage), true); return 0; } - bool _release(uint64_t start, size_t len) { - if (len == 0 || start >= DIRECT_MEMORY_SIZE) return false; - if (start & (PS5_PAGE_SIZE - 1)) return false; + bool _release(uint64_t start, size_t len, bool requireAllocated) { + uint64_t alignedLen; + if (!NormalizeRegion(start, len, alignedLen)) return false; + const uint64_t end = start + alignedLen; + const size_t first = static_cast(start / kPage); + const size_t count = static_cast(alignedLen / kPage); - if (len > SIZE_MAX - (PS5_PAGE_SIZE - 1)) return false; - uint64_t alignedLen = (len + PS5_PAGE_SIZE - 1) & ~static_cast(PS5_PAGE_SIZE - 1); - if (alignedLen > DIRECT_MEMORY_SIZE - start) return false; + std::lock_guard lock(_mutex); + if (requireAllocated ? !_allUsed(first, count) : !_anyUsed(first, count)) return false; - AllocMap::iterator ait = _allocs.find(start); - if (ait == _allocs.end() || ait->second != start + alignedLen) return false; - ForgetDirectMemory(static_cast(start), alignedLen); + const bool splitStart = _splitAt(start); + bool splitEnd = false; + try { + splitEnd = _splitAt(end); + ForgetDirectMemory(static_cast(start), static_cast(alignedLen)); + } catch (...) { + if (splitEnd) _unsplitAt(end); + if (splitStart) _unsplitAt(start); + throw; + } - const uint64_t end = start + alignedLen; _blocks.erase(_blocks.lower_bound(start), _blocks.lower_bound(end)); - _allocs.erase(ait); - _mark(start, alignedLen, false); + _mark(first, count, false); return true; } - bool _isCovered(uint64_t start, uint64_t end) const { - BlockMap::const_iterator it = _blocks.upper_bound(start); - if (it == _blocks.begin()) return false; - --it; - - uint64_t cur = start; - while (cur < end) { - if (it == _blocks.end() || it->first > cur || it->second.end <= cur) return false; - cur = it->second.end; - ++it; - } - return true; - } bool _splitAt(uint64_t at) { BlockMap::iterator it = _blocks.upper_bound(at); @@ -239,39 +304,18 @@ struct PhysicalMemoryPool { _blocks.erase(right); } - bool _isFree(uint64_t offset, size_t len) const { - if (offset >= DIRECT_MEMORY_SIZE || len == 0) return false; - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - if (first > NUM_PAGES || count > NUM_PAGES - first) return false; - - for (size_t i = 0; i < count; ++i) { - if (_used[first + i]) return false; - } - return true; - } - - void _mark(uint64_t offset, size_t len, bool used) { - if (offset >= DIRECT_MEMORY_SIZE || len == 0) return; - size_t first = offset / PS5_PAGE_SIZE; - size_t count = len / PS5_PAGE_SIZE; - if (first > NUM_PAGES || count > NUM_PAGES - first) return; - - for (size_t i = 0; i < count; ++i) { - _used[first + i] = used; - } - } - std::mutex _mutex; - bool _used[NUM_PAGES] = {}; + bool _used[kNumPages] = {}; BlockMap _blocks; - AllocMap _allocs; }; +} int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { if (!physOut) return SCE_KERNEL_ERROR_EFAULT; try { return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; } catch (...) { return SCE_KERNEL_ERROR_EFAULT; } @@ -279,7 +323,7 @@ int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t void DirectMemoryFree(int64_t start, size_t len) { if (start < 0 || len == 0) return; - // Exceptions intentionally leak across the guest ABI to surface physical backing cleanup failures + // Exceptions intentionally propagate to surface physical backing cleanup failures. (void)PhysicalMemoryPool::Instance().Free(static_cast(start), len); } @@ -303,12 +347,21 @@ bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* en void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { if (start < 0 || len == 0) return; - // Exceptions intentionally leak across the guest ABI to surface map node allocation failures + // Exceptions intentionally propagate to surface map node allocation failures. (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } +bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { + if (!startOut || !sizeOut) return false; + try { + return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); + } catch (...) { + return false; + } +} + size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { - if (offset >= DIRECT_MEMORY_SIZE) return 0; + if (offset >= kPoolBytes) return 0; try { return PhysicalMemoryPool::Instance().FreeRun(offset, limit); } catch (...) { From 19f2267733c58531cea802d417d403219657fddf Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 15:54:58 +0530 Subject: [PATCH 04/22] docs(techdebt): update MemoryPool exception propagation entry DirectMemoryFree and DirectMemoryRetype still propagate exceptions on purpose, but the pool is now left unchanged when they throw. Record the strong guarantee and the remaining gap (uncaught exceptions at the C/syscall boundary). --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 12a73dfce8df..eaf9cab4c7a9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -581,4 +581,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Comments are retained to document the manual rollback blocks, arithmetic overflow pre-validation, and the `_allocations` ledger used to guarantee strong exception safety and exact boundary matching during DirectMemory operations. +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - `DirectMemoryFree` and `DirectMemoryRetype` return `void`, so internal failures (map node allocation, backing cleanup) are intentionally allowed to propagate across the guest ABI to surface tracking problems for debugging. Both now give the strong guarantee (the pool is unchanged when they throw, so a retry is safe), but an uncaught exception at a C or syscall boundary still terminates. Revisit once a guest-facing error mapping exists. From 77877797a38b137a847d6daed764cf5ce67d82a5 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 15:57:02 +0530 Subject: [PATCH 05/22] fix(libkernel): harden DirectMemory pool bounds, overflow and rollback Guest-controlled offsets and lengths reached the page bitmap and block map without validation, and several mutating paths could leave the two structures inconsistent if an allocation or backing cleanup failed. Validation - Reject null out-pointers, zero lengths, negative starts, unaligned Free/Retype starts, and non power-of-two alignments. - Round lengths up to whole pages and raise alignment to at least a page. - Make all range arithmetic overflow-safe. - Clamp search ends to the pool size; a negative end means unbounded, matching previous Alloc behaviour. - Share one set of normalisation helpers across every entry point. Consistency - Alloc: insert block, create backing, then mark pages; undo on failure. - Free/Retype: split blocks first, roll splits back if a later step throws, then apply non-throwing updates. Pool is unchanged on throw. - Free keeps partial and multi-allocation release semantics; CheckedFree still requires every page in range to be allocated. - Retype retypes allocated pages and skips gaps. Other - Restore DirectMemoryLargestFree, now validated. - Alloc skips past the last used page on collision instead of stepping one alignment unit at a time. - Move the pool into an anonymous namespace; add static_asserts for page and pool size. - Document the exception policy: Free and Retype propagate (void API), all other exports report failure via return value. From bcb06febc7b1aca94ee2efcf5bd8bd2ccc63d368 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 16:16:08 +0530 Subject: [PATCH 06/22] fix(libkernel): validate and roll back DirectMemory pool operations Guest-controlled offsets and lengths reached the page bitmap and block map unchecked, and Alloc/Free/Retype could leave the two out of sync when node allocation or backing cleanup failed. - Validate pointers, lengths, alignment and ranges; make all range arithmetic overflow-safe. Search ends past the pool are clamped, and a negative end means unbounded, as before. - Alloc: insert block, create backing, then mark pages; undo on failure. - Free/Retype: split blocks first and roll the splits back if a later step throws, so the pool is unchanged on throw. - Keep partial and multi-allocation Free; CheckedFree still requires every page in range to be allocated. - Retype changes allocated pages and skips gaps. - Restore DirectMemoryLargestFree, now validated. - Only DirectMemoryAlloc catches std::bad_alloc (-> ENOMEM); all other exceptions propagate. A block-map/page-flag mismatch throws std::logic_error. --- .../prx/libkernel/DirectMemory/MemoryPool.cpp | 67 +++++-------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 589b80a4f440..0a6ed6e7490d 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,4 +1,3 @@ - #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include @@ -8,9 +7,9 @@ #include #include #include +#include #include - #ifndef SCE_KERNEL_ERROR_EINVAL #define SCE_KERNEL_ERROR_EINVAL static_cast(0x80020016u) #endif @@ -36,7 +35,6 @@ constexpr size_t kNumPages = static_cast(kPoolBytes / kPage); static_assert(kPage != 0 && (kPage & (kPage - 1)) == 0, "PS5_PAGE_SIZE must be a power of two"); static_assert(kPoolBytes % kPage == 0, "DIRECT_MEMORY_SIZE must be a multiple of PS5_PAGE_SIZE"); - bool AlignUp(uint64_t v, uint64_t a, uint64_t& out) { const uint64_t m = a - 1; if (v > UINT64_MAX - m) return false; @@ -44,7 +42,6 @@ bool AlignUp(uint64_t v, uint64_t a, uint64_t& out) { return true; } - bool NormalizeAlignment(size_t alignment, uint64_t& out) { const uint64_t a = (alignment == 0) ? kPage : static_cast(alignment); if ((a & (a - 1)) != 0) return false; @@ -52,7 +49,6 @@ bool NormalizeAlignment(size_t alignment, uint64_t& out) { return true; } - bool NormalizeLength(size_t len, uint64_t& out) { if (len == 0) return false; uint64_t rounded; @@ -69,7 +65,6 @@ bool NormalizeSearchRange(int64_t searchStart, int64_t searchEnd, uint64_t& star return start < end; } - bool NormalizeRegion(uint64_t start, size_t len, uint64_t& alignedLen) { if (start >= kPoolBytes || (start & (kPage - 1)) != 0) return false; if (!NormalizeLength(len, alignedLen)) return false; @@ -99,26 +94,18 @@ struct PhysicalMemoryPool { while (cur <= end && alignedLen <= end - cur) { size_t lastUsed; if (!_lastUsedPage(static_cast(cur / kPage), pageCount, lastUsed)) { - const int rc = _commit(cur, alignedLen, memoryType); - if (rc == 0) *physOut = static_cast(cur); - return rc; + _commit(cur, alignedLen, memoryType); + *physOut = static_cast(cur); + return 0; } - if (!AlignUp((static_cast(lastUsed) + 1) * kPage, align, cur)) break; } return SCE_KERNEL_ERROR_EAGAIN; } + bool Free(uint64_t start, size_t len) { return _release(start, len, false); } - bool Free(uint64_t start, size_t len) { return _release(start, len, /*requireAllocated=*/false); } - - bool CheckedFree(uint64_t start, size_t len) { - try { - return _release(start, len, /*requireAllocated=*/true); - } catch (...) { - return false; - } - } + bool CheckedFree(uint64_t start, size_t len) { return _release(start, len, true); } bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { if (!start || !end || !memoryType || offset >= kPoolBytes) return false; @@ -240,21 +227,18 @@ struct PhysicalMemoryPool { for (size_t i = 0; i < count; ++i) _used[first + i] = used; } - int _commit(uint64_t start, uint64_t len, int type) { - const uint64_t end = start + len; - - std::pair ins = _blocks.emplace(start, Block{end, type}); - if (!ins.second) return SCE_KERNEL_ERROR_EFAULT; + void _commit(uint64_t start, uint64_t len, int type) { + std::pair ins = _blocks.emplace(start, Block{start + len, type}); + if (!ins.second) throw std::logic_error("DirectMemory pool: block map and page flags disagree"); try { CreateDirectMemoryBacking(static_cast(start), static_cast(len), type); } catch (...) { _blocks.erase(ins.first); - return SCE_KERNEL_ERROR_ENOMEM; + throw; } _mark(static_cast(start / kPage), static_cast(len / kPage), true); - return 0; } bool _release(uint64_t start, size_t len, bool requireAllocated) { @@ -267,7 +251,6 @@ struct PhysicalMemoryPool { std::lock_guard lock(_mutex); if (requireAllocated ? !_allUsed(first, count) : !_anyUsed(first, count)) return false; - const bool splitStart = _splitAt(start); bool splitEnd = false; try { @@ -284,7 +267,6 @@ struct PhysicalMemoryPool { return true; } - bool _splitAt(uint64_t at) { BlockMap::iterator it = _blocks.upper_bound(at); if (it == _blocks.begin()) return false; @@ -310,61 +292,42 @@ struct PhysicalMemoryPool { }; } + int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { if (!physOut) return SCE_KERNEL_ERROR_EFAULT; try { return PhysicalMemoryPool::Instance().Alloc(searchStart, searchEnd, len, alignment, memoryType, physOut); } catch (const std::bad_alloc&) { return SCE_KERNEL_ERROR_ENOMEM; - } catch (...) { - return SCE_KERNEL_ERROR_EFAULT; } } void DirectMemoryFree(int64_t start, size_t len) { if (start < 0 || len == 0) return; - // Exceptions intentionally propagate to surface physical backing cleanup failures. (void)PhysicalMemoryPool::Instance().Free(static_cast(start), len); } bool DirectMemoryCheckedFree(int64_t start, size_t len) { if (start < 0 || len == 0) return false; - try { - return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); - } catch (...) { - return false; - } + return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); } bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { if (offset < 0 || !start || !end || !memoryType) return false; - try { - return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); - } catch (...) { - return false; - } + return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { if (start < 0 || len == 0) return; - // Exceptions intentionally propagate to surface map node allocation failures. (void)PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); } bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alignment, int64_t* startOut, size_t* sizeOut) { if (!startOut || !sizeOut) return false; - try { - return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); - } catch (...) { - return false; - } + return PhysicalMemoryPool::Instance().LargestFree(searchStart, searchEnd, alignment, startOut, sizeOut); } size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { if (offset >= kPoolBytes) return 0; - try { - return PhysicalMemoryPool::Instance().FreeRun(offset, limit); - } catch (...) { - return 0; - } + return PhysicalMemoryPool::Instance().FreeRun(offset, limit); } From 441c0b4052043453027e084bb8953951e8eee099 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 16:19:20 +0530 Subject: [PATCH 07/22] test(libkernel): cover DirectMemory pool bounds and rollback Add MemoryPoolTest with cases for argument validation, rounding, alignment and clamping, partial and spanning Free, Retype splitting, and rollback after injected bad_alloc and backing/forget failures. Register each case with ctest. docs(techdebt): update MemoryPool exception propagation entry Only DirectMemoryAlloc translates bad_alloc; all other entry points propagate, and mutating paths roll back before throwing. --- core/libs/tests/MemoryPoolTest.cpp | 336 +++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 core/libs/tests/MemoryPoolTest.cpp diff --git a/core/libs/tests/MemoryPoolTest.cpp b/core/libs/tests/MemoryPoolTest.cpp new file mode 100644 index 000000000000..fe0e117bb329 --- /dev/null +++ b/core/libs/tests/MemoryPoolTest.cpp @@ -0,0 +1,336 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/MemoryPool.hpp" +#include +#include +#include +#include +#include +#include + +bool DirectMemoryCheckedFree(int64_t start, size_t len); +void DirectMemoryRetype(int64_t start, size_t len, int memoryType); +size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); + +namespace { + +constexpr uint64_t kPage = static_cast(PS5_PAGE_SIZE); +constexpr uint64_t kPool = static_cast(DIRECT_MEMORY_SIZE); +constexpr int kEinval = static_cast(0x80020016u); +constexpr int kEfault = static_cast(0x8002000Eu); +constexpr int kEagain = static_cast(0x80020023u); +constexpr int kEnomem = static_cast(0x8002000Cu); + +enum class Fault { None, BackingBadAlloc, BackingRuntimeError, ForgetRuntimeError }; +Fault g_fault = Fault::None; +long g_newBudget = -1; + +int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf(" FAILED line %d: %s\n", __LINE__, #cond); \ + ++g_failures; \ + } \ + } while (0) + +struct NewBudget { + explicit NewBudget(long n) { g_newBudget = n; } + ~NewBudget() { g_newBudget = -1; } +}; + +bool BlockAt(uint64_t offset, int64_t& s, int64_t& e, int& t) { + return DirectMemoryFind(static_cast(offset), false, &s, &e, &t); +} + +bool BlockIs(uint64_t offset, uint64_t start, uint64_t end, int type) { + int64_t s = 0, e = 0; + int t = 0; + return BlockAt(offset, s, e, t) && s == static_cast(start) && e == static_cast(end) && t == type; +} + +bool PoolEmpty() { + int64_t s, e; + int t; + return !DirectMemoryFind(0, true, &s, &e, &t) && DirectMemoryFreeRun(0, kPool) == kPool; +} + +void alloc_invalid_args() { + int64_t out = -1; + CHECK(DirectMemoryAlloc(0, kPool, 0, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(-1, kPool, kPage, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(0, kPool, SIZE_MAX, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(0, kPool, kPool + kPage, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 3 * kPage, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(kPage, kPage, kPage, 0, 1, &out) == kEinval); + CHECK(DirectMemoryAlloc(kPool, kPool + kPage, kPage, 0, 1, &out) == kEinval); + CHECK(out == -1); + CHECK(PoolEmpty()); +} + +void alloc_null_out() { + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, nullptr) == kEfault); + CHECK(PoolEmpty()); +} + +void alloc_rounding_alignment_and_clamping() { + int64_t a = -1, b = -1; + CHECK(DirectMemoryAlloc(0, kPool, 1, 1, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, -1, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + CHECK(DirectMemoryAlloc(0, INT64_MAX, kPage, 0, 1, &a) == 0 && a == 0); + DirectMemoryFree(0, kPage); + + CHECK(DirectMemoryAlloc(1, kPool, kPage, 0, 1, &a) == 0 && a == static_cast(kPage)); + DirectMemoryFree(0, kPool); + + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, kPage, static_cast(1) << 62, 1, &b) == kEagain); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void alloc_exhaustion_and_gap_reuse() { + int64_t a, b, c; + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &b) == 0 && b == static_cast(kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(2 * kPage)); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == kEagain); + DirectMemoryFree(static_cast(kPage), kPage); + CHECK(DirectMemoryAlloc(0, 3 * kPage, kPage, 0, 1, &c) == 0 && c == static_cast(kPage)); + DirectMemoryFree(0, kPool); +} + +void free_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryFree(0, SIZE_MAX); + DirectMemoryFree(static_cast(kPool), kPage); + DirectMemoryFree(static_cast(kPool - kPage), 2 * kPage); + DirectMemoryFree(1, kPage); + DirectMemoryFree(INT64_MAX, kPage); + CHECK(!DirectMemoryCheckedFree(0, SIZE_MAX)); + CHECK(!DirectMemoryCheckedFree(static_cast(kPool), kPage)); + CHECK(!DirectMemoryCheckedFree(1, kPage)); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_out_of_range_is_ignored() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 7, &a) == 0 && a == 0); + DirectMemoryRetype(0, SIZE_MAX, 9); + DirectMemoryRetype(static_cast(kPool), kPage, 9); + DirectMemoryRetype(static_cast(kPool - kPage), 2 * kPage, 9); + DirectMemoryRetype(1, kPage, 9); + CHECK(BlockIs(0, 0, 4 * kPage, 7)); + DirectMemoryFree(0, kPool); +} + +void free_partial_and_spanning() { + int64_t a, b; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &a) == 0 && a == 0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 2, &b) == 0 && b == static_cast(4 * kPage)); + DirectMemoryFree(static_cast(2 * kPage), 4 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 1)); + CHECK(BlockIs(6 * kPage, 6 * kPage, 8 * kPage, 2)); + CHECK(DirectMemoryFreeRun(2 * kPage, kPool) == 4 * kPage); + CHECK(!DirectMemoryCheckedFree(0, 4 * kPage)); + CHECK(DirectMemoryCheckedFree(0, 2 * kPage)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +void retype_splits_and_skips_gaps() { + int64_t a; + CHECK(DirectMemoryAlloc(2 * kPage, kPool, 4 * kPage, 0, 1, &a) == 0 && a == static_cast(2 * kPage)); + DirectMemoryRetype(static_cast(3 * kPage), kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 1)); + CHECK(BlockIs(3 * kPage, 3 * kPage, 4 * kPage, 9)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 6 * kPage, 1)); + DirectMemoryRetype(0, 8 * kPage, 5); + CHECK(BlockIs(2 * kPage, 2 * kPage, 3 * kPage, 5)); + CHECK(BlockIs(5 * kPage, 4 * kPage, 6 * kPage, 5)); + int64_t s, e; + int t; + CHECK(!BlockAt(0, s, e, t) && !BlockAt(6 * kPage, s, e, t)); + DirectMemoryFree(0, kPool); +} + +void alloc_backing_bad_alloc_rolls_back() { + int64_t out = -1; + g_fault = Fault::BackingBadAlloc; + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == kEnomem); + g_fault = Fault::None; + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_node_bad_alloc_rolls_back() { + int64_t out = -1; + { + NewBudget budget(0); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == kEnomem); + } + CHECK(out == -1); + CHECK(PoolEmpty()); + CHECK(DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out) == 0 && out == 0); + DirectMemoryFree(0, kPool); +} + +void alloc_unexpected_exception_propagates() { + int64_t out = -1; + g_fault = Fault::BackingRuntimeError; + bool threw = false; + try { + (void)DirectMemoryAlloc(0, kPool, 4 * kPage, 0, 1, &out); + } catch (const std::runtime_error&) { + threw = true; + } + g_fault = Fault::None; + CHECK(threw); + CHECK(PoolEmpty()); +} + +void free_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 0); + } + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + CHECK(BlockIs(0, 0, 2 * kPage, 3)); + CHECK(BlockIs(4 * kPage, 4 * kPage, 8 * kPage, 3)); + CHECK(DirectMemoryFreeRun(2 * kPage, 4 * kPage) == 2 * kPage); + DirectMemoryFree(0, kPool); +} + +void retype_node_bad_alloc_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + for (long budget = 0; budget < 2; ++budget) { + bool threw = false; + { + NewBudget guard(budget); + try { + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + } catch (const std::bad_alloc&) { + threw = true; + } + } + CHECK(threw); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + } + DirectMemoryRetype(static_cast(2 * kPage), 2 * kPage, 9); + CHECK(BlockIs(2 * kPage, 2 * kPage, 4 * kPage, 9)); + DirectMemoryFree(0, kPool); +} + +void forget_exception_propagates_and_rolls_back() { + int64_t a; + CHECK(DirectMemoryAlloc(0, kPool, 8 * kPage, 0, 3, &a) == 0 && a == 0); + g_fault = Fault::ForgetRuntimeError; + bool threwFree = false, threwChecked = false; + try { + DirectMemoryFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwFree = true; + } + try { + (void)DirectMemoryCheckedFree(static_cast(2 * kPage), 2 * kPage); + } catch (const std::runtime_error&) { + threwChecked = true; + } + g_fault = Fault::None; + CHECK(threwFree); + CHECK(threwChecked); + CHECK(BlockIs(0, 0, 8 * kPage, 3)); + DirectMemoryFree(0, kPool); + CHECK(PoolEmpty()); +} + +struct Case { + const char* name; + void (*fn)(); +}; + +const Case kCases[] = { + {"alloc_invalid_args", alloc_invalid_args}, + {"alloc_null_out", alloc_null_out}, + {"alloc_rounding_alignment_and_clamping", alloc_rounding_alignment_and_clamping}, + {"alloc_exhaustion_and_gap_reuse", alloc_exhaustion_and_gap_reuse}, + {"free_out_of_range_is_ignored", free_out_of_range_is_ignored}, + {"retype_out_of_range_is_ignored", retype_out_of_range_is_ignored}, + {"free_partial_and_spanning", free_partial_and_spanning}, + {"retype_splits_and_skips_gaps", retype_splits_and_skips_gaps}, + {"alloc_backing_bad_alloc_rolls_back", alloc_backing_bad_alloc_rolls_back}, + {"alloc_node_bad_alloc_rolls_back", alloc_node_bad_alloc_rolls_back}, + {"alloc_unexpected_exception_propagates", alloc_unexpected_exception_propagates}, + {"free_node_bad_alloc_rolls_back", free_node_bad_alloc_rolls_back}, + {"retype_node_bad_alloc_rolls_back", retype_node_bad_alloc_rolls_back}, + {"forget_exception_propagates_and_rolls_back", forget_exception_propagates_and_rolls_back}, +}; + +} // namespace + +void CreateDirectMemoryBacking(int64_t, size_t, int) { + if (g_fault == Fault::BackingBadAlloc) throw std::bad_alloc(); + if (g_fault == Fault::BackingRuntimeError) throw std::runtime_error("backing"); +} + +void ForgetDirectMemory(int64_t, size_t) { + if (g_fault == Fault::ForgetRuntimeError) throw std::runtime_error("forget"); +} + +void* operator new(size_t n) { + if (g_newBudget == 0) throw std::bad_alloc(); + if (g_newBudget > 0) --g_newBudget; + void* p = std::malloc(n ? n : 1); + if (!p) throw std::bad_alloc(); + return p; +} +void operator delete(void* p) noexcept { std::free(p); } +void operator delete(void* p, size_t) noexcept { std::free(p); } + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--list") == 0) { + for (const Case& c : kCases) std::printf("%s\n", c.name); + return 0; + } + int ran = 0; + for (const Case& c : kCases) { + if (argc == 2 && std::strcmp(argv[1], c.name) != 0) continue; + std::printf("[ RUN ] %s\n", c.name); + const int before = g_failures; + try { + c.fn(); + } catch (...) { + std::printf(" FAILED: unexpected exception escaped\n"); + ++g_failures; + } + std::printf("[ %s ] %s\n", g_failures == before ? " OK " : "FAIL", c.name); + ++ran; + } + if (ran == 0) { + std::printf("no such case\n"); + return 2; + } + return g_failures == 0 ? 0 : 1; +} From e9bebec949a96e1c6c8a8f0625ab3727e4c374e3 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 16:21:22 +0530 Subject: [PATCH 08/22] docs(techdebt): update MemoryPool exception propagation entry Only DirectMemoryAlloc translates std::bad_alloc (to ENOMEM); all other entry points, including the void DirectMemoryFree and DirectMemoryRetype, let exceptions propagate. Mutating paths roll back before throwing, so the pool is unchanged. Record that nothing catches at the guest boundary yet. --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 004b6c0af038..87e08cde85b6 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -588,4 +588,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - `DirectMemoryFree` and `DirectMemoryRetype` return `void`, so internal failures (map node allocation, backing cleanup) are intentionally allowed to propagate across the guest ABI to surface tracking problems for debugging. Both now give the strong guarantee (the pool is unchanged when they throw, so a retry is safe), but an uncaught exception at a C or syscall boundary still terminates. Revisit once a guest-facing error mapping exists. +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. Mutating paths roll back before throwing, so the pool is unchanged. Nothing catches at the guest boundary yet. From e76d34ec567bd07c6cb479ed16b66188ba086560 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 16:27:13 +0530 Subject: [PATCH 09/22] docs(techdebt): update MemoryPool exception propagation entry --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 87e08cde85b6..f1e75d386bdf 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -588,4 +588,4 @@ Throughout the project, every function at every stage either **does exactly what - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. Mutating paths roll back before throwing, so the pool is unchanged. Nothing catches at the guest boundary yet. +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. From ffc494151867db1223a0dac34ce9b112ba00a9ae Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 17:19:21 +0530 Subject: [PATCH 10/22] docs(libkernel): update MemoryPool technical debt registrations Moves the existing functional inline comment registration to prevent merge conflicts at the end of the block. Documents the silent invalid range handling in DirectMemoryFree/Retype and the unbounded negative searchEnd in LargestFree under the unknown function info section. --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 696a635e1dbe..fff6c92c5344 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -589,3 +589,4 @@ Throughout the project, every function at every stage either **does exactly what - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. - [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. +- - [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - DirectMemoryFree and DirectMemoryRetype silently ignore invalid ranges; a negative searchEnd means unbounded in LargestFree. From 9842ee5635b786af1cd307f96c8ddab06ddae679 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 17:27:50 +0530 Subject: [PATCH 11/22] test(libkernel): register MemoryPoolTest executable in CMake Compiles the test directly against MemoryPool.cpp to ensure custom mocked functions are utilized, and registers it to run locally and in CI via ctest. --- core/libs/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e4c0..bf78731704b9 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -329,4 +329,11 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(MemoryPoolTest + tests/MemoryPoolTest.cpp + prx/libkernel/DirectMemory/MemoryPool.cpp + ) + add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) + +endif() endif() From 0e14043d6a2332924d613beaaebb45696724e1bf Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 18:00:54 +0530 Subject: [PATCH 12/22] test(libkernel): register MemoryPoolTest executable in CMake Compiles the test directly against MemoryPool.cpp to ensure custom mocked functions are utilized, and registers it to run locally and in CI via ctest. --- core/libs/CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index bf78731704b9..d4625f151365 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -329,11 +329,21 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + + add_test_executable(guest_posix_random_tests tests/GuestPosixRandom.cpp) + target_include_directories(guest_posix_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_posix_random_tests PRIVATE libScePosixForWebKit) + configure_windows_unwind(guest_posix_random_tests) + add_test(NAME guest_posix_random COMMAND guest_posix_random_tests) + if(WIN32) + set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(MemoryPoolTest tests/MemoryPoolTest.cpp prx/libkernel/DirectMemory/MemoryPool.cpp ) add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) - -endif() endif() From 627f8d6f001222b2e41ff84ccaa9f37dc9ea10ee Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 18:08:01 +0530 Subject: [PATCH 13/22] docs(libkernel): relocate MemoryPool technical debt entries Moves the functional exception documentation away from the end of the section to prevent merge conflicts. Relocates the silent invalid range handling and unbounded negative searchEnd behaviors to the Unknown function info section as requested by reviewers. --- docs/dev/TechnicalDebt.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c4d4f82c894e..04dbc97d70a7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -97,6 +97,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - DirectMemoryFree and DirectMemoryRetype silently ignore invalid ranges; a negative searchEnd means unbounded in LargestFree. - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified @@ -591,11 +592,11 @@ Throughout the project, every function at every stage either **does exactly what - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- - [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. -- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. -- - [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - DirectMemoryFree and DirectMemoryRetype silently ignore invalid ranges; a negative searchEnd means unbounded in LargestFree. + From a1efe65763c47500de1e119eaba782d059c06537 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 18:13:35 +0530 Subject: [PATCH 14/22] build: restore imgui url in .gitmodules --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitmodules b/.gitmodules index fc2e48631c8e..53bafa4fda69 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "3rdparty/libjpeg-turbo"] path = 3rdparty/libjpeg-turbo url = https://github.com/libjpeg-turbo/libjpeg-turbo.git +[submodule "3rdparty/imgui"] + path = 3rdparty/imgui + url = https://github.com/ocornut/imgui.git From a4621fb9b77ea2e4fd6a7fc986b199557cdff776 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 18:24:04 +0530 Subject: [PATCH 15/22] build: drop unintended imgui submodule addition Removes the 3rdparty/imgui entry from .gitmodules that is not present on main to ensure a clean CI checkout. --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 53bafa4fda69..fc2e48631c8e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,6 +31,3 @@ [submodule "3rdparty/libjpeg-turbo"] path = 3rdparty/libjpeg-turbo url = https://github.com/libjpeg-turbo/libjpeg-turbo.git -[submodule "3rdparty/imgui"] - path = 3rdparty/imgui - url = https://github.com/ocornut/imgui.git From 61eaa135b3ac4e661bb35d77b394f61e2a8a2f4e Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 18:26:03 +0530 Subject: [PATCH 16/22] test(libkernel): remove duplicate test block in CMake Removes the accidentally duplicated guest_posix_random_tests block to resolve CMake configure failures, leaving only the clean registration for MemoryPoolTest. --- core/libs/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d4625f151365..46cb97c02cb9 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -340,7 +340,6 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test_executable(MemoryPoolTest tests/MemoryPoolTest.cpp prx/libkernel/DirectMemory/MemoryPool.cpp From 45234f60a9a066a0c837b64698a952afd57af155 Mon Sep 17 00:00:00 2001 From: Yash Singh Date: Sun, 11 Oct 2026 18:29:48 +0530 Subject: [PATCH 17/22] docs(libkernel): fix formatting and accuracy of MemoryPool debt Removes a stray markdown list character and updates the unknown function info to accurately reflect that invalid ranges are now rejected, leaving only the unbounded negative searchEnd documentation. --- docs/dev/TechnicalDebt.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 04dbc97d70a7..a7c5c115c251 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -97,7 +97,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels -- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - DirectMemoryFree and DirectMemoryRetype silently ignore invalid ranges; a negative searchEnd means unbounded in LargestFree. +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - A negative searchEnd means unbounded in LargestFree. - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified @@ -592,7 +592,7 @@ Throughout the project, every function at every stage either **does exactly what - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. -- - [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. From e2cb58d39f2e781bd04268c998c13518839a5341 Mon Sep 17 00:00:00 2001 From: Yaash Singhh <186028205+yash27git@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:02:03 +0530 Subject: [PATCH 18/22] fix(tests): remove duplicate POSIX random test registration --- core/libs/CMakeLists.txt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 46cb97c02cb9..1ffbef37c583 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -331,18 +331,11 @@ if(BUILD_TESTING) endif() - add_test_executable(guest_posix_random_tests tests/GuestPosixRandom.cpp) - target_include_directories(guest_posix_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_posix_random_tests PRIVATE libScePosixForWebKit) - configure_windows_unwind(guest_posix_random_tests) - add_test(NAME guest_posix_random COMMAND guest_posix_random_tests) - if(WIN32) - set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() + add_test_executable(MemoryPoolTest tests/MemoryPoolTest.cpp prx/libkernel/DirectMemory/MemoryPool.cpp ) + target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) endif() From 80b019a1ad1853c3ec40e66218b13cce4435dc43 Mon Sep 17 00:00:00 2001 From: Yaash Singhh <186028205+yash27git@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:02:23 +0530 Subject: [PATCH 19/22] docs(libkernel): clarify unverified DirectMemory behavior --- docs/dev/TechnicalDebt.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index a7c5c115c251..68cc76cc4770 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -97,7 +97,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels -- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - A negative searchEnd means unbounded in LargestFree. +- [DirectMemoryFree, DirectMemoryRetype, LargestFree](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Invalid ranges passed to `DirectMemoryFree` and `DirectMemoryRetype` are ignored, while `DirectMemoryCheckedFree` returns false; whether the console reports errors for these invalid ranges is unknown. A negative `searchEnd` is treated as unbounded in `LargestFree`, but this behavior has not been confirmed against a console or a reference implementation. - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified @@ -346,6 +346,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. + - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. @@ -592,7 +594,7 @@ Throughout the project, every function at every stage either **does exactly what - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. -- [MemoryPool](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Only `DirectMemoryAlloc` translates `std::bad_alloc` (to `SCE_KERNEL_ERROR_ENOMEM`); every other entry point, including the `void` `DirectMemoryFree` and `DirectMemoryRetype`, lets exceptions propagate across the guest ABI so internal tracking failures surface for debugging instead of becoming plausible return values. The pool's own bookkeeping rolls back before throwing, but `ForgetDirectMemory`'s physical page and mapping updates are not transactional yet, so a throw while splitting a partially overlapped mapping is not retry-safe. Nothing catches at the guest boundary yet. + - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. From 572b0f25d2fb3d62ac97b5c076764c2c1b3867da Mon Sep 17 00:00:00 2001 From: Yaash Singhh <186028205+yash27git@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:02:46 +0530 Subject: [PATCH 20/22] test(libkernel): configure unwind handling for MemoryPoolTest --- core/libs/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 1ffbef37c583..156343a40ee3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -337,5 +337,6 @@ if(BUILD_TESTING) prx/libkernel/DirectMemory/MemoryPool.cpp ) target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + configure_windows_unwind(MemoryPoolTest) add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest) endif() From 65e62d3fd6463dd994f0325dbeac7dda46d3d217 Mon Sep 17 00:00:00 2001 From: Yaash Singhh <186028205+yash27git@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:08:22 +0530 Subject: [PATCH 21/22] style(tests): clean MemoryPoolTest CMake registration --- core/libs/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 156343a40ee3..5de7c6f8f234 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -330,7 +330,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - add_test_executable(MemoryPoolTest tests/MemoryPoolTest.cpp From aa3d76ff614c4d5180fd04834968fa8dd5fe8048 Mon Sep 17 00:00:00 2001 From: Yaash Singhh <186028205+yash27git@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:08:25 +0530 Subject: [PATCH 22/22] docs(libkernel): correct DirectMemory validation debt note --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 68cc76cc4770..c7cc685e88ef 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -97,7 +97,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented - [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels -- [DirectMemoryFree, DirectMemoryRetype, LargestFree](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Invalid ranges passed to `DirectMemoryFree` and `DirectMemoryRetype` are ignored, while `DirectMemoryCheckedFree` returns false; whether the console reports errors for these invalid ranges is unknown. A negative `searchEnd` is treated as unbounded in `LargestFree`, but this behavior has not been confirmed against a console or a reference implementation. +- [DirectMemoryFree, DirectMemoryRetype, LargestFree](../../core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp) (libkernel) - Invalid ranges are rejected with `EINVAL` or `EFAULT` in the current implementation. A negative `searchEnd` is treated as unbounded in `LargestFree`, but this behavior has not been confirmed against a console or a reference implementation. - [sceVideoOutGetResolutionStatus](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown - [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified