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 <christos@zoulas.com> =
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 8 Mar 2026 21:04:54 -0000 =
1.60</div><div>+++ strftime.c =
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. 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:
(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.