There's an issue with upstream tzdata 2026b that breaks date calculations
with America/Vancouver. Observed in mailx, but there are probably other
programs that do it. I haven't dug up just what's happening yet, but I'm
working on that. In the meantime, what I see is that gmtime(3) returns the
wrong data now if you're in America/Vancouver. As nothing should have
changed for now (July) gmtime(3) should return the same data for today -
the change takes effect in November:
/etc/localtime Sun Mar 8 09:59:59 2026 UT = Sun Mar 8 01:59:59 2026 PST isdst=0 gmtoff=-28800
/etc/localtime Sun Mar 8 10:00:00 2026 UT = Sun Mar 8 03:00:00 2026 PDT isdst=1 gmtoff=-25200
/etc/localtime Sun Nov 1 08:59:59 2026 UT = Sun Nov 1 01:59:59 2026 PDT isdst=1 gmtoff=-25200
-/etc/localtime Sun Nov 1 09:00:00 2026 UT = Sun Nov 1 01:00:00 2026 PST isdst=0 gmtoff=-28800
+/etc/localtime Sun Nov 1 09:00:00 2026 UT = Sun Nov 1 02:00:00 2026 MST isdst=0 gmtoff=-25200
As it stands, mailx will issue the wrong time offset, which can be
problematic for things. An observed example involved monitoring emails
discarded because they were assumed to be over an hour old. The following
short program models what mailx does to calculate the date offset:
---------------------------------------------------------------------------
#include <stdio.h>
#include <time.h>
int main(int argc, char *argv[])
{
time_t t;
struct tm *tmptr;
int tzdiff, tzdiff_hour, tzdiff_min;
time(&t);
tmptr = localtime(&t);
tzdiff = t - mktime(gmtime(&t));
tzdiff_hour = (int)(tzdiff / 60);
tzdiff_min = tzdiff_hour % 60;
tzdiff_hour /= 60;
if (tmptr->tm_isdst > 0)
tzdiff_hour++;
printf("t (epoch seconds) is %d\n", t);
printf("mktime(gmtime(&t)) is %d\n", mktime(gmtime(&t)));
printf("t - mktime(gmtime(&t)) is %d\n", tzdiff);
printf("tzdiff_hour (that / 3600) is %d\n", tzdiff_hour);
printf("tm_isdst is %d\n", tmptr->tm_isdst);
printf("%+05d\n", tzdiff_hour * 100 + tzdiff_min);
}
---------------------------------------------------------------------------
To reproduce the issue, set your timezone to America/Vancouver, and run the
program. With tzdata_2026b, it will show an offset of -0600, which is
incorrect. It should be -0600, and it showed -0700 with tzdata_2026a. This
shows that gmtime(3) cares about the change in some detrimental way.
tzdata_2026b is current. 2026a can be found here - thanks NickH for
pointing me to it:
https://snapshot.debian.org/archive/debian/20260403T024139Z/pool/main/t/tzdata/tzdata_2026a-3_all.deb
It's also possible Java has problems with the new timezone, but I don't
have enough data to issue a report yet.
control: reassign -1 libc6/2.36-9
control: retitle -1 mktime() heuristics fail with the America/Vancouver change
control: severity -1 normal
Hi,
The change is actually causing the mktime() heuristics in glibc to fail,
it's not linked to gmtime(3). But the code you submitted is also buggy.
The main issue is there. gmtime() returns a tm structure with tm_isdst
set to 0, which is not correct as the time (at least when executed
currently) includes a DST. With the previous tzdata version, the
mktime heuristics were able to fix it, but it is not the case anymore for
dates after 2026-06-06). It is not clear yet why.
The correct way is to force tm_isdst to -1, which ensure that the DST is
determined by mktime(). This will also work correctly with the POSIX 2024
version of mktime, which forbids applying any heuristics if tm_isdst is 0 or 1
(the behaviour was not fully clear for earlier POSIX versions).
With the above change, this should not be needed anymore. Anyway, this is
dangerous as tmptr got overwritten by the call to gmtime().
The following updated code should work in all cases:
#include <stdio.h>
#include <time.h>
int main(int argc, char *argv[])
{
time_t t;
struct tm *tmptr;
struct tm *gmtr;
int tzdiff, tzdiff_hour, tzdiff_min;
time(&t);
tmptr = localtime(&t);
gmtr = gmtime(&t);
gmtr->tm_isdst = -1;
tzdiff = t - mktime(gmtr);
tzdiff_hour = (int)(tzdiff / 60);
tzdiff_min = tzdiff_hour % 60;
tzdiff_hour /= 60;
printf("t (epoch seconds) is %d\n", t);
printf("mktime(gmtime(&t)) is %d\n", mktime(gmtime(&t)));
printf("t - mktime(gmtime(&t)) is %d\n", tzdiff);
printf("tzdiff_hour (that / 3600) is %d\n", tzdiff_hour);
printf("tm_isdst is %d\n", tmptr->tm_isdst);
printf("%+05d\n", tzdiff_hour * 100 + tzdiff_min);
}
Regards
Aurelien
Ah, hm. Thank you for the clarification.
Alright. For what it's worth, I tried to copy this with as little
modification as possible from Heirlom mailx, as shipped by RHEL. I don't
see equiovalent functionality in bsd-mailx. Here's the code, which RHEL
doesn't patch, so this is unmodified upstream:
/*
* Create a Date: header field.
* We compare the localtime() and gmtime() results to get the timezone,
* because numeric timezones are easier to read and because $TZ is
* not set on most GNU systems.
*/
int
mkdate(FILE *fo, const char *field)
{
time_t t;
struct tm *tmptr;
int tzdiff, tzdiff_hour, tzdiff_min;
time(&t);
tzdiff = t - mktime(gmtime(&t));
tzdiff_hour = (int)(tzdiff / 60);
tzdiff_min = tzdiff_hour % 60;
tzdiff_hour /= 60;
tmptr = localtime(&t);
if (tmptr->tm_isdst > 0)
tzdiff_hour++;
return fprintf(fo, "%s: %s, %02d %s %04d %02d:%02d:%02d %+05d\n",
field,
weekday_names[tmptr->tm_wday],
tmptr->tm_mday, month_names[tmptr->tm_mon],
tmptr->tm_year + 1900, tmptr->tm_hour,
tmptr->tm_min, tmptr->tm_sec,
tzdiff_hour * 100 + tzdiff_min);
}
Alright, that's interesting. I've pointed the IANA list to this bug, and if
there's additional discussion there I'll refresh my pointer. Thank you.
Useful. I'll figure out how to submit an upstream bug for Heirloom mailx,
I'll point them here, and then I'll point the Red Hat maintainers to the
upstream bug.
I'll still be quite interested in what's making glibc mktime(3) fail,
espeically given that FreeBSD seems to fail similarly.
Thank you again.
Paul Eggert made the Vancouver change on March 3. Robert Bastian of Unicode CLDR complained that the change stepped on CLDR's use of tm_isdst (which Eggert and - I think - POSIX consider deprecated). This gave rise to a long thread, and ultimately the installation of a temporary hack to accomodate CLDR in TZDB until they can make a release in autumn. I've chedked; the removal of that hack (committed on March 7) makes Mason's code work. I don't know what the CLDR status is, but there will probably be some discussion of it on the TZDB mailing list now. /Lars
Hi, [1] https://sourceware.org/bugzilla/show_bug.cgi?id=34480 So the remaining issue is due to the bugs in the code you submitted. I am therefore closing the bug. Regards Aurelien