2

I have the following file test.c:

 #define _POSIX_THREAD_SAFE_FUNCTIONS
 #include <time.h>
 #include <stdio.h>
 #include <string.h>
 #include <stdint.h>
 #include <inttypes.h>

 int main(int argc,char**argv) {
    struct tm t1, t2, t3;
    time_t w1, w2, w3;
    memset(&t1,0,sizeof(struct tm));
    memset(&t2,0,sizeof(struct tm));
    memset(&t3,0,sizeof(struct tm));
    w1 = 0;
    errno = 0;
    localtime_r(&w1,&t1);
    printf("localtime_r: errno=%d\n",errno);
    errno = 0;
    w2 = mktime(&t1);
    printf("mktime: errno=%d result=%" PRId64 "\n",errno,((int64_t)w2));
    errno = 0;
    localtime_r(&w2,&t2);
    printf("localtime_r: errno=%d\n",errno);
    errno = 0;
    w3 = mktime(&t2);
    printf("mktime: errno=%d result=%" PRId64 "\n",errno,((int64_t)w3));
    errno = 0;
    localtime_r(&w3,&t3);
    printf("localtime_r: errno=%d\n",errno);
    printf("sizeof(time_t)=%" PRId64 "\n", ((int64_t)sizeof(time_t)));
    printf("W1=%" PRId64 " W2=%" PRId64 " W3=%" PRId64 "\n",((int64_t)w1),((int64_t)w2),((int64_t)w3));
    printf("Y1=%d Y2=%d Y3=%d\n",t1.tm_year,t2.tm_year,t3.tm_year);
    return 0;
 }

I compile it like this:

i686-w64-mingw32-gcc -D__MINGW_USE_VC2005_COMPAT=1 -o test.exe test.c

Note, i686-w64-mingw32-gcc --version reports 8.3-win32 20190406 This is running in a Docker image of Ubuntu 19.04, using the MinGW version that comes with Ubuntu 19.04 (it says version 6.0.0-3).

I have a Windows 10 VM (Version 1809 OS Build 17763.379). By default, time zone is set to US Pacific Time (UTC-8). I copy test.exe to this VM and run it there.

It prints:

localtime_r: errno=0
mktime: errno=0 result=0
localtime_r: errno=0
mktime: errno=0 result=0
localtime_r: errno=0
sizeof(time_t)=8
W1=0 W2=0 W3=0
Y1=69 Y2=69 Y3=69

That's the expected result. (At UTC midnight on 1 Jan 1970, it was still 1969 in UTC-8.)

I change the Windows time zone to UTC+10 (Canberra, Melbourne, Sydney). Run it again. It prints:

localtime_r: errno=0
mktime: errno=0 result=47244640256
localtime_r: errno=22
mktime: errno=22 result=4294967295
localtime_r: errno=0
sizeof(time_t)=8
W1=0 W2=47244640256 W3=4294967295
Y1=70 Y2=-1 Y3=206

It seems the mktime() call is returning an invalid value in UTC+10 time zone, but returns the correct value of 0 in UTC-8 time zone.

Why does this code work in one timezone break in another?

Note, this is only a problem with -D__MINGW_USE_VC2005_COMPAT=1 to enable 64-bit time_t. If I leave that out, which means 32-bit time_t, then the code works in both timezones. (But, 32-bit time_t is not a good idea, because it breaks in the year 2038, and that's less than twenty years away now.)

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
Simon Kissane
  • 4,373
  • 3
  • 34
  • 59
  • what if you use : `printf("W1=%lld W2=%lld W3=%lld\n",((long long) w1),((long long) w2),((long long) w3));` ? – Sander De Dycker Sep 11 '19 at 09:26
  • @SanderDeDycker Thanks, you are right. `%d` is the wrong printf format to use because it is a 64-bit value. Going to update my question with adjusted code, the problem appears a bit different after doing this. – Simon Kissane Sep 11 '19 at 09:47
  • Perhaps [For portable code tzset(3) should be called before localtime_r()](https://linux.die.net/man/3/localtime_r) applies? – chux - Reinstate Monica Sep 11 '19 at 10:38
  • @chux yes you are right about PRId64 for errno being overkill. I had a problem before I was using %d for 64-bit values, I went a bit overboard in remedying that. Changed it now. – Simon Kissane Sep 11 '19 at 10:43
  • As a matter of good investigation of why things may be failing, it would make sense to check the return value of the various `localtime_r()` calls as it "returns the value described, or NULL ... in case an error was detected." – chux - Reinstate Monica Sep 11 '19 at 10:45
  • @chux: Thanks for your suggestions. I tried adding `tzset()`, it made no difference. I also tried checking the return value of `localtime_r`. When it leaves errno as 0, it returns the expected pointer. When it sets errno to 22 (which I believe is EINVAL), the return value is NULL (for the second localtime_r call), or ((struct tm*)-1) (for the third one.) – Simon Kissane Sep 11 '19 at 10:53
  • 1
    Note: 47244640256 == 0xB00000000, certainly an error value. I suspect problem is simplified to why does `w2 = mktime(&t1);` act wonky. Might be illustrative to print the entire contents of `t1` before and after `mktime()`. Perhaps the prior `localtime_r(&w1,&t1);` put junk in `t1` - or is it `mktime(&t1);` that is errant? – chux - Reinstate Monica Sep 11 '19 at 11:11
  • 2
    Might it be that `mktime` returns a 32bit value (ie. `0x00000000`), but the caller expects a 64bit value (adding the `0xB` garbage) ? Ie. might this be an abi mismatch ? Or iow : was the library that implements `mktime` also built with `-D__MINGW_USE_VC2005_COMPAT=1` or equivalent ? – Sander De Dycker Sep 11 '19 at 11:38
  • @SanderDeDycker Thanks, that was it basically. When you `#define __MINGW_USE_VC2005_COMPAT 1`, then `localtime_s` is defined as an inline function which calls `_localtime64_s`. And `#define _POSIX_THREAD_SAFE_FUNCTIONS` defines `localtime_r` as an inline function which calls `localtime_s`. However, `mktime` is still 32-bit. To get 64-bit `mktime`, you need to also `#define __MSVCRT_VERSION__ 0x1400` (or higher). Once you do that, `mktime` becomes an inline function which calls `_mktime64`. Before that, `mktime` is a normal function declaration which is linked to the legacy 32-bit `mktime` – Simon Kissane Sep 11 '19 at 20:51
  • So `#define __MINGW_USE_VC2005_COMPAT 1` without `#define __MSVCRT_VERSION__ 0x1400` (or `-D` equivalent) gives you a `localtime_r` with 64-bit `time_t`, but a `mktime` with 32-bit `time_t`, which obviously won't work. (Or, I think even worse than that, the actual implementation of the `mktime` symbol is returning a 32-bit `time_t`, but the function declaration is for a 64-bit `time_t`, which is what causes the junk in the upper 32-bits.) – Simon Kissane Sep 11 '19 at 20:54
  • @SimonKissane : can you turn that information into an answer, so it serves others with the same issue ? Related : [Can I answer my own qustion ?](https://stackoverflow.com/help/self-answer) – Sander De Dycker Sep 12 '19 at 06:19
  • 1
    @SanderDeDycker Thanks, I just did that now. – Simon Kissane Sep 12 '19 at 07:56

1 Answers1

1

I worked out the cause of the problem. Sander De Dycker's suggestion, that mktime is returning a 32-bit value, is correct.

The problem is basically this: the MSVCRT defines three mktime functions: _mktime32 for 32-bit time_t, _mktime64 for 64-bit time_t, and _mktime which is a legacy alias for _mktime32.

_mingw.h does a #define _USE_32BIT_TIME_T in 32-bit code unless you #define __MINGW_USE_VC2005_COMPAT to disable that. Once you have #define __MINGW_USE_VC2005_COMPAT, then localtime_s is defined as an inline function which calls _localtime64_s. And #define _POSIX_THREAD_SAFE_FUNCTIONS defines localtime_r as an inline function which calls localtime_s. However, mktime is still 32-bit. To get 64-bit mktime, you need to also #define __MSVCRT_VERSION__ 0x1400 (or higher). Once you do that, mktime becomes an inline function which calls _mktime64. Before that, mktime is a normal function declaration which is linked to the legacy 32-bit mktime.

So #define __MINGW_USE_VC2005_COMPAT 1 without #define __MSVCRT_VERSION__ 0x1400 (or -D equivalent) gives you a localtime_r with 64-bit time_t, but a mktime with 32-bit time_t, which obviously won't work. Even worse than that, the actual implementation of the mktime symbol is returning a 32-bit time_t, but the function declaration is for a 64-bit time_t, which is what causes the junk in the upper 32-bits.

As to the difference behaviour in different time zones, I don't have a complete explanation for that, but I think the reason is likely as follows: when you have a function which actually returns a 32-bit value but is incorrectly being defined to return a 64-bit value, the upper 32-bits of the return value will hold random junk data left over from previous calculations. So, any difference in the previous calculations, or slightly different code paths, may result in different random junk. With a UTC-8 timezone, for whatever reason, the random junk is coincidentally zero, so the code (despite its incorrectness) actually works. With a UTC+10 timezone, the random junk turns out to be non-zero, which causes the rest of the code to stop working.

Simon Kissane
  • 4,373
  • 3
  • 34
  • 59