Commit 0dee42e672 for openssl.org

commit 0dee42e672dbcc5a6bd277786e04bbd45f2de386
Author: Bob Beck <beck@openssl.org>
Date:   Tue Sep 15 06:53:52 2026 -0600

    Poison the ASN1_STRING NUL terminator under Valgrind

    Where <valgrind/memcheck.h> is available, mark the terminator
    inaccessible to memcheck too, issuing the requests only when actually
    running under Valgrind. After a realloc the new buffer is re-marked
    addressable, since memcheck's realloc carries over the old poison.
    The daily CT validation job is tweaked to also run the test, as it
    is currently the only thing we set up valgrind in.

    Stacked on https://github.com/openssl/openssl/pull/32829

    Reviewed-by: Milan Broz <mbroz@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Mon Sep 21 08:49:24 2026
    Merged-from: https://github.com/openssl/openssl/pull/32837

diff --git a/.github/workflows/ct-validation-daily.yml b/.github/workflows/ct-validation-daily.yml
index 6151f8ef5d..4d88dfc9cf 100644
--- a/.github/workflows/ct-validation-daily.yml
+++ b/.github/workflows/ct-validation-daily.yml
@@ -146,7 +146,9 @@ jobs:
         # - test_ct_validation_helpers: The Valgrind-based constant-time
         #   validation helpers from constant_time.h, like CONSTTIME_SECRET and
         #   CONSTTIME_DECLASSIFY
+        # - test_asn1_string_poison: memcheck reports C-string use of
+        #   ASN1_STRING data, whose NUL terminator libcrypto marks inaccessible
         run: |
-          make TESTS="test_internal_ml_kem test_internal_ml_dsa test_crypto_memcmp test_ct_validation_helpers" \
+          make TESTS="test_internal_ml_kem test_internal_ml_dsa test_crypto_memcmp test_ct_validation_helpers test_asn1_string_poison" \
                OSSL_VALGRIND_CT=yes \
                test
diff --git a/CHANGES.md b/CHANGES.md
index f7327a6354..0a287e8ba1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -543,14 +543,16 @@ OpenSSL 4.1
    `size_t` length values in the future.

    The data of an `ASN1_STRING` has never been guaranteed to be
-   NUL-terminated, although some operations terminated it anyway.  A future
-   release will stop doing so; the new setters above already do not add a
-   terminator.  Strings built by libcrypto itself, including decoded ones,
-   still carry one, but when OpenSSL is built with AddressSanitizer or
-   MemorySanitizer that byte is marked inaccessible, so treating the result
-   of `ASN1_STRING_get0_data()` as a C string (`strlen()`, `%s`, `strdup()`
-   and the like) is reported as an error.  All such uses must be changed to
-   honour `ASN1_STRING_get_length()`.
+   NUL-terminated, although some operations terminated it anyway.  A
+   future release will stop doing so; the new setters above already do
+   not add a terminator. When OpenSSL is built with AddressSanitizer
+   or MemorySanitizer, or is run under Valgrind having been built
+   where the Valgrind headers are installed, the added nul byte is
+   marked inaccessible, so treating the result of
+   `ASN1_STRING_get0_data()` as a C string (`strlen()`, `%s`,
+   `strdup()` and the like) is reported as an error.  All such uses
+   must be changed to honour `ASN1_STRING_get_length()`. The Valgrind
+   check may be disabled by building with OPENSSL_NO_VALGRIND_CHECK.
    <!-- https://github.com/openssl/openssl/pull/31194 -->

    *Bob Beck*
diff --git a/crypto/asn1/asn1_lib.c b/crypto/asn1/asn1_lib.c
index b815851e96..825886ecbf 100644
--- a/crypto/asn1/asn1_lib.c
+++ b/crypto/asn1/asn1_lib.c
@@ -30,6 +30,14 @@
 #if defined(ASN1_HAVE_MSAN)
 #include <sanitizer/msan_interface.h>
 #endif
+#if !defined OPENSSL_NO_VALGRIND_CHECK && defined __has_include
+/* Any compiler you're going to run valgrind on has this */
+#if __has_include(<valgrind/memcheck.h>)
+#include <valgrind/memcheck.h>
+#include "internal/thread_once.h"
+#define ASN1_HAVE_VALGRIND 1
+#endif
+#endif /* defined(__has_include) */

 static int asn1_get_length(const unsigned char **pp, int *inf, long *rl,
     long max);
@@ -308,11 +316,31 @@ ASN1_STRING *ASN1_STRING_dup(const ASN1_STRING *str)
     return ret;
 }

+#if defined(ASN1_HAVE_VALGRIND)
+static CRYPTO_ONCE valgrind_once = CRYPTO_ONCE_STATIC_INIT;
+static int valgrind_present = 0;
+
+DEFINE_RUN_ONCE_STATIC(detect_valgrind)
+{
+    valgrind_present = RUNNING_ON_VALGRIND != 0;
+    return 1;
+}
+
+static int under_valgrind(void)
+{
+    return RUN_ONCE(&valgrind_once, detect_valgrind) && valgrind_present;
+}
+#endif /* defined(ASN1_HAVE_VALGRIND) */
+
 /**
  * @brief Mark the NUL terminator at p as inaccessible to memory checkers.
- * Under AddressSanitizer and MemorySanitizer a read of the byte is reported
- * as an error, so C-string use of ASN1_STRING data is caught while the byte
- * stays present for builds without a sanitizer.
+ * Under AddressSanitizer, MemorySanitizer and Valgrind memcheck a read of
+ * the byte is reported as an error, so C-string use of ASN1_STRING data is
+ * caught while the byte stays present for builds without a checker. The
+ * Valgrind client requests are compiled in wherever its header is found and
+ * are issued only when the process is running under Valgrind.
+ * A poisoned byte needs no unpoisoning before free(): every checker marks
+ * the whole block on free without regard to its previous state.
  * @param p the terminator byte
  */
 static void poison_terminator(uint8_t *p)
@@ -323,6 +351,10 @@ static void poison_terminator(uint8_t *p)
 #if defined(ASN1_HAVE_MSAN)
     __msan_poison(p, 1);
 #endif
+#if defined(ASN1_HAVE_VALGRIND)
+    if (under_valgrind())
+        VALGRIND_MAKE_MEM_NOACCESS(p, 1);
+#endif
 }

 /**
@@ -337,6 +369,18 @@ static void unpoison_terminator(uint8_t *p)
 #if defined(ASN1_HAVE_MSAN)
     __msan_unpoison(p, 1);
 #endif
+#if defined(ASN1_HAVE_VALGRIND)
+    if (under_valgrind())
+        VALGRIND_MAKE_MEM_UNDEFINED(p, 1);
+#endif
+}
+
+static void unpoison_buffer(uint8_t *buf, size_t buf_len)
+{
+#if defined(ASN1_HAVE_VALGRIND)
+    if (under_valgrind())
+        VALGRIND_MAKE_MEM_UNDEFINED(buf, buf_len);
+#endif
 }

 int ossl_asn1_string_set_internal(ASN1_STRING *str, const uint8_t *data,
@@ -393,6 +437,7 @@ int ossl_asn1_string_set_internal(ASN1_STRING *str, const uint8_t *data,
         if (c == NULL)
             return 0;
         str->data = c;
+        unpoison_buffer(str->data, alloc_len);
     }
     /* length never includes the added \0 byte */
     str->length = (int)len;
diff --git a/test/asn1_string_poison_test.c b/test/asn1_string_poison_test.c
index fff6ea1db5..76508117c5 100644
--- a/test/asn1_string_poison_test.c
+++ b/test/asn1_string_poison_test.c
@@ -10,12 +10,17 @@
 /**
  * @file asn1_string_poison_test.c
  * Checks that the NUL terminator libcrypto writes after ASN1_STRING data is
- * inaccessible under AddressSanitizer and MemorySanitizer. Run with no
- * argument the program reads the terminator with strlen() and is expected
- * to be killed by the sanitizer; run with "counted" it reads only the
- * counted bytes and is expected to exit successfully. Without a sanitizer
- * the strlen() run exits with failure itself; with one, surviving the
- * strlen() exits successfully, which the recipe reports as a failure.
+ * inaccessible under AddressSanitizer, MemorySanitizer and Valgrind. Run
+ * with no argument the program reads the terminator with strlen() and is
+ * expected to be killed by the sanitizer; run with "counted" it reads only
+ * the counted bytes and is expected to exit successfully. Without a
+ * sanitizer the strlen() run exits with failure itself; with one, surviving
+ * the strlen() exits successfully, which the recipe reports as a failure.
+ * Under Valgrind the process survives the strlen() and memcheck reports the
+ * read. When OSSL_VALGRIND_CT is set the test framework runs memcheck with
+ * --error-exitcode=1, and the program exits successfully after the strlen()
+ * the same way, leaving the exit status to memcheck; under any other
+ * Valgrind wrapper it exits with failure.
  */

 #include <stdlib.h>
@@ -32,10 +37,35 @@
 #if defined(__SANITIZE_ADDRESS__) && !defined(HAVE_SANITIZER)
 #define HAVE_SANITIZER 1
 #endif
+#if defined __has_include
+/* Any compiler you're going to run valgrind on has this */
+#if __has_include(<valgrind/valgrind.h>)
+#include <valgrind/valgrind.h>
+#endif
+#endif /* defined(__has_include) */

 /* DER UTF8String "hello" */
 static const unsigned char der[] = { 0x0c, 0x05, 'h', 'e', 'l', 'l', 'o' };

+/**
+ * @brief Report whether a checker decides this run's exit status.
+ * That is a sanitizer, or memcheck run by the test framework with
+ * --error-exitcode=1 (OSSL_VALGRIND_CT).
+ * @returns 1 when the checker sets the exit status, 0 otherwise
+ */
+static int checker_sets_exit_status(void)
+{
+#if defined(HAVE_SANITIZER)
+    return 1;
+#else
+#if defined(RUNNING_ON_VALGRIND)
+    if (RUNNING_ON_VALGRIND && getenv("OSSL_VALGRIND_CT") != NULL)
+        return 1;
+#endif
+    return 0;
+#endif /* defined(HAVE_SANITIZER) */
+}
+
 /*
  * A plain main() rather than the test framework's: the failing run is
  * expected to die inside the sanitizer, which the framework would report
@@ -61,17 +91,22 @@ int main(int argc, char *argv[])
         goto end;
     }

-    /* Reads the terminator; a sanitizer kills the process here. */
-    sink = strlen((const char *)data);
-    (void)sink;
-#if defined(HAVE_SANITIZER)
     /*
-     * Reached only when the sanitizer did not report the read. The recipe
-     * expects this run to fail; a successful exit is the failure it sees.
+     * Reads the terminator; a sanitizer kills the process here, memcheck
+     * reports the read and lets the process continue.
      */
-    TEST_error("strlen() on ASN1_STRING data was not reported");
-    exitcode = EXIT_SUCCESS;
-#endif
+    sink = strlen((const char *)data);
+    (void)sink;
+    if (checker_sets_exit_status()) {
+        /*
+         * Reached under a sanitizer only when it did not report the read.
+         * The recipe expects this run to fail: under a sanitizer a
+         * successful exit is that failure; under memcheck the exit status
+         * is replaced by --error-exitcode when the read was reported.
+         */
+        TEST_note("strlen() on ASN1_STRING data survived");
+        exitcode = EXIT_SUCCESS;
+    }

 end:
     ASN1_UTF8STRING_free(str);