Commit 2da90c2f05 for openssl.org

commit 2da90c2f053dd97821a518b62a3553888af861e2
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Thu Jul 23 22:35:19 2026 +0900

    Fix C99 printf length modifiers with VS2013 CRT

    The Visual Studio 2013 CRT does not support the C99 z, t and j length modifiers. Passing such formats to _vsnprintf_s invokes the invalid-parameter path and normally terminates the process.

    For _MSC_VER < 1900, translate z and t to the pointer-sized I modifier and j to I64 before calling the CRT. Allocate a translated format only when needed, leaving newer MSVC versions and unaffected formats unchanged. Delegate BIO_snprintf to BIO_vsnprintf so all BIO formatting entry points use the same compatibility path.

    Add focused coverage for BIO_snprintf, BIO_vsnprintf and BIO_printf, including the large-output allocation path and truncation behavior.

    Fixes #31645

    Assisted-by: OpenCode:GLM-5.2

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Milan Broz <mbroz@openssl.org>
    MergeDate: Fri Jul 31 13:13:06 2026
    (Merged from https://github.com/openssl/openssl/pull/31988)

diff --git a/crypto/bio/bio_print.c b/crypto/bio/bio_print.c
index 5366587a2a..382a64047a 100644
--- a/crypto/bio/bio_print.c
+++ b/crypto/bio/bio_print.c
@@ -47,20 +47,107 @@ int BIO_printf(BIO *bio, const char *format, ...)
  * https://stackoverflow.com/questions/2915672/snprintf-and-visual-studio-2010
  *
  */
+static int msvc_translate_printf_format(const char *format, const char **out,
+    char **tmp)
+{
+    /* Valid printf conversion specifiers, grouped by category: signed
+     * integers (d i), unsigned (o u x X), floating-point (f F e E g G a A),
+     * misc (c s p n) and MSVC-specific (S Z C). */
+    static const char conv[] = "diouxXfFeEgGaAcspnSZC";
+    const char *p = format;
+    char *dst = NULL, *q = NULL;
+
+    /*
+     * The VS 2013 CRT does not understand the C99 z, t and j length
+     * modifiers. Translate z and t to I (both are pointer-sized on Windows)
+     * and j to I64 (intmax_t is 64 bits). Every input character expands to
+     * at most three output characters (j -> I64), so 3 * length is a safe
+     * bound for the buffer.
+     *
+     * This is done in a single pass: nothing is allocated until the first
+     * modifier is seen, so formats that need no translation return the
+     * original string untouched. EMIT_CHAR() appends a character to the
+     * output once the buffer exists; before that it is a no-op.
+     */
+#define EMIT_CHAR(c)     \
+    do {                 \
+        if (dst != NULL) \
+            *q++ = (c);  \
+    } while (0)
+
+    *out = format;
+    *tmp = NULL;
+
+    while (*p != '\0') {
+        if (*p != '%') { /* literal character */
+            EMIT_CHAR(*p);
+            p++;
+            continue;
+        }
+        p++; /* consume '%' */
+        if (*p == '%') { /* literal "%%" */
+            EMIT_CHAR('%');
+            EMIT_CHAR('%');
+            p++;
+            continue;
+        }
+        EMIT_CHAR('%');
+        while (*p != '\0' && strchr(conv, *p) == NULL) {
+            char c = *p++;
+            if (c != 'z' && c != 't' && c != 'j') { /* verbatim */
+                EMIT_CHAR(c);
+                continue;
+            }
+            if (dst == NULL) { /* first modifier: allocate + flush prefix */
+                size_t len = strlen(format);
+                if (len > (SIZE_MAX - 1) / 3) /* make static analysis happy */
+                    return 0;
+                dst = (char *)OPENSSL_malloc(3 * len + 1);
+                if (dst == NULL)
+                    return 0;
+                q = dst;
+                memcpy(q, format, (size_t)(p - 1 - format));
+                q += p - 1 - format;
+            }
+            EMIT_CHAR('I');
+            if (c == 'j') {
+                EMIT_CHAR('6');
+                EMIT_CHAR('4');
+            }
+        }
+        if (*p != '\0') { /* copy the conversion specifier */
+            EMIT_CHAR(*p);
+            p++;
+        }
+    }
+#undef EMIT_CHAR
+
+    if (dst != NULL) {
+        *q = '\0';
+        *out = dst;
+        *tmp = dst;
+    }
+    return 1;
+}
+
 static int msvc_bio_vprintf(BIO *bio, const char *format, va_list args)
 {
     char buf[512];
-    char *abuf;
+    char *abuf, *fmt_alloc;
+    const char *fmt;
     int ret, sz;

-    sz = _vsnprintf_s(buf, sizeof(buf), _TRUNCATE, format, args);
+    if (!msvc_translate_printf_format(format, &fmt, &fmt_alloc))
+        return -1;
+
+    sz = _vsnprintf_s(buf, sizeof(buf), _TRUNCATE, fmt, args);
     if (sz == -1) {
-        sz = _vscprintf(format, args) + 1;
+        sz = _vscprintf(fmt, args) + 1;
         abuf = (char *)OPENSSL_malloc(sz);
         if (abuf == NULL) {
             ret = -1;
         } else {
-            sz = _vsnprintf(abuf, sz, format, args);
+            sz = _vsnprintf(abuf, sz, fmt, args);
             ret = BIO_write(bio, abuf, sz);
             OPENSSL_free(abuf);
         }
@@ -68,6 +155,7 @@ static int msvc_bio_vprintf(BIO *bio, const char *format, va_list args)
         ret = BIO_write(bio, buf, sz);
     }

+    OPENSSL_free(fmt_alloc);
     return ret;
 }
 #endif
@@ -82,7 +170,21 @@ int ossl_BIO_snprintf_msvc(char *buf, size_t n, const char *format, ...)
     int ret;

     va_start(args, format);
+#if defined(_MSC_VER) && _MSC_VER < 1900
+    {
+        char *fmt_alloc;
+        const char *fmt;
+
+        if (!msvc_translate_printf_format(format, &fmt, &fmt_alloc)) {
+            ret = -1;
+        } else {
+            ret = _vsnprintf_s(buf, n, _TRUNCATE, fmt, args);
+            OPENSSL_free(fmt_alloc);
+        }
+    }
+#else
     ret = _vsnprintf_s(buf, n, _TRUNCATE, format, args);
+#endif
     va_end(args);

     return ret;
@@ -144,14 +246,7 @@ int BIO_snprintf(char *buf, size_t n, const char *format, ...)
     int ret;

     va_start(args, format);
-
-#if defined(_MSC_VER) && _MSC_VER < 1900
-    ret = _vsnprintf_s(buf, n, _TRUNCATE, format, args);
-#else
-    ret = vsnprintf(buf, n, format, args);
-    if ((size_t)ret >= n)
-        ret = -1;
-#endif
+    ret = BIO_vsnprintf(buf, n, format, args);
     va_end(args);

     return ret;
@@ -159,10 +254,17 @@ int BIO_snprintf(char *buf, size_t n, const char *format, ...)

 int BIO_vsnprintf(char *buf, size_t n, const char *format, va_list args)
 {
+#if defined(_MSC_VER) && _MSC_VER < 1900
+    char *fmt_alloc;
+    const char *fmt;
+#endif
     int ret;

 #if defined(_MSC_VER) && _MSC_VER < 1900
-    ret = _vsnprintf_s(buf, n, _TRUNCATE, format, args);
+    if (!msvc_translate_printf_format(format, &fmt, &fmt_alloc))
+        return -1;
+    ret = _vsnprintf_s(buf, n, _TRUNCATE, fmt, args);
+    OPENSSL_free(fmt_alloc);
 #else
     ret = vsnprintf(buf, n, format, args);
     if ((size_t)ret >= n)
diff --git a/test/bio_core_test.c b/test/bio_core_test.c
index 26ff787eec..0214040817 100644
--- a/test/bio_core_test.c
+++ b/test/bio_core_test.c
@@ -7,6 +7,8 @@
  * https://www.openssl.org/source/license.html
  */

+#include <stdarg.h>
+#include <stddef.h>
 #include <string.h>
 #include <openssl/bio.h>
 #include "testutil.h"
@@ -64,6 +66,18 @@ static const OSSL_DISPATCH biocbs[] = {
     OSSL_DISPATCH_END
 };

+static int call_bio_vsnprintf(char *buf, size_t n, const char *format, ...)
+{
+    va_list args;
+    int ret;
+
+    va_start(args, format);
+    ret = BIO_vsnprintf(buf, n, format, args);
+    va_end(args);
+
+    return ret;
+}
+
 static int test_bio_core(void)
 {
     BIO *cbio = NULL, *cbiobad = NULL;
@@ -140,6 +154,70 @@ err:
     return testresult;
 }

+static int test_bio_printf_c99_length_modifiers(void)
+{
+    static const char expected[] = "zu=12345 zd=-42 zx=3039 td=-7 ju=4294967338 jx=10000002a";
+    static const char long_tail[] = "12345";
+    BIO *bio = NULL;
+    char buf[128];
+    char *memdata = NULL;
+    long memlen;
+    size_t z = (size_t)12345;
+    ossl_ssize_t zs = (ossl_ssize_t)-42;
+    ptrdiff_t t = (ptrdiff_t)-7;
+    ossl_uintmax_t j = (((ossl_uintmax_t)1) << 32) + 42;
+    int expected_len = (int)strlen(expected);
+    size_t long_tail_len = strlen(long_tail);
+    int testresult = 0;
+
+    if (!TEST_int_eq(BIO_snprintf(buf, sizeof(buf),
+                         "zu=%zu zd=%zd zx=%zx td=%td ju=%ju jx=%jx",
+                         z, zs, z, t, j, j),
+            expected_len)
+        || !TEST_str_eq(buf, expected))
+        goto err;
+
+    if (!TEST_int_eq(call_bio_vsnprintf(buf, sizeof(buf),
+                         "zu=%zu zd=%zd zx=%zx td=%td ju=%ju jx=%jx",
+                         z, zs, z, t, j, j),
+            expected_len)
+        || !TEST_str_eq(buf, expected))
+        goto err;
+
+    if (!TEST_ptr(bio = BIO_new(BIO_s_mem()))
+        || !TEST_int_eq(BIO_printf(bio,
+                            "zu=%zu zd=%zd zx=%zx td=%td ju=%ju jx=%jx",
+                            z, zs, z, t, j, j),
+            expected_len))
+        goto err;
+
+    memlen = BIO_get_mem_data(bio, &memdata);
+    if (!TEST_long_eq(memlen, (long)strlen(expected))
+        || !TEST_mem_eq(memdata, (size_t)memlen, expected, strlen(expected)))
+        goto err;
+
+    BIO_free(bio);
+    bio = NULL;
+    memdata = NULL;
+    if (!TEST_ptr(bio = BIO_new(BIO_s_mem()))
+        || !TEST_int_eq(BIO_printf(bio, "%600zu", z), 600))
+        goto err;
+
+    memlen = BIO_get_mem_data(bio, &memdata);
+    if (!TEST_long_eq(memlen, 600)
+        || !TEST_mem_eq(memdata + (size_t)memlen - long_tail_len,
+            long_tail_len, long_tail, long_tail_len))
+        goto err;
+
+    if (!TEST_int_eq(BIO_snprintf(buf, 4, "%zu", z), -1))
+        goto err;
+
+    testresult = 1;
+err:
+    BIO_free(bio);
+    return testresult;
+}
+
 int setup_tests(void)
 {
     if (!test_skip_common_options()) {
@@ -149,5 +227,6 @@ int setup_tests(void)

     ADD_TEST(test_bio_core);
     ADD_TEST(test_bio_vprintf_boundary);
+    ADD_TEST(test_bio_printf_c99_length_modifiers);
     return 1;
 }