Commit c0fd83dc7c for openssl.org

commit c0fd83dc7c25f046c266a44ccb178a8b7e8a4572
Author: Bob Beck <beck@openssl.org>
Date:   Mon Jun 22 11:06:55 2026 -0600

    Add a build target for MSVC 2013

    Apparently it is desired to keep this on life support.

    So we add a build target for this non-C99 platform, and
    provide internal versions of snprintf and vsnprintf for use
    only on this platform that match the C99 semantics.

    This allows us to use C99 semantics normally elsewhere.

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    MergeDate: Wed Aug 26 16:20:04 2026
    (Merged from https://github.com/openssl/openssl/pull/31640)

diff --git a/CHANGES.md b/CHANGES.md
index bb66efbfa4..f4145d7cf6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -171,6 +171,12 @@ OpenSSL Releases

    *Adriano Sela Aviles*

+ * Add a build target to provide MSVC 2013 hacks in a C99 world.
+   The build target adds internal functions to provide C99 functions
+   that are not present with MSVC 2013.
+
+   *Bob Beck*
+
  * SubjectPublicKeyInfo blobs whose AlgorithmIdentifier uses id-RSAES-OAEP
    (`NID_rsaesOaep`, 1.2.840.113549.1.1.7) with a plain RSAPublicKey body
    are now decoded as RSA keys.  This is required for interoperability
diff --git a/Configurations/50-win-msvc2013.conf b/Configurations/50-win-msvc2013.conf
new file mode 100644
index 0000000000..755144376e
--- /dev/null
+++ b/Configurations/50-win-msvc2013.conf
@@ -0,0 +1,19 @@
+## -*- mode: perl; -*-
+# Windows MSVC 2013 compatibility targets.
+#
+# Visual Studio versions earlier than VS 2015 (_MSC_VER < 1900) do not
+# provide the C99 snprintf or vsnprintf functions, and their
+# _snprintf / _vsnprintf counterparts have non-C99 semantics.  These
+# target variants build in a small compatibility source file that
+# supplies C99 snprintf and vsnprintf.
+
+my %targets = (
+    "VC-WIN32-MSVC2013" => {
+        inherit_from              => [ "VC-WIN32" ],
+        needs_c99_snprintf_compat => 1,
+    },
+    "VC-WIN64A-MSVC2013" => {
+        inherit_from              => [ "VC-WIN64A" ],
+        needs_c99_snprintf_compat => 1,
+    },
+);
diff --git a/apps/include/platform.h b/apps/include/platform.h
index ec12a41af7..74a9385629 100644
--- a/apps/include/platform.h
+++ b/apps/include/platform.h
@@ -29,4 +29,18 @@ char **copy_argv(int *argc, char *argv[]);
 void win32_utf8argv(int *argc, char **argv[]);
 #endif

+/*
+ * MSVC versions earlier than Visual Studio 2015 (_MSC_VER < 1900) do not
+ * declare or define C99 snprintf or vsnprintf.  Definitions are supplied
+ * by crypto/msvc2013_snprintf.c, which is built only on the matching
+ * Configure target variants.
+ */
+#if defined(_MSC_VER) && _MSC_VER < 1900
+#include <stdarg.h>
+int msvc_translate_printf_format(const char *format, const char **out,
+    char **tmp);
+int snprintf(char *buf, size_t n, const char *fmt, ...);
+int vsnprintf(char *buf, size_t n, const char *fmt, va_list args);
+#endif
+
 #endif
diff --git a/apps/lib/build.info b/apps/lib/build.info
index a781117264..9512a43b76 100644
--- a/apps/lib/build.info
+++ b/apps/lib/build.info
@@ -21,3 +21,7 @@ ENDIF
 IF[{- !$disabled{srp} -}]
   SOURCE[../libapps.a]=tlssrp_depr.c
 ENDIF
+
+IF[{- $target{needs_c99_snprintf_compat} -}]
+  SOURCE[../libapps.a]=../../crypto/msvc2013_snprintf.c
+ENDIF
diff --git a/crypto/bio/bio_print.c b/crypto/bio/bio_print.c
index 382a64047a..3f25356142 100644
--- a/crypto/bio/bio_print.c
+++ b/crypto/bio/bio_print.c
@@ -28,190 +28,35 @@ int BIO_printf(BIO *bio, const char *format, ...)
     return ret;
 }

-#if defined(_MSC_VER) && _MSC_VER < 1900
-/*
- * _MSC_VER described here:
- * https://learn.microsoft.com/en-us/cpp/overview/compiler-versions?view=msvc-170
- *
- * Beginning with the UCRT in Visual Studio 2015 and Windows 10, snprintf is no
- * longer identical to _snprintf. The snprintf behavior is now C99 standard
- * conformant. The difference is that if you run out of buffer, snprintf
- * null-terminates the end of the buffer and returns the number of characters
- * that would have been required whereas _snprintf doesn't null-terminate the
- * buffer and returns -1. Also, snprintf() includes one more character in the
- * output because it doesn't null-terminate the buffer.
- * [ https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/snprintf-snprintf-snprintf-l-snwprintf-snwprintf-l?view=msvc-170#remarks
- *
- * for older MSVC (older than 2015) we can use _vscprintf() and _vsnprintf()
- * as suggested here:
- * 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)
+int BIO_vprintf(BIO *bio, const char *format, va_list args)
 {
+    va_list cp_args;
+    int sz;
+    int ret = -1;
     char buf[512];
-    char *abuf, *fmt_alloc;
-    const char *fmt;
-    int ret, sz;
-
-    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(fmt, args) + 1;
-        abuf = (char *)OPENSSL_malloc(sz);
-        if (abuf == NULL) {
-            ret = -1;
-        } else {
-            sz = _vsnprintf(abuf, sz, fmt, args);
-            ret = BIO_write(bio, abuf, sz);
-            OPENSSL_free(abuf);
-        }
-    } else {
-        ret = BIO_write(bio, buf, sz);
-    }
-
-    OPENSSL_free(fmt_alloc);
-    return ret;
-}
+    char *abuf;
+#if defined(_MSC_VER) && _MSC_VER < 1900
+    char *msvc_fmt_alloc = NULL;
 #endif
+    const char *fmt;

-#ifdef _MSC_VER
-/*
- * This function is for unit test on windows only when built with Visual Studio
- */
-int ossl_BIO_snprintf_msvc(char *buf, size_t n, const char *format, ...)
-{
-    va_list args;
-    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);
-        }
-    }
+    /* Fix MSVC 2013 format to accept C99 format strings */
+    if (!msvc_translate_printf_format(format, &fmt, &msvc_fmt_alloc))
+        goto done;
 #else
-    ret = _vsnprintf_s(buf, n, _TRUNCATE, format, args);
+    fmt = format;
 #endif
-    va_end(args);
-
-    return ret;
-}
-#endif
-
-int BIO_vprintf(BIO *bio, const char *format, va_list args)
-{
-    va_list cp_args;
-#if !defined(_MSC_VER) || _MSC_VER >= 1900
-    int sz;
-#endif
-    int ret = -1;

     va_copy(cp_args, args);
-#if defined(_MSC_VER) && _MSC_VER < 1900
-    ret = msvc_bio_vprintf(bio, format, cp_args);
-#else
-    char buf[512];
-    char *abuf;
+
     /*
      * some compilers modify va_list, hence each call to v*printf()
      * should operate with its own instance of va_list. The first
      * call to vsnprintf() here uses args we got in function argument.
      * The second call is going to use cp_args we made earlier.
      */
-    sz = vsnprintf(buf, sizeof(buf), format, args);
+    sz = vsnprintf(buf, sizeof(buf), fmt, args);
     if (sz >= 0) {
         if ((size_t)sz >= sizeof(buf)) {
             sz += 1;
@@ -219,7 +64,7 @@ int BIO_vprintf(BIO *bio, const char *format, va_list args)
             if (abuf == NULL) {
                 ret = -1;
             } else {
-                sz = vsnprintf(abuf, sz, format, cp_args);
+                sz = vsnprintf(abuf, sz, fmt, cp_args);
                 ret = BIO_write(bio, abuf, sz);
                 OPENSSL_free(abuf);
             }
@@ -228,8 +73,11 @@ int BIO_vprintf(BIO *bio, const char *format, va_list args)
             ret = BIO_write(bio, buf, sz);
         }
     }
-#endif
     va_end(cp_args);
+#if defined(_MSC_VER) && _MSC_VER < 1900
+done:
+    OPENSSL_free(msvc_fmt_alloc);
+#endif
     return ret;
 }

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

     va_start(args, format);
-    ret = BIO_vsnprintf(buf, n, format, args);
+
+    ret = vsnprintf(buf, n, format, args);
+    if ((size_t)ret >= n)
+        ret = -1;
     va_end(args);

     return ret;
@@ -254,21 +105,11 @@ 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
-    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)
         ret = -1;
-#endif
+
     return ret;
 }
diff --git a/crypto/build.info b/crypto/build.info
index 593c6521cd..471eaa070a 100644
--- a/crypto/build.info
+++ b/crypto/build.info
@@ -140,3 +140,7 @@ GENERATE[loongarch64cpuid.s]=loongarch64cpuid.pl
 IF[{- $config{target} =~ /^(?:Cygwin|mingw|VC-|BC-)/ -}]
   SHARED_SOURCE[../libcrypto]=dllmain.c
 ENDIF
+
+IF[{- $target{needs_c99_snprintf_compat} -}]
+  SOURCE[../libcrypto]=msvc2013_snprintf.c
+ENDIF
diff --git a/crypto/msvc2013_snprintf.c b/crypto/msvc2013_snprintf.c
new file mode 100644
index 0000000000..973e86b11b
--- /dev/null
+++ b/crypto/msvc2013_snprintf.c
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+/*
+ * C99 snprintf and vsnprintf emulation for MSVC versions earlier than
+ * Visual Studio 2015 (_MSC_VER < 1900).  Those compilers ship _snprintf
+ * and _vsnprintf with non-C99 semantics (return -1 on truncation, no
+ * guaranteed NUL termination) and do not provide the standard names at
+ * all.  This file supplies the missing C99 names.
+ *
+ * This file is only compiled when the configured target is one of the
+ * Windows MSVC 2013 compatibility variants; on every other platform
+ * the standard library already provides these symbols.
+ *
+ * IMPORTANT: This translation unit MUST define snprintf and vsnprintf
+ * and nothing else.  This single file is compiled directly into libcrypto,
+ * libssl, and the apps; each references it from its own build.info rather
+ * than keeping a copy.  Because each definition lives in its own .obj, the
+ * linker's archive-search rule ensures only one copy is pulled into any
+ * final binary.  Defining any additional symbol here would risk pulling
+ * multiple copies and producing a duplicate-symbol link error.
+ */
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <string.h>
+#include <openssl/bio.h>
+#include <openssl/crypto.h>
+
+/*
+ * _MSC_VER described here:
+ * https://learn.microsoft.com/en-us/cpp/overview/compiler-versions?view=msvc-170
+ *
+ * Beginning with the UCRT in Visual Studio 2015 and Windows 10, snprintf is no
+ * longer identical to _snprintf. The snprintf behavior is now C99 standard
+ * conformant. The difference is that if you run out of buffer, snprintf
+ * null-terminates the end of the buffer and returns the number of characters
+ * that would have been required whereas _snprintf doesn't null-terminate the
+ * buffer and returns -1. Also, snprintf() includes one more character in the
+ * output because it doesn't null-terminate the buffer.
+ * [ https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/snprintf-snprintf-snprintf-l-snwprintf-snwprintf-l?view=msvc-170#remarks
+ *
+ * for older MSVC (older than 2015) we can use _vscprintf() and _vsnprintf()
+ * as suggested here:
+ * https://stackoverflow.com/questions/2915672/snprintf-and-visual-studio-2010
+ *
+ */
+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;
+}
+
+int vsnprintf(char *buf, size_t n, const char *format, va_list args)
+{
+    int count = -1;
+    va_list args_copy;
+    char *fmt_alloc = NULL;
+    const char *fmt;
+
+    if (!msvc_translate_printf_format(format, &fmt, &fmt_alloc))
+        goto done;
+    va_copy(args_copy, args);
+    count = _vscprintf(fmt, args_copy);
+    va_end(args_copy);
+
+    if (count < 0)
+        goto done;
+
+    if (n > 0)
+        (void)_vsnprintf_s(buf, n, _TRUNCATE, fmt, args);
+
+done:
+    OPENSSL_free(fmt_alloc);
+    return count;
+}
+
+int snprintf(char *buf, size_t n, const char *fmt, ...)
+{
+    va_list args;
+    int ret;
+
+    va_start(args, fmt);
+    ret = vsnprintf(buf, n, fmt, args);
+    va_end(args);
+    return ret;
+}
diff --git a/include/internal/bio.h b/include/internal/bio.h
index 91a27b2218..bec4242b26 100644
--- a/include/internal/bio.h
+++ b/include/internal/bio.h
@@ -107,8 +107,4 @@ int ossl_core_bio_vprintf(OSSL_CORE_BIO *cb, const char *format, va_list args);

 int ossl_bio_init_core(OSSL_LIB_CTX *libctx, const OSSL_DISPATCH *fns);

-#ifdef _MSC_VER
-int ossl_BIO_snprintf_msvc(char *buf, size_t n, const char *fmt, ...);
-#endif
-
 #endif
diff --git a/include/internal/e_os.h b/include/internal/e_os.h
index 444f888674..4fb1840f61 100644
--- a/include/internal/e_os.h
+++ b/include/internal/e_os.h
@@ -152,6 +152,20 @@ FILE *__iob_func(void);
 #define check_win_minplat(x) (LOBYTE(LOWORD(GetVersion())) >= (x))
 #endif

+/*
+ * MSVC versions earlier than Visual Studio 2015 (_MSC_VER < 1900) do not
+ * declare or define C99 snprintf or vsnprintf.  Definitions are supplied
+ * by crypto/msvc2013_snprintf.c, which is built only on the matching
+ * Configure target variants.
+ */
+#if defined(_MSC_VER) && _MSC_VER < 1900
+#include <stdarg.h>
+int msvc_translate_printf_format(const char *format, const char **out,
+    char **tmp);
+int snprintf(char *buf, size_t n, const char *fmt, ...);
+int vsnprintf(char *buf, size_t n, const char *fmt, va_list args);
+#endif
+
 #else /* The non-microsoft world */

 #if defined(OPENSSL_SYS_VXWORKS)
diff --git a/ssl/build.info b/ssl/build.info
index 35a614e389..e45db959cc 100644
--- a/ssl/build.info
+++ b/ssl/build.info
@@ -58,3 +58,7 @@ IF[{- !$disabled{dtls} || !$disabled{quic} -}]
     SHARED_SOURCE[../libssl]=../crypto/siphash/siphash.c
   ENDIF
 ENDIF
+
+IF[{- $target{needs_c99_snprintf_compat} -}]
+  SOURCE[../libssl]=../crypto/msvc2013_snprintf.c
+ENDIF