NetBSD Problem Report #60452

From wiz@netbsd.org  Thu Jul 16 12:39:26 2026
Return-Path: <wiz@netbsd.org>
Received: from mail.netbsd.org (mail.netbsd.org [199.233.217.200])
	(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
	 key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256
	 client-signature RSA-PSS (2048 bits) client-digest SHA256)
	(Client CN "mail.netbsd.org", Issuer "YR2" (not verified))
	by mollari.NetBSD.org (Postfix) with ESMTPS id CD5901A923B
	for <gnats-bugs@gnats.NetBSD.org>; Thu, 16 Jul 2026 12:39:26 +0000 (UTC)
Message-Id: <20260716123926.2681884DB2@mail.netbsd.org>
Date: Thu, 16 Jul 2026 12:39:26 +0000 (UTC)
From: wiz@NetBSD.org
Reply-To: wiz@NetBSD.org
To: gnats-bugs@NetBSD.org
Subject: gmtime, gmtime_r put wrong time zone into result
X-Send-Pr-Version: 3.95
X-From4GNATS: "wiz@NetBSD.org via gnats" <gnats-admin@NetBSD.org>

>Number:         60452
>Category:       lib
>Synopsis:       gmtime, gmtime_r put TZ into struct tm
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    lib-bug-people
>State:          analyzed
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Thu Jul 16 12:40:00 +0000 2026
>Closed-Date:    
>Last-Modified:  Thu Jul 16 21:25:01 +0000 2026
>Originator:     Thomas Klausner
>Release:        NetBSD 11.99.7/x86_64
>Organization:

>Environment:


Architecture: x86_64
Machine: amd64
>Description:
gmtime() and gmtime_r() are documented to return Coordinated
Universal Time (UTC), but they put the time zone offset of the
current timezone (from the system, or from the TZ environment
variable) into the resulting struct tm, while the actual time value
_is_ UTC.

I found this while debugging a jq test failure.
>How-To-Repeat:
Save this to jq.c (based on the actual code in jq, but heavily
simplified):

--- begin ---
#include <err.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
void f_gmtime(time_t secs, struct tm *tmp) {
  memset(tmp, 0, sizeof(*tmp));
  if (gmtime_r(&secs, tmp) == NULL)
    err(1, "gmtime_r failed");
}

char *f_strftime(time_t secs) {
  char buf[1000];
  struct tm tm;
  memset(&tm, 0, sizeof(struct tm));
  f_gmtime(secs, &tm);
  // struct tm *tmp;
  // tmp = gmtime(&secs);
  const char *fmt = "%FT%T%Z";
  size_t n = strftime(buf, sizeof(buf), fmt, &tm);
  // size_t n = strftime(buf, sizeof(buf), fmt, tmp);
  /* POSIX doesn't provide errno values for strftime() failures; weird */
  if ((n == 0) || n > sizeof(buf))
    err(1, "strftime failed");

  return strdup(buf);
}

void f_localtime(time_t secs, struct tm *tmp) {
  memset(tmp, 0, sizeof(*tmp));
  if (localtime_r(&secs, tmp) == NULL)
    err(1, "localtime_r failed");
}

char *f_strflocaltime(time_t secs) {
  struct tm tm;
  char buf[1000];
  f_localtime(secs, &tm);
  const char *fmt = "%FT%T%z";
  size_t n = strftime(buf, sizeof(buf), fmt, &tm);
  /* POSIX doesn't provide errno values for strftime() failures; weird */
  if ((n == 0) || n > sizeof(buf))
    err(1, "strftime failed");
  return strdup(buf);
}

int main(int argc, char *argv[]) {
  uint64_t epoch_secs = 1731627341;
  printf("%s\n", f_strftime(epoch_secs));
  printf("%s\n", f_strflocaltime(epoch_secs));
}
--- end ---

and run this script:
--- begin ---
#!/bin/sh
make jq
./jq
TZ=Etc/GMT+7 ./jq
--- end ---

On my system this reports:
--- begin ---
2024-11-14T23:35:41CET
2024-11-15T00:35:41+0100
2024-11-14T23:35:41-07
2024-11-14T16:35:41-0700
--- end ---

The second and fourth lines are the correct result.

The first and third lines are wrong. They report the (correct) time
in UTC, but the reported time zones are CET (my system default) or
-7 (from TZ) respectively.
>Fix:
Yes, please.

>Release-Note:

>Audit-Trail:

Responsible-Changed-From-To: gnats-admin->lib-bug-people
Responsible-Changed-By: wiz@NetBSD.org
Responsible-Changed-When: Thu, 16 Jul 2026 12:41:13 +0000
Responsible-Changed-Why:
lib bug report


State-Changed-From-To: open->analyzed
State-Changed-By: kre@NetBSD.org
State-Changed-When: Thu, 16 Jul 2026 17:26:23 +0000
State-Changed-Why:
What is happening is understood, looks like user error.


From: Thomas Klausner <wiz@netbsd.org>
To: Robert Elz <kre@munnari.oz.au>
Cc: netbsd-bugs@netbsd.org
Subject: Re: lib/60452: gmtime, gmtime_r put wrong time zone into result
Date: Thu, 16 Jul 2026 19:38:14 +0200

 On Fri, Jul 17, 2026 at 12:23:14AM +0100, Robert Elz wrote:
 > The problem you're seeing is from tzcode's attempt to obey the C
 > standard when it comes to %z and %Z conversions - according to which
 > the values that are in tm_zone and tm_gmtoff cannot be used to generate
 > those values - that's because those fields aren't required in C (they
 > are now in POSIX) and so a conforming application can't set them.
 > 
 > The strftime() implementation doesn't know (cannot possibly know) whether
 > the struct tm passed to it was generated by localtime/gmtime, or hand
 > crafted as in "tm.tm_secs = 33; tm.tm_hour = 2; ...." - in the former
 > it could reasonably expect tm_gmtoff and tm_zone to be set.  In the latter
 > it cannot, they may simply be uninit'd stack garbage.
 > 
 > All that strftime() has to rely upon in those cases are the "timezone"
 > and "altzone" variables, which gmtime() doesn't set, they always reflect
 > the offsets for the local timezone.

 Thank you, that was interesting.

 As a data point, I tried the same test problem (s/uint64_t/time_t) on
 macOS Tahoe, FreeBSD 15, and Debian forky, and on those systems, I get
 the result I expected with my test program.

 So I tend to agree with christos' suggestion of changing strftime()'s
 behaviour.
  Thomas

From: Christos Zoulas <christos@zoulas.com>
To: "wiz@netbsd.org" <wiz@NetBSD.org>
Cc: NetBSD Problem Report DB Administrator <gnats@netbsd.org>
Subject: Re: pending/60452: gmtime, gmtime_r put wrong time zone into result
 (pending)
Date: Thu, 16 Jul 2026 14:25:13 -0400

 --Apple-Mail=_6C252C7D-66FF-40B3-8267-B2E481E5D4AE
 Content-Transfer-Encoding: quoted-printable
 Content-Type: text/plain;
 	charset=utf-8



 On Jul 16, 2026, at 1:20=E2=80=AFPM, Christos Zoulas =
 <christos@zoulas.com> wrote:

 This is fixed by removing:

 diff -u -p -u -r1.60 strftime.c
 --- strftime.c  8 Mar 2026 21:04:54 -0000       1.60
 +++ strftime.c  16 Jul 2026 17:18:41 -0000
 @@ -53,17 +53,6 @@ __RCSID("$NetBSD: strftime.c,v 1.60 2026

 #include "private.h"

 -/*
 -** We don't use these extensions in strftime operation even when
 -** supported by the local tzcode configuration.  A strictly
 -** conforming C application may leave them in undefined state.
 -*/
 -
 -#ifdef _LIBC
 -#undef TM_ZONE
 -#undef TM_GMTOFF
 -#endif
 -

 Because without these extensions strftime does not have a way to
 determine the timezone in struct tm and defaults to what the current
 timezone.

 I think we should remove, because the current behavior is unexpected
 and this is what everyone else does (BSD, MacOS, Linux)

 christos


 --Apple-Mail=_6C252C7D-66FF-40B3-8267-B2E481E5D4AE
 Content-Transfer-Encoding: quoted-printable
 Content-Type: text/html;
 	charset=utf-8

 <html aria-label=3D"message body"><head><meta http-equiv=3D"content-type" =
 content=3D"text/html; charset=3Dutf-8"></head><body =
 style=3D"overflow-wrap: break-word; -webkit-nbsp-mode: space; =
 line-break: after-white-space;"><br =
 id=3D"lineBreakAtBeginningOfMessage"><div><br><div>On Jul 16, 2026, at =
 1:20=E2=80=AFPM, Christos Zoulas &lt;christos@zoulas.com&gt; =
 wrote:</div><blockquote type=3D"cite"></blockquote><div><font =
 color=3D"#5856d6"><span style=3D"caret-color: rgb(88, 86, =
 214);"><br></span></font></div><div>This is fixed by =
 removing:</div><div><font color=3D"#5856d6"><span style=3D"caret-color: =
 rgb(88, 86, 214);"><br></span></font></div><div>diff -u -p -u -r1.60 =
 strftime.c</div><div>--- strftime.c &nbsp;8 Mar 2026 21:04:54 -0000 =
 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;1.60</div><div>+++ strftime.c =
 &nbsp;16 Jul 2026 17:18:41 -0000</div><div>@@ -53,17 +53,6 @@ =
 __RCSID("$NetBSD: strftime.c,v 1.60 2026</div><div><font =
 color=3D"#5856d6"><span style=3D"caret-color: rgb(88, 86, =
 214);"><br></span></font></div><div>#include "private.h"</div><div><font =
 color=3D"#5856d6"><span style=3D"caret-color: rgb(88, 86, =
 214);"><br></span></font></div><div>-/*</div><div>-** We don't use these =
 extensions in strftime operation even when</div><div>-** supported by =
 the local tzcode configuration. &nbsp;A strictly</div><div>-** =
 conforming C application may leave them in undefined =
 state.</div><div>-*/</div><div>-</div><div>-#ifdef =
 _LIBC</div><div>-#undef TM_ZONE</div><div>-#undef =
 TM_GMTOFF</div><div>-#endif</div><div>-</div><div><font =
 color=3D"#5856d6"><span style=3D"caret-color: rgb(88, 86, =
 214);"><br></span></font></div><div>Because without these extensions =
 strftime does not have a way to</div><div>determine the timezone in =
 struct tm and defaults to what the =
 current</div><div>timezone.</div><div><font color=3D"#5856d6"><span =
 style=3D"caret-color: rgb(88, 86, 214);"><br></span></font></div><div>I =
 think we should remove, because the current behavior is =
 unexpected</div><div>and this is what everyone else does (BSD, MacOS, =
 Linux)</div><div><br></div><div><div>christos<br></div></div></div><br></b=
 ody></html>=

 --Apple-Mail=_6C252C7D-66FF-40B3-8267-B2E481E5D4AE--

From: Robert Elz <kre@munnari.OZ.AU>
To: gnats-bugs@netbsd.org
Cc: wiz@NetBSD.org, netbsd-bugs@netbsd.org
Subject: Re: lib/60452: gmtime, gmtime_r put wrong time zone into result
Date: Fri, 17 Jul 2026 01:40:46 +0700

     Date:        Thu, 16 Jul 2026 17:40:01 +0000 (UTC)
     From:        "Thomas Klausner via gnats" <gnats-admin@NetBSD.org>
     Message-ID:  <20260716174001.E69BF1A923C@mollari.NetBSD.org>

   |  As a data point, I tried the same test problem (s/uint64_t/time_t) on
   |  macOS Tahoe, FreeBSD 15, and Debian forky, and on those systems, I get
   |  the result I expected with my test program.

 There are two explanations, perhaps they make gmtime() set the magic
 global variables, which I think is permitted, any call to any of
 gmtime() localtime() ctime() or asctime() is allowed to alter the
 static state environment of any of the others, though I wouldn't be
 surprised if there was software out there which assume that doesn't
 happen.   Or they are using the tm_zone and tm_gmtoff fields of the
 struct tm in strftime().

 It is easy to test which that is, simply make the f_gmtime() function
 in your test be:

 void f_gmtime(time_t secs, struct tm *tmp) {
   memset(tmp, 0, sizeof(*tmp));
   if (gmtime_r(&secs, tmp) == NULL)
     err(1, "gmtime_r failed");
   tmp->tm_gmtoff = -1234;
   tmp->tm_zone = (char *)-1234;
 }

 and see what the effect is on those systems.

   |  So I tend to agree with christos' suggestion of changing strftime()'s
   |  behaviour.

 Sorry, I haven't seen that, so I'm not sure what kind of change he
 meant.   If there was an easy solution to this, which would work
 everywhere, then tzcode would already have it.

 The simple solution is not to expect %z or %Z to work after gmtime(),
 and to fix anything which does.   But there might be lots of that, perhaps.

 kre

 ps: and yes, I forgot in my earlier message, that I also changed the
 type of epoch_secs in main() in jq.c to time_t ... assuming that whatever
 it was, and time_t, are the same type is insane (but so is simply init'int
 a time_t to an constant which is supposed to mean something on a standard C
 system - it works on POSIX, now finally. though.)


From: Thomas Klausner <wiz@netbsd.org>
To: Robert Elz <kre@munnari.oz.au>
Cc: gnats-bugs@netbsd.org
Subject: Re: lib/60452: gmtime, gmtime_r put wrong time zone into result
Date: Thu, 16 Jul 2026 20:46:25 +0200

 On Fri, Jul 17, 2026 at 01:40:46AM +0100, Robert Elz wrote:
 > It is easy to test which that is, simply make the f_gmtime() function
 > in your test be:
 > 
 > void f_gmtime(time_t secs, struct tm *tmp) {
 >   memset(tmp, 0, sizeof(*tmp));
 >   if (gmtime_r(&secs, tmp) == NULL)
 >     err(1, "gmtime_r failed");
 >   tmp->tm_gmtoff = -1234;
 >   tmp->tm_zone = (char *)-1234;
 > }
 > 
 > and see what the effect is on those systems.

 macOS:

 cc     jq.c   -o jq
 ./test.sh: line 3: 53331 Segmentation fault: 11  TZ=Etc/GMT+7 ./jq
 ./test.sh: line 4: 53332 Segmentation fault: 11  ./jq

 Debian:

 cc     jq.c   -o jq
 Segmentation fault
 Segmentation fault

 FreeBSD:

 cc -O2 -pipe  jq.c  -o jq
 In-address space security exception (core dumped)
 In-address space security exception (core dumped)

  Thomas

From: Robert Elz <kre@munnari.OZ.AU>
To: Thomas Klausner <wiz@netbsd.org>
Cc: gnats-bugs@netbsd.org
Subject: Re: lib/60452: gmtime, gmtime_r put wrong time zone into result
Date: Fri, 17 Jul 2026 04:22:38 +0700

     Date:        Thu, 16 Jul 2026 20:46:25 +0200
     From:        Thomas Klausner <wiz@netbsd.org>
     Message-ID:  <alkmgRfoTh5e8iLo@exadelic.gatalith.at>

   | macOS:
   | ./test.sh: line 3: 53331 Segmentation fault: 11  TZ=Etc/GMT+7 ./jq
   | ./test.sh: line 4: 53332 Segmentation fault: 11  ./jq
   |
   | Debian:
   | Segmentation fault
   | Segmentation fault
   |
   | FreeBSD:
   | In-address space security exception (core dumped)
   | In-address space security exception (core dumped)

 Then they're all just using tm_zone, which means a properly conforming
 C application is likely to fail, as it cannot init that field, all it
 can do is leave random stack garbage in its place if it uses strftime
 to format a date/time which it has (for example) parsed into a struct
 tm from some other source (and it uses strftime %Z - which the C standard
 does define).

 The trash init of the tm_zone field I added to jq.c, and you tested, was
 just to simulate that random stack garbage, which gmtime() would never
 leave, but which alternate code to fill the struct tm would have no option
 but to do (using memset() as that code does would help, but is not a
 requirement).

 Perhaps that is acceptable, I'm not sure.   Using the tm_gmtoff and
 tm_zone fields certainly makes strftime much simpler to implement.

 kre


>Unformatted:

NetBSD Home
NetBSD PR Database Search

(Contact us) $NetBSD: query-full-pr,v 1.51 2026/08/10 02:28:17 riastradh Exp $
$NetBSD: gnats_config.sh,v 1.10 2026/05/13 22:00:09 riastradh Exp $
Copyright © 1994-2026 The NetBSD Foundation, Inc. ALL RIGHTS RESERVED.