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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iLxaSz4qjmirfqKZdhVQk
This commit is contained in:
Claude committed 2026-10-13 11:27:28 +00:00
1 parent 5d50886ee4
commit 56c8f346fb
3 files changed
+81 -8

No files matched your search

+40 -8
View File
@@ -10,6 +10,7 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <functional>
#include <mutex>
#include <unordered_map>
@@ -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<int>(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<std::time_t>(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<std::int64_t>(standardBias), local.tm_isdst > 0 ? -static_cast<std::int64_t>(daylightBias) : 0};
#else
const std::time_t t = static_cast<std::time_t>(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<std::int64_t>(local.tm_gmtoff) + ::timezone : 0;
return {static_cast<std::int64_t>(local.tm_gmtoff) - daylight, daylight};
#endif
}
static KernelTimesec ZoneState(std::int64_t time, const HostZone& zone) {
return {time, static_cast<std::uint32_t>(-zone.standardEast), static_cast<std::uint32_t>(zone.daylight)};
}
int APS5_VABI sceKernelClockGetres(KernelClockid clock_id, KernelTimespec* tp) {
if (tp == nullptr) return SceKernelErrorEfault;
if (clock_getres_nid_postfix(static_cast<int>(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<int32_t>(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<int32_t>(zone.daylight);
return 0;
}
+40
View File
@@ -4,6 +4,7 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <stdexcept>
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<std::uint32_t>(daylight - east) && universal.dst_sec == static_cast<std::uint32_t>(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);
+1
View File
@@ -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