wcsftime(3) computes %V (ISO 8601 week number) incorrectly

Serhiy Storchaka <[email protected]> Wed, 22 Jul 2026 12:41:31 +0300
Newsgroups gmane.os.openbsd.bugs
Message-ID <[email protected]>
 >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') {