Commit 9c15051a9f for openssl.org

commit 9c15051a9fe23aae617c5604b81081dbb1ece4d1
Author: Bob Beck <beck@openssl.org>
Date:   Sun Aug 16 09:55:28 2026 -0700

    Make test framework output records atomic across threads

    A single TEST_info() message is composed of several writes, each
    taking the output lock independently, so concurrent callers
    interleaved mid-line. This makes it annoying when we run multiple
    threads in a test job and get interleaved output that is hard to
    diagnose.

    Track the lock depth per thread so it can be held across a whole
    record.  A thread with no thread-local data cannot track the depth and
    falls back to the previous per-write locking.

    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Tue Sep  1 14:17:16 2026
    Merged-from: https://github.com/openssl/openssl/pull/32404

diff --git a/test/testutil/basic_output.c b/test/testutil/basic_output.c
index 110cc252d4..6cd77b6c13 100644
--- a/test/testutil/basic_output.c
+++ b/test/testutil/basic_output.c
@@ -24,6 +24,7 @@ static BIO *tap_err = NULL;

 typedef struct local_test_data_st {
     BIO *override_bio_out, *override_bio_err;
+    int io_lock_depth; /**< Recursion depth of this thread's io_lock hold */
 } LOCAL_TEST_DATA;

 #if defined(OPENSSL_THREADS)
@@ -185,9 +186,20 @@ void test_close_streams(void)
 #endif
 }

+/*-
+ * The lock is taken recursively, tracked by a per-thread depth count, so
+ * that a caller may hold it across a whole output record while the
+ * individual writes making up that record continue to take it.  A thread
+ * with no thread-local data available cannot track the depth, and so
+ * falls back to taking the lock for each write.
+ */
 static ossl_inline void test_io_lock(void)
 {
 #if defined(OPENSSL_THREADS)
+    LOCAL_TEST_DATA *data = get_local_test_data();
+
+    if (data != NULL && data->io_lock_depth++ > 0)
+        return;
     OPENSSL_assert(CRYPTO_THREAD_write_lock(io_lock) > 0);
 #endif
 }
@@ -195,10 +207,44 @@ static ossl_inline void test_io_lock(void)
 static ossl_inline void test_io_unlock(void)
 {
 #if defined(OPENSSL_THREADS)
+    LOCAL_TEST_DATA *data = get_local_test_data();
+
+    if (data != NULL && --data->io_lock_depth > 0)
+        return;
     CRYPTO_THREAD_unlock(io_lock);
 #endif
 }

+/*-
+ * A thread with no thread-local data has nowhere to record the depth, so
+ * it must not hold the lock across the record: the writes within would
+ * deadlock against it.  Such a thread falls back to the per-write
+ * locking, and its records may interleave as they did before.
+ *
+ * The depth is tested rather than assumed on the way out, so that a
+ * begin which found no thread-local data is not matched by an end which
+ * finds some and releases a lock this thread never took.
+ */
+void test_output_record_begin(void)
+{
+#if defined(OPENSSL_THREADS)
+    if (get_local_test_data() == NULL)
+        return;
+    test_io_lock();
+#endif
+}
+
+void test_output_record_end(void)
+{
+#if defined(OPENSSL_THREADS)
+    LOCAL_TEST_DATA *data = get_local_test_data();
+
+    if (data == NULL || data->io_lock_depth == 0)
+        return;
+    test_io_unlock();
+#endif
+}
+
 int test_vprintf_stdout(const char *fmt, va_list ap)
 {
     int r;
diff --git a/test/testutil/format_output.c b/test/testutil/format_output.c
index 5bb5302989..9136202c74 100644
--- a/test/testutil/format_output.c
+++ b/test/testutil/format_output.c
@@ -129,15 +129,19 @@ void test_fail_string_message(const char *prefix, const char *file,
     const char *op, const char *m1, size_t l1,
     const char *m2, size_t l2)
 {
+    test_output_record_begin();
     test_fail_string_common(prefix, file, line, type, left, right, op,
         m1, l1, m2, l2);
     test_printf_stderr("\n");
+    test_output_record_end();
 }

 void test_output_string(const char *name, const char *m, size_t l)
 {
+    test_output_record_begin();
     test_fail_string_common("string", NULL, 0, NULL, NULL, NULL, name,
         m, l, m, l);
+    test_output_record_end();
 }

 /* BIGNUM formatted output routines */
@@ -369,8 +373,10 @@ void test_fail_bignum_message(const char *prefix, const char *file,
     const char *op,
     const BIGNUM *bn1, const BIGNUM *bn2)
 {
+    test_output_record_begin();
     test_fail_bignum_common(prefix, file, line, type, left, right, op, bn1, bn2);
     test_printf_stderr("\n");
+    test_output_record_end();
 }

 void test_fail_bignum_mono_message(const char *prefix, const char *file,
@@ -378,12 +384,15 @@ void test_fail_bignum_mono_message(const char *prefix, const char *file,
     const char *left, const char *right,
     const char *op, const BIGNUM *bn)
 {
+    test_output_record_begin();
     test_fail_bignum_common(prefix, file, line, type, left, right, op, bn, bn);
     test_printf_stderr("\n");
+    test_output_record_end();
 }

 void test_output_bignum(const char *name, const BIGNUM *bn)
 {
+    test_output_record_begin();
     if (bn == NULL || BN_is_zero(bn)) {
         test_printf_stderr("bignum: '%s' = %s\n",
             name == NULL ? "<NULL>" : name,
@@ -404,6 +413,7 @@ void test_output_bignum(const char *name, const BIGNUM *bn)
         test_fail_bignum_common("bignum", NULL, 0, NULL, NULL, NULL, name,
             bn, bn);
     }
+    test_output_record_end();
 }

 /* Memory output routines */
@@ -524,13 +534,17 @@ void test_fail_memory_message(const char *prefix, const char *file,
     const unsigned char *m1, size_t l1,
     const unsigned char *m2, size_t l2)
 {
+    test_output_record_begin();
     test_fail_memory_common(prefix, file, line, type, left, right, op,
         m1, l1, m2, l2);
     test_printf_stderr("\n");
+    test_output_record_end();
 }

 void test_output_memory(const char *name, const unsigned char *m, size_t l)
 {
+    test_output_record_begin();
     test_fail_memory_common("memory", NULL, 0, NULL, NULL, NULL, name,
         m, l, m, l);
+    test_output_record_end();
 }
diff --git a/test/testutil/output.h b/test/testutil/output.h
index e4f6058ac7..7a52454dd4 100644
--- a/test/testutil/output.h
+++ b/test/testutil/output.h
@@ -40,6 +40,21 @@ int test_flush_tapout(void);
 int test_flush_stderr(void);
 int test_flush_taperr(void);

+/**
+ * @brief Begin a logical output record.
+ * Writes made by this thread until the matching test_output_record_end()
+ * are emitted as a unit, so that a record composed of several writes is
+ * not interleaved with output from another thread.  Calls nest; only the
+ * outermost pair has an effect.  Has no effect in a build without
+ * threads.
+ */
+void test_output_record_begin(void);
+
+/**
+ * @brief End a logical output record begun by test_output_record_begin().
+ */
+void test_output_record_end(void);
+
 /* Commodity functions.  There's no need to override these */
 int test_printf_stdout(const char *fmt, ...)
     ossl_test__attr__((__format__(__printf__, 1, 2)));
diff --git a/test/testutil/tests.c b/test/testutil/tests.c
index 44908f9779..88e8789225 100644
--- a/test/testutil/tests.c
+++ b/test/testutil/tests.c
@@ -76,12 +76,14 @@ static void test_fail_message_va(const char *prefix, const char *file,
     const char *left, const char *right,
     const char *op, const char *fmt, va_list ap)
 {
+    test_output_record_begin();
     test_fail_message_prefix(prefix, file, line, type, left, right, op);
     if (fmt != NULL) {
         test_vprintf_stderr(fmt, ap);
         test_printf_stderr("\n");
     }
     test_flush_stderr();
+    test_output_record_end();
 }

 static void test_fail_message(const char *prefix, const char *file,
@@ -118,20 +120,24 @@ void test_error_c90(const char *desc, ...)
 {
     va_list ap;

+    test_output_record_begin();
     va_start(ap, desc);
     test_fail_message_va(NULL, NULL, -1, NULL, NULL, NULL, NULL, desc, ap);
     va_end(ap);
     test_printf_stderr("\n");
+    test_output_record_end();
 }

 void test_error(const char *file, int line, const char *desc, ...)
 {
     va_list ap;

+    test_output_record_begin();
     va_start(ap, desc);
     test_fail_message_va(NULL, file, line, NULL, NULL, NULL, NULL, desc, ap);
     va_end(ap);
     test_printf_stderr("\n");
+    test_output_record_end();
 }

 void test_perror(const char *s)
@@ -145,6 +151,7 @@ void test_perror(const char *s)

 void test_note(const char *fmt, ...)
 {
+    test_output_record_begin();
     test_flush_stdout();
     if (fmt != NULL) {
         va_list ap;
@@ -155,6 +162,7 @@ void test_note(const char *fmt, ...)
         test_printf_stderr("\n");
     }
     test_flush_stderr();
+    test_output_record_end();
 }

 int test_skip(const char *file, int line, const char *desc, ...)
@@ -171,16 +179,20 @@ int test_skip_c90(const char *desc, ...)
 {
     va_list ap;

+    test_output_record_begin();
     va_start(ap, desc);
     test_fail_message_va("SKIP", NULL, -1, NULL, NULL, NULL, NULL, desc, ap);
     va_end(ap);
     test_printf_stderr("\n");
+    test_output_record_end();
     return TEST_SKIP_CODE;
 }

 void test_openssl_errors(void)
 {
+    test_output_record_begin();
     ERR_print_errors_cb(openssl_error_cb, NULL);
+    test_output_record_end();
     ERR_clear_error();
 }