>Synopsis: wcsftime(3) computes %V (ISO 8601 week number) incorrectly
>Category: library
>Environment:
System : OpenBSD 7.9
Details : OpenBSD 7.9 (GENERIC.MP) #449: Wed May 6 13:17:25
MDT 2026
[email protected]:/usr/src/sys/arch/amd64/compile/GENERIC.MP
Architecture: OpenBSD.amd64
Machine : amd64
>Description:
wcsftime(3) returns 53 for %V whenever the ISO 8601 week belongs to a
different year than tm_year. strftime(3) returns the correct value for
the same struct tm. %G and %u are correct in both.
Both functions contain the same XPG4-1994 override, applied after the
loop that has already computed the ISO week number correctly:
if ((w == 52 && t->tm_mon == TM_JANUARY) ||
(w == 1 && t->tm_mon == TM_DECEMBER))
w = 53;
In strftime.c it is wrapped in #ifdef XPG4_1994_04_09, so it is dead
code. In wcsftime.c the guard is missing, so it always runs and
clobbers exactly the year-boundary cases. XPG4_1994_04_09 is not
defined anywhere in the tree; it occurs only in strftime.c, in the
#ifdef and #endif lines themselves.
The guard was already absent when wcsftime.c was added in 2011 -- the
file was adapted from strftime.c and the #ifdef/#endif lines were
dropped while the code between them was kept -- so this has been wrong
since the file was created.
This was reported once before, in 2018, from a downstream Python bug:
https://marc.info/?l=openbsd-bugs&m=153728102618747
>How-To-Repeat:
Compile and run the program below. On OpenBSD 7.9:
1905-01-01 strftime 1904-52 wcsftime 1904-53 expected 1904-52
<-- WRONG
1906-12-31 strftime 1907-01 wcsftime 1907-53 expected 1907-01
<-- WRONG
2008-12-29 strftime 2009-01 wcsftime 2009-53 expected 2009-01
<-- WRONG
2009-01-01 strftime 2009-01 wcsftime 2009-01 expected 2009-01
2010-01-03 strftime 2009-53 wcsftime 2009-53 expected 2009-53
3 of 5 wrong
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <wchar.h>
/* %V is the ISO 8601 week number. strftime() and wcsftime() must
agree. */
static int
check(int year, int mon, int mday, const wchar_t *expected)
{
struct tm tm;
char sbuf[16];
wchar_t wbuf[16];
int bad;
memset(&tm, 0, sizeof tm);
tm.tm_year = year - 1900;
tm.tm_mon = mon - 1;
tm.tm_mday = mday;
tm.tm_isdst = -1;
mktime(&tm);
strftime(sbuf, sizeof sbuf, "%G-%V", &tm);
wcsftime(wbuf, sizeof wbuf / sizeof *wbuf, L"%G-%V", &tm);
bad = wcscmp(wbuf, expected) != 0;
printf("%04d-%02d-%02d strftime %s wcsftime %ls expected
%ls%s\n",
year, mon, mday, sbuf, wbuf, expected, bad ? " <-- WRONG"
: "");
return bad;
}
int
main(void)
{
int bad = 0;
bad += check(1905, 1, 1, L"1904-52");
bad += check(1906, 12, 31, L"1907-01");
bad += check(2008, 12, 29, L"2009-01");
bad += check(2009, 1, 1, L"2009-01");
bad += check(2010, 1, 3, L"2009-53");
printf("%d of 5 wrong\n", bad);
return bad != 0;
}
>Fix:
Restore the guard, so that wcsftime() matches strftime(). I compiled
lib/libc/time/wcsftime.c with this diff applied and ran the test above
against it: 0 of 5 wrong, and it agrees with strftime(3) on every
input.
Index: lib/libc/time/wcsftime.c
===================================================================
RCS file: /cvs/src/lib/libc/time/wcsftime.c,v
retrieving revision 1.7
diff -u -p -r1.7 wcsftime.c
--- lib/libc/time/wcsftime.c 12 May 2019 12:49:52 -0000 1.7
+++ lib/libc/time/wcsftime.c 22 Jul 2026 09:30:00 -0000
@@ -386,9 +386,11 @@ label:
DAYSPERLYEAR :
DAYSPERNYEAR;
}
+#ifdef XPG4_1994_04_09
if ((w == 52 && t->tm_mon == TM_JANUARY) ||
(w == 1 && t->tm_mon == TM_DECEMBER))
w = 53;
+#endif /* defined XPG4_1994_04_09 */
if (*format == 'V')
pt = _conv(w, L"%02d", pt, ptlim);
else if (*format == 'g') {