From 78338a3eec9e142f75e797a1c7d785b5e3e49d34 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Thu, 29 Oct 2026 13:11:01 +0100 Subject: [PATCH 1/4] feat(libc): implement asctime_r --- core/libs/prx/libc/include/Api.hpp | 1 + core/libs/prx/libc/src/Time.cpp | 17 +++++++++++------ core/libs/tests/GuestStrftime.cpp | 6 ++++++ core/libs/tests/GuestTimeConversion.cpp | 8 ++++++++ docs/dev/debt/libc.md | 2 +- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 30f3f29895e..48cf54f8364 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -1137,6 +1137,7 @@ int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr); int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr); size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr); char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr); +char* APS5_VABI asctime_r_nid_postfix(const GuestTm* timeptr, char* buffer); char* APS5_VABI ctime_nid_postfix(const int64_t* timer); size_t APS5_VABI strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr); size_t APS5_VABI wcsftime_nid_postfix(std::uint16_t* str, size_t count, const std::uint16_t* format, const GuestTm* timeptr); diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 189bde0aa50..f375f845307 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -597,17 +597,22 @@ size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* return formatGuestTime(str, count, format, *timeptr); } -char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { - static thread_local char result[ASCTIME_SIZE]; +char* APS5_VABI asctime_r_nid_postfix(const GuestTm* timeptr, char* buffer) { + if (buffer == nullptr) return nullptr; if (timeptr == nullptr) { errno = EINVAL; - std::memcpy(result, "??? ??? ?? ??:??:?? ????\n", sizeof("??? ??? ?? ??:??:?? ????\n")); - return result; + std::memcpy(buffer, "??? ??? ?? ??:??:?? ????\n", sizeof("??? ??? ?? ??:??:?? ????\n")); + return buffer; } const std::int64_t year = static_cast(timeptr->tm_year) + YEAR_BASE; - std::snprintf(result, sizeof(result), "%s %s%3d %02d:%02d:%02d %04d\n", tableName(timeptr->tm_wday, WEEKDAY_NAMES, WEEKDAY_NAMES, 7), tableName(timeptr->tm_mon, MONTH_NAMES, MONTH_NAMES, 12), + std::snprintf(buffer, 26, "%s %s%3d %02d:%02d:%02d %04d\n", tableName(timeptr->tm_wday, WEEKDAY_NAMES, WEEKDAY_NAMES, 7), tableName(timeptr->tm_mon, MONTH_NAMES, MONTH_NAMES, 12), clampedField(timeptr->tm_mday, 100), clampedField(timeptr->tm_hour, 100), clampedField(timeptr->tm_min, 100), clampedField(timeptr->tm_sec, 100), clampedField(year, 10000)); - return result; + return buffer; +} + +char* APS5_VABI asctime_nid_postfix(const GuestTm* timeptr) { + static thread_local char result[ASCTIME_SIZE]; + return asctime_r_nid_postfix(timeptr, result); } char* APS5_VABI ctime_nid_postfix(const int64_t* timer) { diff --git a/core/libs/tests/GuestStrftime.cpp b/core/libs/tests/GuestStrftime.cpp index c7a55368ece..10f35b02db6 100644 --- a/core/libs/tests/GuestStrftime.cpp +++ b/core/libs/tests/GuestStrftime.cpp @@ -41,6 +41,12 @@ void CheckAsctime(const char* name, const GuestTm& time, const char* expected) { std::fprintf(stderr, "asctime %s: expected \"%s\", received \"%s\"\n", name, expected, result); correct = false; } + char buffer[26]{}; + const char* reentrant = asctime_r_nid_postfix(&time, buffer); + if (reentrant != buffer || std::strcmp(buffer, expected) != 0) { + std::fprintf(stderr, "asctime_r %s: expected \"%s\", received \"%s\"\n", name, expected, buffer); + correct = false; + } } } diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index d51d617b196..d95cbe8d75e 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -75,6 +75,9 @@ static GuestTm UtcTime(std::int64_t timer) { static void CheckAsctime(const GuestTm& brokenDown, const char* expected) { const char* text = asctime_nid_postfix(&brokenDown); Require(text != nullptr && std::strcmp(text, expected) == 0, "asctime did not format the broken-down time"); + char buffer[26]{}; + Require(asctime_r_nid_postfix(&brokenDown, buffer) == buffer && std::strcmp(buffer, expected) == 0, + "asctime_r did not format the broken-down time"); } struct UtcCase { @@ -496,6 +499,11 @@ int main() { *__error_nid_postfix() = 0; Require(std::strcmp(ctime_nid_postfix(&underflow), "??? ??? ?? ??:??:?? ????\n") == 0 && *__error_nid_postfix() == 22, "ctime of an unrepresentable time did not report it"); + char asctimeBuffer[26]{}; + *__error_nid_postfix() = 0; + Require(asctime_r_nid_postfix(nullptr, asctimeBuffer) == asctimeBuffer && std::strcmp(asctimeBuffer, "??? ??? ?? ??:??:?? ????\n") == 0 + && *__error_nid_postfix() == 22, "asctime_r of a null time did not report it"); + Require(asctime_r_nid_postfix(&roundTrip, nullptr) == nullptr, "asctime_r accepted a null buffer"); { std::barrier finish(3); std::array texts{}; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c2bc2774d3b..8a14be16587 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -199,7 +199,7 @@ - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A payload process on a PS5 (firmware 13.60) read both names empty with `TZ` unset, and its `%Z` printed nothing whatever `tm_zone` held; the names a title process sees are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. -- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname` +- [strftime, asctime and asctime_r](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname` - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) converts the format to UTF-8, runs this library's `strftime` and converts the result back, where the console's `libSceLibcInternal.sprx` (0x854e0) copies literal wide characters and expands each conversion straight into the buffer; the results match for the formats tried, and it has the `strftime` gaps above. A result that does not fit returns 0 with the buffer untouched, where the console leaves the characters written so far. A null buffer returns 0, where the console writes through it, and a size too large for the narrow buffer returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. From a861a4ca967892faddbb43ade4cbc4dd84603aa4 Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Thu, 29 Oct 2026 13:23:08 +0100 Subject: [PATCH 2/4] docs(libc): clarify asctime_r technical debt notes --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 8a14be16587..c2623d7a5c6 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -199,7 +199,7 @@ - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A payload process on a PS5 (firmware 13.60) read both names empty with `TZ` unset, and its `%Z` printed nothing whatever `tm_zone` held; the names a title process sees are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. -- [strftime, asctime and asctime_r](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname` +- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname`. The formatting of `asctime_r` is shared with `asctime`; the console's `asctime_r` was not measured, and the null-buffer result is assumed - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) converts the format to UTF-8, runs this library's `strftime` and converts the result back, where the console's `libSceLibcInternal.sprx` (0x854e0) copies literal wide characters and expands each conversion straight into the buffer; the results match for the formats tried, and it has the `strftime` gaps above. A result that does not fit returns 0 with the buffer untouched, where the console leaves the characters written so far. A null buffer returns 0, where the console writes through it, and a size too large for the narrow buffer returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. From e908061e3243c54655215bec7d156276baa4cb6c Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Thu, 29 Oct 2026 14:24:22 +0100 Subject: [PATCH 3/4] fix(libc): throw std::invalid_argument on null buffer in asctime_r and shorten debt note --- core/libs/prx/libc/src/Time.cpp | 2 +- core/libs/tests/GuestTimeConversion.cpp | 8 +++++++- docs/dev/debt/libc.md | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index f375f845307..152e05f18f7 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -598,7 +598,7 @@ size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* } char* APS5_VABI asctime_r_nid_postfix(const GuestTm* timeptr, char* buffer) { - if (buffer == nullptr) return nullptr; + if (buffer == nullptr) throw std::invalid_argument("asctime_r: null buffer"); if (timeptr == nullptr) { errno = EINVAL; std::memcpy(buffer, "??? ??? ?? ??:??:?? ????\n", sizeof("??? ??? ?? ??:??:?? ????\n")); diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index d95cbe8d75e..82c75d8d4d8 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -503,7 +503,13 @@ int main() { *__error_nid_postfix() = 0; Require(asctime_r_nid_postfix(nullptr, asctimeBuffer) == asctimeBuffer && std::strcmp(asctimeBuffer, "??? ??? ?? ??:??:?? ????\n") == 0 && *__error_nid_postfix() == 22, "asctime_r of a null time did not report it"); - Require(asctime_r_nid_postfix(&roundTrip, nullptr) == nullptr, "asctime_r accepted a null buffer"); + bool nullBufferThrew = false; + try { + asctime_r_nid_postfix(&roundTrip, nullptr); + } catch (const std::invalid_argument&) { + nullBufferThrew = true; + } + Require(nullBufferThrew, "asctime_r accepted a null buffer"); { std::barrier finish(3); std::array texts{}; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c2623d7a5c6..dfc83a51bea 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -199,7 +199,7 @@ - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A payload process on a PS5 (firmware 13.60) read both names empty with `TZ` unset, and its `%Z` printed nothing whatever `tm_zone` held; the names a title process sees are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. -- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname`. The formatting of `asctime_r` is shared with `asctime`; the console's `asctime_r` was not measured, and the null-buffer result is assumed +- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname`. `asctime_r` shares the `asctime` formatting, unmeasured; a null buffer throws - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) converts the format to UTF-8, runs this library's `strftime` and converts the result back, where the console's `libSceLibcInternal.sprx` (0x854e0) copies literal wide characters and expands each conversion straight into the buffer; the results match for the formats tried, and it has the `strftime` gaps above. A result that does not fit returns 0 with the buffer untouched, where the console leaves the characters written so far. A null buffer returns 0, where the console writes through it, and a size too large for the narrow buffer returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. From 3eb1642424f3323a863e4b68ca3258d3f9035d0c Mon Sep 17 00:00:00 2001 From: Hedi Trigui Date: Sun, 1 Nov 2026 23:55:35 +0100 Subject: [PATCH 4/4] docs(libc): restore upstream wcsftime debt entry and format asctime_r correctly --- docs/dev/debt/libc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 1b65341ec58..f1c1a529be9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -223,8 +223,8 @@ - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A payload process on a PS5 (firmware 13.60) read both names empty with `TZ` unset, and its `%Z` printed nothing whatever `tm_zone` held; the names a title process sees are not known - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. -- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in %G, %g, %V, %U and %W follow SDK 7.00 libc.prx that PPSA06416 ships, read from disassembly and run, and are measured on console only for the test cases; it counts a year divisible by 100 as leap only when m_year / 400 is 100 or -300. %k, %l, %P, %s, %+, %v, flags and widths are copied as written, E or O before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (%j at 1000, %u and %w at 10); %Y and %G are the year mod 10000, %C is m_year / 100 + 19. A negative weekday or month gives Sun or Jan. %z and %Z ignore m_gmtoff and m_zone and use the guest zone and zname. sctime_r shares the sctime formatting, unmeasured; a null buffer throws -- [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) has the strftime gaps above. A null buffer returns 0, where the console writes through it, and a size at or above SIZE_MAX / _Getmbcurmax() returns 0 with EINVAL, where the console formats as usual. A null format or m throws. Conversion-overflow continuation follows [reported native-PC measurements of firmware 13.42](https://github.com/boykopovar/AnyPS5/pull/10895#issuecomment-6494817352), not a physical-console run. %Z/%z, failures within a conversion and differences in title-bundled libc.prx remain unverified by that measurement +- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname`. `asctime_r` shares the `asctime` formatting, unmeasured; a null buffer throws +- [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) has the `strftime` gaps above. A null buffer returns 0, where the console writes through it, and a size at or above `SIZE_MAX / _Getmbcurmax()` returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws. Conversion-overflow continuation follows [reported native-PC measurements of firmware 13.42](https://github.com/boykopovar/AnyPS5/pull/10895#issuecomment-6494817352), not a physical-console run. `%Z`/`%z`, failures within a conversion and differences in title-bundled `libc.prx` remain unverified by that measurement - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [fwscanf, vfwscanf, wscanf, vwscanf and their `_s` forms](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) run the `swscanf` scanner on characters read with `fgetwc`, so everything listed for `swscanf` in this file applies to them. As in the console's `libc.prx`, a field takes characters while they can still begin a valid item, so a failed field such as `0x` for `%x` stays consumed, and only the character that ended the scan goes back to the stream. That character is pushed back as its UTF-8 bytes with `ungetc`, because `ungetwc` takes single bytes only; the Windows C runtime guarantees one byte of push-back, so a non-ASCII lookahead can be lost there. A null stream or format returns `EOF`; the console checks them only in the `_s` forms. - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981))