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();
}