Search code examples
cmktime

mktime() call never returns. Library bug?


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.'

Solution

  • Congratulations, you've found a bug about daylight savings time!

    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.

    I've found a lot of them.


    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.