Repository navigation
Conversation
robobun
force-pushed
the
robobun/13b3b63c/process-time-zone-offset
branch
2 times, most recently
from
October 23, 2026 19:01
f4d90a0 to
e028c17
Compare
|
Preview build of 66eecb2: |
retrieveTimeZoneInformation() resolves the host time zone by name only.
ucal_getHostTimeZone() returns the ID of the zone ICU detects. When that
ID is not a zone name, the function keeps a default-constructed TimeZone,
which is UTC, with no error. That is the case for a POSIX TZ value
("MSK-3", "EST5"), for a TZ that is the path of a tzfile
(":/etc/localtime"), and for an /etc/localtime that no zoneinfo path
names. V8 does not have this problem: it keeps the zone object ICU makes,
which has the offset of the C library.
Three more results of the same lookup are wrong:
- An override that WTF::setTimeZoneOverride() admits and that has no IANA
name ("GMT+05:30", "SystemV/EST5") is UTC.
- With TZ unset and /etc/localtime a regular file, ICU guesses a zone
from the abbreviation. "CST" on a host in China is America/Chicago.
- With a TZ that ICU cannot read as a name (a POSIX value with a DST
part), ICU names /etc/localtime, and that zone is used.
The change is under USE(BUN_JSC_ADDITIONS). Upstream's lines stay as the
other branch.
- An override with no IANA name is the zone at the offset that ICU has
for the ID.
- For the host, TZ is read first, as the C library reads it. A zone name
in it is the zone. A path into a zoneinfo directory is the zone the
path names after "/zoneinfo/". ICU hands a name in TZ back as the host
ID on Linux, but not on every platform, not for a path with a digit
before its end, and from ICU 80 not as a path (ICU-13694).
- With no TZ, a long name from ICU is the zone, as before: it is the name
in the path /etc/localtime links to, or of the zoneinfo file with the
same content.
- Every other name from ICU is a guess: an abbreviation with TZ unset,
any name when TZ names no zone. A guess stays only if it tells the same
time as the C library in the middle of January and of July of this
year. The same test applies to the zone that a link such as
":/etc/localtime" points to: in a container with the host's
/etc/localtime mounted over the target of that link, the path names the
zone of the image and the file is the zone of the host. ICU has this
rule for abbreviations, with the raw offset only.
- What is left is a zone the C library knows and nothing names. It is the
zone at the standard offset of the C library.
- A fixed offset is UTC for zero, the IANA zone for whole hours
(Etc/GMT-14 to Etc/GMT+12), and an offset time zone ("+05:30") for the
rest. Date, Intl.DateTimeFormat and Temporal.Now read the same zone.
A zone with a daylight saving rule and no name (a POSIX value such as
"CET-1CEST,M3.5.0,M10.5.0/3" on a host where ICU finds no zone with that
time, a tzfile outside a zoneinfo directory) has its standard offset
only. TimeZone is a name or a fixed offset, as the time zone of
ECMA-262 is.
Tests: JSTests/complex/host-timezone-*.js,
JSTests/stress/host-time-zone-fixed-offset.js
See oven-sh/bun#3649.
robobun
force-pushed
the
robobun/13b3b63c/process-time-zone-offset
branch
from
October 24, 2026 05:13
e028c17 to
66eecb2
Compare
This was referenced Oct 24, 2026
Collaborator
Author
|
Reworked in 66eecb2, and rebased on main:
The three test lanes pass on this commit. The PR stays a draft: it needs a review from someone who owns the fork, and the body lists the open points. Bun's pin must not move here before oven-sh/bun#46698 merges. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
TZ=MSK-3 jsc -e 'print(new Date().getTimezoneOffset())'prints0. Node prints-180.retrieveTimeZoneInformation()(runtime/JSDateMath.cpp) resolves the ID fromucal_getHostTimeZone()by name. An ID with no name (a POSIX TZ value, a tzfile path, the overrideGMT+05:30) is UTC, with no error.Fix
USE(BUN_JSC_ADDITIONS). An override with no IANA name is the zone at ICU's offset for it.TZis read first: a zone name in it, or in its path after/zoneinfo/, is the zone. A name that ICU guessed, or that a link such as:/etc/localtimepoints to, stays only if it tells the C library's time in January and July. The rest is the C library's standard offset.complex.yamlpass.Background
TimeZoneis a name or a fixed offset, as in ECMA-262./zoneinfo/path in TZ itself (ICU-13694). So the path is read fromgetenv("TZ"), not from the host ID.TZwriter through one setter that readsMSK-3and:namebun#46698 merges. Bun's start gives the rawTZtoWTF::setTimeZoneOverride()today, andTZ=GMT+3would become three hours east.Downsides
TZ=:/etc/localtimethat is a regular file. Before: UTC.ucal_open, 20,843 allocations, before and after.JSDateMath.cpp.o: +7,240 bytes.Notes
Results in the
jscshellLinux x64, glibc 2.41, ICU 78.3,
/etc/localtime -> /usr/share/zoneinfo/Etc/UTC. The values aregetTimezoneOffset()for 09:00 local time on 2026-01-15 and on 2026-07-15, andIntl.DateTimeFormat().resolvedOptions().timeZone.date(1)MSK-3UTCEtc/GMT-3EST5UTCEtc/GMT+5GMT+3UTCEtc/GMT+3XXX+13UTC-13:00<+03>-3UTCEtc/GMT-3IST-5:30UTC+05:30CET-1CEST,M3.5.0,M10.5.0/3UTCEtc/GMT-1:/usr/share/zoneinfo/America/New_YorkUTCAmerica/New_York/nix/store/tzdata-2025a/share/zoneinfo/Asia/Seoul(no such file)UTCAsia/Seoul:/tmp/linktozoneinfo/Europe/BerlinUTCEurope/Berlin:/tmp/copyofAmerica/New_YorkUTCEtc/GMT+5Europe/Moscow,EST,CET,Cuba,PRC,ACT,EST5EDT,Etc/GMT+5Node v26.3.0 on the same host has the offset of
MSK-3,EST5,GMT+3andXXX+13, reportsundefinedfor the zone, has no daylight saving time for the three path forms, and is UTC for<+03>-3,IST-5:30and the rule form.A POSIX value with a daylight saving rule
ICU does not read such a TZ. It names /etc/localtime, or it guesses from the abbreviations when there is no such file. On a host with no /etc/localtime,
TZ=CET-1CEST,M3.5.0,M10.5.0/3isEurope/Brusselsbefore and after this change (-60/-120), because ICU knowsCETand that zone tells the time that the C library tells. On a host where /etc/localtime is another zone, the result was that other zone. It is now the standard offset of the value.TZ unset, /etc/localtime a regular file, no zoneinfo directory
Each of the 312 zones of
zone1970.tabwas copied to /etc/localtime in a private mount namespace, with /usr/share/zoneinfo hidden. The first column counts the zones for which both offsets above are the ones ofdate(1). The second counts the zones for whichgetTimezoneOffset()is the offset oflocaltime()at every hour of 2026.No zone that was exact before is wrong after, by either count. Of the 35 zones that are not exact at every hour, 24 have daylight saving time and get the standard offset. The other 11 have a long name from ICU's abbreviation table with other transition dates:
Africa/CairoisEurope/Athens,America/Mexico_CityisAmerica/Chicago. They are the same before this change. A long name is not checked, because ICU also returns a long name for the common case, a link into a zoneinfo directory, and that case must not cost more. The two instants are a test that a guess is plausible. They are not a proof that it is the host's zone.With the zoneinfo directory present, all 312 zones are exact at the two instants before and after.
Why not the raw offset
ICU compares the raw offset of a guessed zone with libc's
timezone.timezoneis the standard offset after the last rule of the tzfile. ForAmerica/Vancouverin tzdata 2025c it is -7 hours, the permanent offset from November 2026, while January 2026 is at -8. With that test, a host onAmerica/Vancouverthat ICU namesAmerica/Los_Angeles(correct for 2026) would get a fixed -7. The test here is the offset thatlocaltime_r()gives in the middle of January and of July of this year, against the offset of the guessed zone at the same times.A name that TZ gives is never checked.
TZ=ESTon a Debian host withouttzdata-legacyis -5 hours to ICU and UTC to the C library, and it stays -5 hours./etc/localtime of the host mounted into a container
docker run -v /etc/localtime:/etc/localtime:roon an image where /etc/localtime is a link (Debian: to/usr/share/zoneinfo/Etc/UTC) mounts the file of the host over the target of the link. The path then namesEtc/UTC, and the file is the zone of the host. The C library reads the file.TZ=:/etc/localtime, the file isAustralia/SydneyAsia/KolkataUTCUTCEtc/GMT-10+05:30date(1)With TZ unset in that container, ICU names
Etc/UTCfrom the link, and the zone is UTC before and after, as in node. A long name with TZ unset is not checked (see above).Not in this change
TimeZonecannot hold. It gets the standard offset. WithTZ=:/etc/localtimeand /etc/localtime a regular file, that is 105 of the 312 zones.WTF::setTimeZoneOverride()does not change. An earlier revision made it admit+05:30. Bun can map that toGMT+05:30itself.Open points for the review
Bun__hook with the policy in Bun, asBun__reportUnhandledErroris. It is here because thejscshell and JSTests then cover it, and because the defect is the engine's: upstream has the same lookup.