From 56c8f346fbdfcaffeca875e07540eaaecba7e23b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 11:27:28 +0000 Subject: [PATCH] fix(libkernel): convert local time with the host time zone sceKernelConvertUtcToLocaltime and sceKernelConvertLocaltimeToUtc returned the time unchanged while libc localtime follows the host zone, so a title that mixes them shows UTC as local time (#5067). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017iLxaSz4qjmirfqKZdhVQk --- core/libs/prx/libkernel/Time/Time.cpp | 48 +++++++++++++++++++++----- core/libs/tests/GuestFreebsdClocks.cpp | 40 +++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4513d7604e..83ce6f5fc7 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -554,10 +555,38 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { // Moved as-is (not yet implemented) from the monolithic libkernel/Export.cpp. // --------------------------------------------------------------------------- -// The sce* clock calls share the POSIX implementations above. The console clock is kept in UTC: -// the time zone reported here and by gettimeofday has no offset and no daylight saving. static constexpr int SceKernelErrorEfault = static_cast(0x8002000e); +struct HostZone { + std::int64_t standardEast; + std::int64_t daylight; +}; + +static HostZone HostZoneAt(std::int64_t utcTime) { + std::tm local{}; +#ifdef _WIN32 + constexpr std::int64_t cycleSeconds = 146097LL * 86400LL; + constexpr std::int64_t lastHostSecond = 32535215999LL; + const std::int64_t cycles = utcTime >= 0 && utcTime <= lastHostSecond ? 0 : (utcTime >= 0 ? utcTime / cycleSeconds : -((-(utcTime + 1)) / cycleSeconds + 1)); + const std::time_t t = static_cast(utcTime - cycles * cycleSeconds); + if (localtime_s(&local, &t) != 0) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime)); + long standardBias = 0; + long daylightBias = 0; + _get_timezone(&standardBias); + _get_dstbias(&daylightBias); + return {-static_cast(standardBias), local.tm_isdst > 0 ? -static_cast(daylightBias) : 0}; +#else + const std::time_t t = static_cast(utcTime); + if (localtime_r(&t, &local) == nullptr) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime)); + const std::int64_t daylight = local.tm_isdst > 0 ? static_cast(local.tm_gmtoff) + ::timezone : 0; + return {static_cast(local.tm_gmtoff) - daylight, daylight}; +#endif +} + +static KernelTimesec ZoneState(std::int64_t time, const HostZone& zone) { + return {time, static_cast(-zone.standardEast), static_cast(zone.daylight)}; +} + int APS5_VABI sceKernelClockGetres(KernelClockid clock_id, KernelTimespec* tp) { if (tp == nullptr) return SceKernelErrorEfault; if (clock_getres_nid_postfix(static_cast(clock_id), tp) != 0) return SceKernelError(*__error_nid_postfix()); @@ -572,16 +601,19 @@ int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp) int APS5_VABI sceKernelConvertLocaltimeToUtc(int64_t local_time, int64_t reserved, int64_t* utc_time, KernelTimesec* timezone, int32_t* dst_seconds) { (void)reserved; - if (utc_time != nullptr) *utc_time = local_time; - if (timezone != nullptr) *timezone = {local_time, 0u, 0u}; - if (dst_seconds != nullptr) *dst_seconds = 0; + const HostZone guess = HostZoneAt(local_time); + const HostZone zone = HostZoneAt(local_time - guess.standardEast - guess.daylight); + if (utc_time != nullptr) *utc_time = local_time - zone.standardEast - zone.daylight; + if (timezone != nullptr) *timezone = ZoneState(local_time, zone); + if (dst_seconds != nullptr) *dst_seconds = static_cast(zone.daylight); return 0; } int APS5_VABI sceKernelConvertUtcToLocaltime(int64_t utc_time, int64_t* local_time, KernelTimesec* st, int32_t* dst_sec) { - if (local_time != nullptr) *local_time = utc_time; - if (st != nullptr) *st = {utc_time, 0u, 0u}; - if (dst_sec != nullptr) *dst_sec = 0; + const HostZone zone = HostZoneAt(utc_time); + if (local_time != nullptr) *local_time = utc_time + zone.standardEast + zone.daylight; + if (st != nullptr) *st = ZoneState(utc_time, zone); + if (dst_sec != nullptr) *dst_sec = static_cast(zone.daylight); return 0; } diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 9353846f08..0dbf9b95f8 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include extern "C" { @@ -36,6 +37,16 @@ static constexpr std::int64_t GIVE_UP_NANOS = 10 * NANOS_PER_SECOND; static void Require(bool value) { if (!value) std::abort(); } +static void SetTimeZone(const char* zone) { +#ifdef _WIN32 + Require(_putenv_s("TZ", zone) == 0); + _tzset(); +#else + Require(setenv("TZ", zone, 1) == 0); + tzset(); +#endif +} + static bool OmittedResolutionSucceeds() { constexpr int clocks[] = {0, 1, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 6, 9999, -1, INT_MIN, INT_MAX}; for (const int clock : clocks) { @@ -173,6 +184,7 @@ static void XtimeTicksAreGettimeofdayMicroseconds() { } static void CalendarConversionsInitializeTheirState() { + SetTimeZone("UTC0"); constexpr std::uint64_t sentinel = 0xa5a5a5a5a5a5a5a5ull; struct GuardedState { std::uint64_t before; @@ -205,10 +217,38 @@ static void CalendarConversionsInitializeTheirState() { Require(sceKernelConvertUtcToLocaltime(0, nullptr, nullptr, nullptr) == SCE_OK); } +static void RequireZone(const char* zone, std::int64_t utc, std::int64_t east, std::int32_t daylight) { + SetTimeZone(zone); + std::int64_t local = -1; + KernelTimesec universal{-1, 0xa5a5a5a5u, 0xa5a5a5a5u}; + std::int32_t universalDst = -1; + Require(sceKernelConvertUtcToLocaltime(utc, &local, &universal, &universalDst) == SCE_OK); + Require(local == utc + east && universalDst == daylight); + Require(universal.t == utc && universal.west_sec == static_cast(daylight - east) && universal.dst_sec == static_cast(daylight)); + std::int64_t converted = -1; + KernelTimesec state{-1, 0xa5a5a5a5u, 0xa5a5a5a5u}; + std::int32_t dst = -1; + Require(sceKernelConvertLocaltimeToUtc(local, 0, &converted, &state, &dst) == SCE_OK); + Require(converted == utc && dst == daylight); + Require(state.t == local && state.west_sec == universal.west_sec && state.dst_sec == universal.dst_sec); +} + +static void CalendarConversionsFollowTheHostZone() { + constexpr std::int64_t winter = 1768478400; + constexpr std::int64_t summer = 1782907200; + RequireZone("UTC0", summer, 0, 0); + RequireZone("IST-5:30", winter, 19800, 0); + RequireZone("BRT+3", summer, -10800, 0); + RequireZone("EST5EDT", winter, -18000, 0); + RequireZone("EST5EDT", summer, -14400, 3600); + SetTimeZone("UTC0"); +} + int main() { if (!OmittedResolutionSucceeds()) return 1; UnsupportedResolutionStillThrows(); CalendarConversionsInitializeTheirState(); + CalendarConversionsFollowTheHostZone(); SecondClockReportsWholeSeconds(); KernelTimezone zone{-1, -1}; Require(gettimeofday_nid_postfix(nullptr, nullptr) == SCE_OK); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index be4a318e02..88ffb6ef9c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -89,6 +89,7 @@ - [sched_get_priority_max, sched_get_priority_min](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - measured on a PS5 (firmware 13.60): `SCHED_FIFO` (1) and `SCHED_RR` (3) report a maximum of 767 and a minimum of 256, and every other policy, `SCHED_OTHER` (2) included, returns -1 with `EINVAL`. `pthread_setschedparam` ignores the policy and does not check the priority, where the console rejects `SCHED_OTHER` and priorities below 256 with `EINVAL` - [inet_pton, inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result +- [sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) convert with the host time zone at the given instant, as libc `localtime` does, instead of the console's time zone setting. `west_sec` is assumed to be the standard-time offset in seconds west of UTC, as the name and FreeBSD's `tz_minuteswest` read, and `dst_sec` the daylight saving seconds in effect; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333/src/core/libraries/kernel/time.cpp) stores seconds east in `west_sec` and declares the fourth argument of `sceKernelConvertLocaltimeToUtc` as an 8-byte `timezone`, not the 16-byte `KernelTimesec`. Neither layout is verified on a console. In a repeated or skipped local hour, `sceKernelConvertLocaltimeToUtc` takes the offset in effect one standard offset earlier. `sceKernelGettimezone` and the `gettimeofday` time zone still report no offset and no daylight saving. A host time the C runtime cannot convert throws ### Functional