From 5ffafa920f7e220d6811f0a8133f50daea294c61 Mon Sep 17 00:00:00 2001 From: SibteProf Date: Mon, 12 Oct 2026 17:06:54 +0500 Subject: [PATCH] fix(libc): reject oversized secure copy destinations --- core/libs/prx/libc/src/Strings.cpp | 4 ++++ core/libs/tests/GuestStrings.cpp | 35 ++++++++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 40 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 564458ace..43d1cedcd 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -304,7 +304,9 @@ int APS5_VABI strcat_s_nid_postfix(char* dest, size_t destsz, const char* src) { int APS5_VABI memcpy_s_nid_postfix(void* dest, size_t destsz, const void* src, size_t count) { constexpr int GuestEinval = 22; constexpr int GuestErange = 34; + constexpr size_t RsizeMax = SIZE_MAX >> 1; if (!dest) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; if (!src || count > destsz) { std::memset(dest, 0, destsz); return src ? GuestErange : GuestEinval; @@ -323,7 +325,9 @@ int APS5_VABI memcpy_s_nid_postfix(void* dest, size_t destsz, const void* src, s int APS5_VABI memmove_s_nid_postfix(void* dest, size_t destsz, const void* src, size_t count) { constexpr int GuestEinval = 22; constexpr int GuestErange = 34; + constexpr size_t RsizeMax = SIZE_MAX >> 1; if (!dest) return GuestEinval; + if (destsz > RsizeMax) return GuestErange; if (!src || count > destsz) { std::memset(dest, 0, destsz); return src ? GuestErange : GuestEinval; diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 4ae9225c7..db037c8d1 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -69,6 +69,40 @@ static bool CheckMemcpyOverlap() { return correct; } +static void CheckSecureCopySizeLimits() { + using Copy = int (APS5_VABI*)(void*, std::size_t, const void*, std::size_t); + constexpr std::size_t RsizeMax = SIZE_MAX >> 1; + const std::array original = {1, 2, 3, 4, 5, 6, 7, 8}; + const unsigned char source[] = {9, 10, 11, 12}; + for (Copy copy : {memcpy_s_nid_postfix, memmove_s_nid_postfix}) { + for (const auto size : {RsizeMax + 1, SIZE_MAX}) { + for (const auto count : {std::size_t{1}, std::size_t{0}, RsizeMax + 1, SIZE_MAX}) { + for (const void* input : {static_cast(source), static_cast(nullptr)}) { + auto bytes = original; + Require(copy(bytes.data() + 2, size, input, count) != 0); + Require(bytes == original); + } + } + } + for (const auto count : {RsizeMax + 1, SIZE_MAX}) { + auto bytes = original; + Require(copy(bytes.data() + 2, 4, source, count) != 0); + const std::array cleared = {1, 2, 0, 0, 0, 0, 7, 8}; + Require(bytes == cleared); + } + auto bytes = original; + Require(copy(bytes.data() + 2, RsizeMax, source, 1) == 0); + const std::array copied = {1, 2, 9, 4, 5, 6, 7, 8}; + Require(bytes == copied); + Require(copy(bytes.data() + 2, 4, source, 0) == 0 && bytes == copied); + Require(copy(nullptr, SIZE_MAX, source, 1) == 22); + bytes = original; + Require(copy(bytes.data() + 2, 4, nullptr, 1) == 22); + const std::array cleared = {1, 2, 0, 0, 0, 0, 7, 8}; + Require(bytes == cleared); + } +} + static void CheckBoundsCheckedFunctions() { char small[4] = "zz"; Require(strcpy_s_nid_postfix(small, sizeof(small), "abc") == 0 && std::strcmp(small, "abc") == 0); @@ -314,5 +348,6 @@ int main() { Require(strcasestr_nid_postfix("", "a") == nullptr); const char highBytes[] = {static_cast(0xff), 'A', 0}; Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); + CheckSecureCopySizeLimits(); return CheckMemcpyOverlap() ? 0 : 1; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 71c07945e..a33fff95a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -336,6 +336,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelAddTimerEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceKernelWaitEqueue](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) +- [memcpy_s and memmove_s](../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 K.3.7.1.1 and K.3.7.1.2 size constraints, with RSIZE_MAX taken as SIZE_MAX >> 1 as in memset_s. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [vsnprintf_s and vsscanf_s](../../core/libs/prx/libc/src/Formatting.cpp) (libc) - `vsnprintf_s` forwards to `vsnprintf` semantics: the C11 K.3.7.1.5 runtime constraints (null buffer, `size == 0`, `%n`, null `%s`) are not checked, so `size == 0` returns the would-be length instead of a negative value, and no constraint handler is invoked. `vsscanf_s` returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Neither is confirmed on a PS5 title - [libSceUlt ulthreads](../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware