Commit 90621b1728 for openssl.org

commit 90621b1728bda81b33674eb5fbe68f614bbfc51c
Author: Bob Beck <beck@openssl.org>
Date:   Mon Sep 14 10:11:47 2026 -0600

    Poison the ASN1_STRING NUL terminator under ASan and MSan

    The terminator libcrypto writes after ASN1_STRING data stays present, but
    sanitizer builds mark the byte inaccessible, so any C-string use of
    ASN1_STRING_get0_data() output is reported. Add a test that expects the
    report, and say in CHANGES.md that the terminator is going away.

    This is stacked on https://github.com/openssl/openssl/pull/32828

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    Reviewed-by: Milan Broz <mbroz@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Fri Sep 18 09:05:59 2026
    Merged-from: https://github.com/openssl/openssl/pull/32829

diff --git a/CHANGES.md b/CHANGES.md
index e2cf20579e..f7327a6354 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -541,6 +541,16 @@ OpenSSL 4.1
    or `ASN1_STRING_set1_string()`, and `ASN1_STRING_get_length()` should be used
    in their place.  This prepares the `ASN1_STRING` type to support modern
    `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()`.
    <!-- https://github.com/openssl/openssl/pull/31194 -->

    *Bob Beck*
diff --git a/crypto/asn1/asn1_lib.c b/crypto/asn1/asn1_lib.c
index 159e084c52..b815851e96 100644
--- a/crypto/asn1/asn1_lib.c
+++ b/crypto/asn1/asn1_lib.c
@@ -13,6 +13,24 @@
 #include <openssl/asn1.h>
 #include "asn1_local.h"

+#if defined(__has_feature)
+#if __has_feature(address_sanitizer)
+#define ASN1_HAVE_ASAN 1
+#endif
+#if __has_feature(memory_sanitizer)
+#define ASN1_HAVE_MSAN 1
+#endif
+#endif /* defined(__has_feature) */
+#if defined(__SANITIZE_ADDRESS__) && !defined(ASN1_HAVE_ASAN)
+#define ASN1_HAVE_ASAN 1
+#endif
+#if defined(ASN1_HAVE_ASAN)
+#include <sanitizer/asan_interface.h>
+#endif
+#if defined(ASN1_HAVE_MSAN)
+#include <sanitizer/msan_interface.h>
+#endif
+
 static int asn1_get_length(const unsigned char **pp, int *inf, long *rl,
     long max);
 static void asn1_put_length(unsigned char **pp, int length);
@@ -290,6 +308,37 @@ ASN1_STRING *ASN1_STRING_dup(const ASN1_STRING *str)
     return ret;
 }

+/**
+ * @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.
+ * @param p the terminator byte
+ */
+static void poison_terminator(uint8_t *p)
+{
+#if defined(ASN1_HAVE_ASAN)
+    ASAN_POISON_MEMORY_REGION(p, 1);
+#endif
+#if defined(ASN1_HAVE_MSAN)
+    __msan_poison(p, 1);
+#endif
+}
+
+/**
+ * @brief Make the byte at p accessible again before it is written.
+ * @param p the byte about to hold a NUL terminator
+ */
+static void unpoison_terminator(uint8_t *p)
+{
+#if defined(ASN1_HAVE_ASAN)
+    ASAN_UNPOISON_MEMORY_REGION(p, 1);
+#endif
+#if defined(ASN1_HAVE_MSAN)
+    __msan_unpoison(p, 1);
+#endif
+}
+
 int ossl_asn1_string_set_internal(ASN1_STRING *str, const uint8_t *data,
     int len_in, int add_nul_byte)
 {
@@ -348,15 +397,18 @@ int ossl_asn1_string_set_internal(ASN1_STRING *str, const uint8_t *data,
     /* length never includes the added \0 byte */
     str->length = (int)len;

-    if (data != NULL && str->data != NULL) {
+    if (data != NULL && str->data != NULL)
         memcpy(str->data, data, len);
-        if (add_nul_byte) {
-            /*
-             * Add a '\0' terminator. This should not be necessary - but we add it as
-             * a safety precaution
-             */
+    if (add_nul_byte) {
+        /*
+         * The terminator byte lies beyond str->length. It is written only
+         * when data is supplied, and is inaccessible to memory checkers
+         * either way; see poison_terminator().
+         */
+        unpoison_terminator(&str->data[len]);
+        if (data != NULL)
             str->data[len] = '\0';
-        }
+        poison_terminator(&str->data[len]);
     }
     ossl_asn1_bit_string_clear_unused_bits(str);

diff --git a/test/asn1_string_poison_test.c b/test/asn1_string_poison_test.c
new file mode 100644
index 0000000000..fff6ea1db5
--- /dev/null
+++ b/test/asn1_string_poison_test.c
@@ -0,0 +1,79 @@
+/*
+ * 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
+ */
+
+/**
+ * @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.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <openssl/asn1.h>
+
+#include "testutil.h"
+
+#if defined(__has_feature)
+#if __has_feature(address_sanitizer) || __has_feature(memory_sanitizer)
+#define HAVE_SANITIZER 1
+#endif
+#endif /* defined(__has_feature) */
+#if defined(__SANITIZE_ADDRESS__) && !defined(HAVE_SANITIZER)
+#define HAVE_SANITIZER 1
+#endif
+
+/* DER UTF8String "hello" */
+static const unsigned char der[] = { 0x0c, 0x05, 'h', 'e', 'l', 'l', 'o' };
+
+/*
+ * A plain main() rather than the test framework's: the failing run is
+ * expected to die inside the sanitizer, which the framework would report
+ * as a crash.
+ */
+int main(int argc, char *argv[])
+{
+    const unsigned char *p = der;
+    ASN1_UTF8STRING *str = d2i_ASN1_UTF8STRING(NULL, &p, sizeof(der));
+    const unsigned char *data;
+    volatile size_t sink;
+    int exitcode = EXIT_FAILURE;
+
+    if (!TEST_ptr(str)
+        || !TEST_size_t_eq(ASN1_STRING_get_length(str), 5))
+        goto end;
+    data = ASN1_STRING_get0_data(str);
+
+    if (argc > 1 && strcmp(argv[1], "counted") == 0) {
+        /* Counted access never touches the terminator. */
+        if (TEST_mem_eq(data, ASN1_STRING_get_length(str), "hello", 5))
+            exitcode = EXIT_SUCCESS;
+        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.
+     */
+    TEST_error("strlen() on ASN1_STRING data was not reported");
+    exitcode = EXIT_SUCCESS;
+#endif
+
+end:
+    ASN1_UTF8STRING_free(str);
+    return exitcode;
+}
diff --git a/test/build.info b/test/build.info
index 20e2ef19ea..dbfc127e54 100644
--- a/test/build.info
+++ b/test/build.info
@@ -59,6 +59,7 @@ IF[{- !$disabled{tests} -}]
           constant_time_test crypto_memcmp_test ct_validation_helpers_test \
           safe_math_test verify_extra_test clienthellotest \
           packettest asynctest secmemtest srptest memleaktest stack_test \
+          asn1_string_poison_test \
           ct_test threadstest d2i_test \
           ssl_test_ctx_test ssl_test x509aux cipherlist_test asynciotest \
           bio_callback_test bio_memleak_test bio_ndef_test bio_core_test bio_dgram_test param_build_test \
@@ -522,6 +523,10 @@ IF[{- !$disabled{tests} -}]
   INCLUDE[memleaktest]=../include ../apps/include
   DEPEND[memleaktest]=../libcrypto libtestutil.a

+  SOURCE[asn1_string_poison_test]=asn1_string_poison_test.c
+  INCLUDE[asn1_string_poison_test]=../include ../apps/include
+  DEPEND[asn1_string_poison_test]=../libcrypto libtestutil.a
+
   SOURCE[pkcs12_format_test]=pkcs12_format_test.c helpers/pkcs12.c
   INCLUDE[pkcs12_format_test]=../include ../apps/include
   DEPEND[pkcs12_format_test]=../libcrypto libtestutil.a
diff --git a/test/recipes/90-test_asn1_string_poison.t b/test/recipes/90-test_asn1_string_poison.t
new file mode 100644
index 0000000000..e6a584384f
--- /dev/null
+++ b/test/recipes/90-test_asn1_string_poison.t
@@ -0,0 +1,19 @@
+#! /usr/bin/env perl
+# 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
+
+
+use OpenSSL::Test;
+use OpenSSL::Test::Utils;
+
+setup("test_asn1_string_poison");
+
+plan tests => 2;
+ok(!run(test(["asn1_string_poison_test"])),
+   "strlen() on ASN1_STRING data is reported");
+ok(run(test(["asn1_string_poison_test", "counted"])),
+   "counted access to ASN1_STRING data is clean");