Considering local time.
In testing some time code with the potential hundreds of time zones, some of them result in a mktime()
call that never returns for select values!
Detail: Code never returns from time_t t = mktime(&tm)
of print_time()
. CPU keeps spinning.
These tm
are all in the case when the time zone advances and hour, but not due to daylight savings time.
The 3 cases below are all in Africa times zones. I suspect there are more, but these 3 should be enough to demo the bug that may exist in many time zones.
I'd expect something to be promptly returned.
Do you see the same no return behavior?
How to best report this bug?
Interestingly if the test uses 1
or 0
for the .tm_isdst
, mktime()
returns.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
const char *tzname2 = "x";
void timezone_set(const char *tz) {
if (setenv("TZ", tz, 1 /* overwrite */)) {
fprintf(stderr, "Unable to set timezone '%s'\n", tz);
exit(EXIT_FAILURE);
}
tzset();
tzname2 = tz;
puts("");
puts(tz);
}
void print_time(struct tm tm, const char *s) {
printf("%10s %4d/%02d/%02d %2d:%02d:%02d dst:%2d", s,
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
tm.tm_sec, tm.tm_isdst);
fflush(stdout);
time_t t = mktime(&tm); // Does not always return.
printf("--> %10lld %4d/%02d/%02d %2d:%02d:%02d dst:%2d\n", (long long) t,
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
tm.tm_sec, tm.tm_isdst);
fflush(stdout);
}
void tz_test(int year, int month, int mday, int hour, int min, int sec, int dst) {
for (int h = 0; h < 5; h++) {
struct tm tm = {.tm_year = year - 1900, .tm_mon = month - 1, .tm_mday = mday,
.tm_hour = hour, .tm_min = min, .tm_sec = sec, .tm_isdst = dst};
tm.tm_hour = hour + h;
print_time(tm, "Local");
}
puts("");
fflush(stdout);
}
int main(void) {
// https://timezonedb.com/time-zones/Africa/Algiers
timezone_set("Africa/Algiers");
tz_test(1971, 4, 25, 23, 0, 0, -1);
// https://timezonedb.com/time-zones/Africa/Tripoli
timezone_set("Africa/Tripoli");
tz_test(1982, 4, 1, 0, 0, 0, -1);
// https://timezonedb.com/time-zones/Africa/Windhoek
timezone_set("Africa/Windhoek");
tz_test(1994, 9, 4, 2, 0, 0, -1);
return 0;
}
Sample output:
Africa/Algiers
Local 1971/04/25 23:00:00 dst:-1
Notes:
/usr/lib/gcc/x86_64-pc-cygwin/12/include
/usr/include
/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../lib/../include/w32api
End of search list.
GNU C17 (GCC) version 12.4.0 (x86_64-pc-cygwin)
compiled by GNU C version 12.4.0, GMP version 6.3.0, MPFR version 4.2.1, MPC version 1.3.1, isl version isl-0.26-GMP
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 2db83001ac4ad148ae13aae27a04c021
COLLECT_GCC_OPTIONS='-O0' '-g3' '-Wpedantic' '-Wall' '-Wextra' '-Wconversion' '-Wsign-conversion' '-c' '-std=c17' '-fmessage-length=0' '-Wformat=1' '-Wformat-security' '-Wformat=2' '-Wmaybe-uninitialized' '-Werror=stringop-truncation' '-v' '-MMD' '-MP' '-MF' 'mystrtod.d' '-MT' 'mystrtod.o' '-o' 'mystrtod.o' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/bin/as.exe -v --gdwarf-5 -o mystrtod.o /cygdrive/c/Users/TPC/AppData/Local/Temp/cccEEI89.s
GNU assembler version 2.43 (x86_64-pc-cygwin) using BFD version (GNU Binutils) 2.43
COMPILER_PATH=/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/:/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/bin/
LIBRARY_PATH=/usr/lib/gcc/x86_64-pc-cygwin/12/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/lib/../lib/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../lib/:/lib/../lib/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../../x86_64-pc-cygwin/lib/:/usr/lib/gcc/x86_64-pc-cygwin/12/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-O0' '-g3' '-Wpedantic' '-Wall' '-Wextra' '-Wconversion' '-Wsign-conversion' '-c' '-std=c17' '-fmessage-length=0' '-Wformat=1' '-Wformat-security' '-Wformat=2' '-Wmaybe-uninitialized' '-Werror=stringop-truncation' '-v' '-MMD' '-MP' '-MF' 'mystrtod.d' '-MT' 'mystrtod.o' '-o' 'mystrtod.o' '-mtune=generic' '-march=x86-64' '-dumpdir' 'mystrtod.'
Calendars are a bunch of cycles (solar cycles, lunar cycles, arbitrary cycles) that we try to force into synchronization. Already hard. The arbitrary decisions of time zones and daylight savings time make that even harder. Not just because they're arbitrary, solvable with a database, but because they cause discontinuities in the calendar. You can write a perfectly valid seeming calendar time and time zone which never happened, or which happened twice. time.h doesn't always deal with them correctly or at all.
This happens twice a year with places that do daylight savings time. In most of the US it was Sunday, March 10, 2024, 1:59:59, and the next second later it was Sunday, March 10, 2024, 3:00:00. There was no Sunday, March 10, 2024, 2:00:00. No Sunday, March 10, 2024, 2:00:01.
Similarly, when clocks reach Sunday, November 3, 2024, 1:59:59 the next second will be Sunday, November 3, 2024, 1:00:00. Then when they reach Sunday, November 3, 2024, 1:59:59 again the next second will be Sunday, November 3, 2024, 2:00:00. It will have been Sunday, November 3, 2024, 1:00:00 twice. And Sunday, November 3, 2024, 1:00:01 twice. And Sunday, November 3, 2024, 1:59:59 twice.
mktime
and localtime
are supposed to round-trip; you should be able to put the time_t
returned by mktime
back into localtime
and get the same original tm
back. But these discontinuities mess that up. This can result in creative values or crashes or infinite loops.
You're asking mktime
to convert calendar times which do not exist. I don't mean because you're asking for 25 o'clock, that's a different thing. I mean moving clocks forward an hour means there are combinations of time zones, dates, and times which do not map to a point in time_t. Moving clocks backwards means one combination of time zone, date, and time can map to two points in time. While time_t
is a steady beat of seconds since midnight January 1st, 1970 UTC.
For example, if we look at Africa/Algers...
After Sunday, 25 April, 1971 10:59:59 PM:
Clocks were moved forward to become Monday, 26 April, 1971 12:00:00 AM
That means in Africa/Algers on 1971/04/25 11 PM to 11:59:59 PM did not happen. There was no 1971/04/25 11:00 PM nor 1971/04/25 11:01 PM nor 1971/04/25 11:59 PM. There is no time_t
you can put into localtime
to correctly get back a tm
of 1971/04/25 11 PM in Africa/Algers.
Similar for Africa/Tripoli
After Wednesday, 31 March, 1982 11:59:59 PM:
Clocks were moved forward to become Thursday, 01 April, 1982 01:00:00 AM
1982/04/01 0:00:00 did not happen in Africa/Tripoli.
And Africa/Windhoek.
After Sunday, 04 September, 1994 01:59:59 AM:
Clocks were moved forward to become Sunday, 04 September, 1994 03:00:00 AM
1994/09/04 2:00:00 did not happen in Africa/Windhoek.
C Standard 7.27.2.3.3 says...
The mktime function returns the specified calendar time encoded as a value of type time_t. If the calendar time cannot be represented, the function returns the value (time_t)(−1).
When you ask mktime
for a calendar time which did not happen you should get back -1, or its equivalent after the jump. For example, on MacOS...
// After Sunday, 25 April, 1971 10:59:59 PM
// Clocks were moved forward to become Monday, 26 April, 1971 12:00:00 AM
Africa/Algiers
Local 1971/04/25 23:00:00 dst:-1--> 41468400 1971/04/26 0:00:00 dst: 1
Local 1971/04/25 24:00:00 dst:-1--> 41468400 1971/04/26 0:00:00 dst: 1
Local 1971/04/25 25:00:00 dst:-1--> 41472000 1971/04/26 1:00:00 dst: 1
Local 1971/04/25 26:00:00 dst:-1--> 41475600 1971/04/26 2:00:00 dst: 1
Local 1971/04/25 27:00:00 dst:-1--> 41479200 1971/04/26 3:00:00 dst: 1
It has interpreted the non-existent 1971/04/25 23:00:00 to be the existent 1971/04/26 0:00:00. Presumably under the logic that 41468399 is 1971/04/25 22:59:59 and 1971/04/25 23:00:00 is one second later, so it should use localtime(41468400)
which is 1971/04/26 0:00:00.
However a particular mktime
deals with these discontinuities, it definitely shouldn't hang. If it hangs that indicates a bug in the library. mktime
might loop through progressively better guesses and the discontinuity has got it caught in an infinite loop.
FWIW here's what MacOS returns.
// After Sunday, 25 April, 1971 10:59:59 PM
// Clocks were moved forward to become Monday, 26 April, 1971 12:00:00 AM
Africa/Algiers
Local 1971/04/25 23:00:00 dst:-1--> 41468400 1971/04/26 0:00:00 dst: 1
Local 1971/04/25 24:00:00 dst:-1--> 41468400 1971/04/26 0:00:00 dst: 1
Local 1971/04/25 25:00:00 dst:-1--> 41472000 1971/04/26 1:00:00 dst: 1
Local 1971/04/25 26:00:00 dst:-1--> 41475600 1971/04/26 2:00:00 dst: 1
Local 1971/04/25 27:00:00 dst:-1--> 41479200 1971/04/26 3:00:00 dst: 1
// After Wednesday, 31 March, 1982 11:59:59 PM
// Clocks were moved forward to become Thursday, 01 April, 1982 01:00:00 AM
Africa/Tripoli
Local 1982/04/01 0:00:00 dst:-1--> 386463600 1982/04/01 1:00:00 dst: 1
Local 1982/04/01 1:00:00 dst:-1--> 386463600 1982/04/01 1:00:00 dst: 1
Local 1982/04/01 2:00:00 dst:-1--> 386467200 1982/04/01 2:00:00 dst: 1
Local 1982/04/01 3:00:00 dst:-1--> 386470800 1982/04/01 3:00:00 dst: 1
Local 1982/04/01 4:00:00 dst:-1--> 386474400 1982/04/01 4:00:00 dst: 1
// After Sunday, 04 September, 1994 01:59:59 AM
// Clocks were moved forward to become Sunday, 04 September, 1994 03:00:00 AM
Africa/Windhoek
Local 1994/09/04 2:00:00 dst:-1--> 778640400 1994/09/04 3:00:00 dst: 1
Local 1994/09/04 3:00:00 dst:-1--> 778640400 1994/09/04 3:00:00 dst: 1
Local 1994/09/04 4:00:00 dst:-1--> 778644000 1994/09/04 4:00:00 dst: 1
Local 1994/09/04 5:00:00 dst:-1--> 778647600 1994/09/04 5:00:00 dst: 1
Local 1994/09/04 6:00:00 dst:-1--> 778651200 1994/09/04 6:00:00 dst: 1
It does cause the weird situation that two different calendar times map to the same time_t. This should only happen when clocks move backward not forward.
Maybe it's because you're right on the boundary? What if we ask for 1971/04/25 23:01:02? Clearly a time which does not exist...
// After Sunday, 25 April, 1971 10:59:59 PM
// Clocks were moved forward to become Monday, 26 April, 1971 12:00:00 AM
Africa/Algiers
Local 1971/04/25 23:01:02 dst:-1--> 41468462 1971/04/26 0:01:02 dst: 1
Local 1971/04/25 24:01:02 dst:-1--> 41468462 1971/04/26 0:01:02 dst: 1
Local 1971/04/25 25:01:02 dst:-1--> 41472062 1971/04/26 1:01:02 dst: 1
Local 1971/04/25 26:01:02 dst:-1--> 41475662 1971/04/26 2:01:02 dst: 1
Local 1971/04/25 27:01:02 dst:-1--> 41479262 1971/04/26 3:01:02 dst: 1
// After Wednesday, 31 March, 1982 11:59:59 PM
// Clocks were moved forward to become Thursday, 01 April, 1982 01:00:00 AM
Africa/Tripoli
Local 1982/04/01 0:01:02 dst:-1--> 386463662 1982/04/01 1:01:02 dst: 1
Local 1982/04/01 1:01:02 dst:-1--> 386463662 1982/04/01 1:01:02 dst: 1
Local 1982/04/01 2:01:02 dst:-1--> 386467262 1982/04/01 2:01:02 dst: 1
Local 1982/04/01 3:01:02 dst:-1--> 386470862 1982/04/01 3:01:02 dst: 1
Local 1982/04/01 4:01:02 dst:-1--> 386474462 1982/04/01 4:01:02 dst: 1
// After Sunday, 04 September, 1994 01:59:59 AM
// Clocks were moved forward to become Sunday, 04 September, 1994 03:00:00 AM
Africa/Windhoek
Local 1994/09/04 2:01:02 dst:-1--> 778640462 1994/09/04 3:01:02 dst: 1
Local 1994/09/04 3:01:02 dst:-1--> 778640462 1994/09/04 3:01:02 dst: 1
Local 1994/09/04 4:01:02 dst:-1--> 778644062 1994/09/04 4:01:02 dst: 1
Local 1994/09/04 5:01:02 dst:-1--> 778647662 1994/09/04 5:01:02 dst: 1
Local 1994/09/04 6:01:02 dst:-1--> 778651262 1994/09/04 6:01:02 dst: 1
Same thing, now even more questionable, but better than hanging.