Bug 31144 - mktime: returns clock for UTC with isdst=1
Summary: mktime: returns clock for UTC with isdst=1
Status: RESOLVED NOTABUG
Alias: None
Product: glibc
Classification: Unclassified
Component: time (show other bugs)
Version: unspecified
: P2 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL: http://bugs.debian.org/1057856
Keywords:
Depends on:
Blocks:
 
Reported: 2023-12-12 05:46 UTC by Aurelien Jarno
Modified: 2023-12-13 17:52 UTC (History)
2 users (show)

See Also:
Host:
Target:
Build:
Last reconfirmed:


Attachments
Reproducer (419 bytes, text/plain)
2023-12-12 05:46 UTC, Aurelien Jarno
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Aurelien Jarno 2023-12-12 05:46:31 UTC
Created attachment 15253 [details]
Reproducer

A Debian user reported a behaviour change of the mktime function affecting Erlang/OTP. When passing isdst=1 with an UTC timezone, a time is now returned (with isdst=0), while in the past it returned an error.

The issue has been tracked to the following commit 83859e1115269cf56d21669361d4ddbe2687831c which went into 2.37, and got backported to the 2.34, 2.35 and 2.36 branches.

The C standard is not fully clear about the behaviour in that case:

"A positive or 0 value for tm_isdst shall cause mktime() to presume initially that Daylight Savings Time, respectively, is or is not in effect for the specified time. A negative value for tm_isdst shall cause mktime() to attempt to determine whether Daylight Savings Time is in effect for the specified time."

However it appears that the original GNU libc behaviour before that commit matches the FreeBSD libc behaviour.

The user provided a small test program to exercise the issue (see attachement):

$ gcc -o check_dst check_dst.c
$ TZ=UTC ./check_dst 0 
tzname[0]: UTC, tzname[1]: UTC
        dst before: 0
        dst after: 0
        clock: 1217548800
$ TZ=UTC ./check_dst 1
tzname[0]: UTC, tzname[1]: UTC
        dst before: 1
        dst after: 0
        clock: 1217545200
Comment 1 Andreas Schwab 2023-12-12 09:40:42 UTC
tm_isdst is just a hint, to resolve ambigous time stamps around the DST rollback.
Comment 2 eggert 2023-12-12 23:37:01 UTC
The C and POSIX standards do not specify what mktime should do with examples like this, so this is not a conformance issue. As Andreas mentioned, tm_isdst is intended as a hint for ambiguous timestamps; unfortunately this does not suffice in general for TZDB Zones, e.g., 1971-10-31 02:30 when TZ='Europe/London' because at 03:00 standard time moved back by an hour in London so there are two distinct matching timestamps both with tm_isdst==0, one with time_t==57720600 and one with time_t==57724200. So the whole area is a bit of an ill-thought-out mess.

glibc currently supports the idea that if you want to find a time that's 1 year from now you can call localtime, add 1 to tm_year, and then call mktime on the result, and you'll get a timestamp that's 1 year later or thereabouts (the concept of "one year later" is not well-defined). This idea didn't work with older glibc (and I guess FreeBSD libc?) if DST is in use now but not 1 year from now (or vice versa) because in those cases older glibc mktime simply failed.

In practice the new glibc behavior seems to be more useful than the old one. The old behavior was changed in response to a real-world use case <https://bugs.gnu.org/48085> whereas the Erlang/OTP issue is an artificial test case which is a weaker argument.

With that in mind I'm going to resolve this as NOTABUG. The Erlang/OTP test case is wrong because it's making assumptions about mktime that are not present in the relative standards
Comment 3 Florian Weimer 2023-12-13 10:11:01 UTC
We already scan the entire tables while loading the time zone data, so we could make a note if we have seen any DST hints, and if there are none present, we could fail as before for tm_isdst > 0. We also don't know how the tz project will model the introduction of DST if a zone changes introduces DST for the first time, so the synthetic offset might well be wrong.
Comment 4 eggert 2023-12-13 17:52:21 UTC
(In reply to Florian Weimer from comment #3)
> We already scan the entire tables while loading the time zone data, so we
> could make a note if we have seen any DST hints, and if there are none
> present, we could fail as before for tm_isdst > 0. We also don't know how
> the tz project will model the introduction of DST if a zone changes
> introduces DST for the first time, so the synthetic offset might well be
> wrong.
Yes, that could be done and shouldn't be hard to do. If so, we'd need to consult the trailing TZ string as well as the tm_isdst flags in the binary tables.

However, failing would not be good, as we already learned when we put in code to have mktime fail. This is because a lot of code expects mktime to succeed unless the resulting time_t would be out of range. Better would be to ignore tm_isdst in timezones that have never observed tm_isdst.

> We also don't know how the tz project will model the introduction of DST
> if a zone changes introduces DST for the first time,
> so the synthetic offset might well be wrong.
There's no way in general to determine a synthetic offset. For example, America/St_Johns observed 1-hour DST in summer 1987 and 2-hour DST in summer 1988. If mktime is given a timestamp in January 1988 with tm_isdst=1 should it use a 1-hour offset or a 2-hour offset? That would depend on whether the timestamp was computed from in the previous summer but adding a few months, or from the next summer but subtracting a few months. Both scenarios are plausible.

One possibility is that glibc mktime could use the incoming tm_gmtoff as a hint as to what synthetic offset to use. If the incoming tm_gmtoff is one of the DST offsets that appears in the TZif's binary data or TZ string, then mktime could use that offset. Although this would in theory rely on undefined behavior (tm_gmtoff might be uninitialized), in practice it would likely work better than what glibc does now. If this possibility were implemented lazily it shouldn't slow down the typical mktime case.