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') {

Reply via email to