Commit 862853b974 for openssl.org
commit 862853b97484857b7d94865fcc3b862a5ed765df
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Wed Sep 2 22:56:36 2026 +0200
Add tests for bio_dgram_pair
Add tests to ensure various race conditions are closed in bio_dgram_pair
usage
Co-authored-by: Neil Horman <nhorman@openssl.org>
Assisted-by: Claude:claude-fable-5-1
Reviewed-by: Andrew Dinh <andrewd@openssl.org>
Reviewed-by: Neil Horman <nhorman@openssl.org>
MergeDate: Fri Sep 11 20:50:59 2026
(Merged from https://github.com/openssl/openssl/pull/32498)
diff --git a/test/bio_dgram_test.c b/test/bio_dgram_test.c
index fab840537c..fdbc3d340f 100644
--- a/test/bio_dgram_test.c
+++ b/test/bio_dgram_test.c
@@ -9,6 +9,7 @@
#include <string.h>
#include <openssl/bio.h>
+#include <openssl/err.h>
#include <openssl/rand.h>
#include "testutil.h"
#include "internal/sockets.h"
@@ -777,6 +778,216 @@ err:
}
#endif /* !defined(OPENSSL_NO_CHACHA) */
+/* Checks that a half without a usable peer fails cleanly. */
+static int check_detached(BIO *bio, int reason)
+{
+ char buf[16];
+ BIO_MSG msg;
+ size_t num_processed = 1;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.data = buf;
+ msg.data_len = sizeof(buf);
+
+ if (!TEST_int_eq(BIO_eof(bio), 1)
+ || !TEST_int_eq(BIO_pending(bio), 0))
+ return 0;
+
+ /* Ctrls reaching for peer data must not touch the freed half. */
+ if (!TEST_uint_eq(BIO_dgram_get_effective_caps(bio), 0)
+ || !TEST_int_eq(BIO_dgram_get_local_addr_cap(bio), 0)
+ || !TEST_true(BIO_dgram_set_mtu(bio, 1506))
+ || !TEST_uint_eq(BIO_dgram_get_mtu(bio), 1506))
+ return 0;
+
+ ERR_clear_error();
+ if (!TEST_int_lt(BIO_read(bio, buf, sizeof(buf)), 0)
+ || !TEST_false(BIO_should_retry(bio))
+ || !TEST_int_eq(ERR_GET_REASON(ERR_peek_error()), reason))
+ return 0;
+
+ ERR_clear_error();
+ if (!TEST_int_lt(BIO_write(bio, "x", 1), 0)
+ || !TEST_false(BIO_should_retry(bio))
+ || !TEST_int_eq(ERR_GET_REASON(ERR_peek_error()), reason))
+ return 0;
+
+ ERR_clear_error();
+ if (!TEST_false(BIO_recvmmsg(bio, &msg, sizeof(msg), 1, 0, &num_processed))
+ || !TEST_size_t_eq(num_processed, 0)
+ || !TEST_int_eq(ERR_GET_REASON(ERR_peek_error()), reason))
+ return 0;
+
+ num_processed = 1;
+ ERR_clear_error();
+ if (!TEST_false(BIO_sendmmsg(bio, &msg, sizeof(msg), 1, 0, &num_processed))
+ || !TEST_size_t_eq(num_processed, 0)
+ || !TEST_int_eq(ERR_GET_REASON(ERR_peek_error()), reason))
+ return 0;
+
+ ERR_clear_error();
+ return 1;
+}
+
+static int test_bio_dgram_pair_free_half(void)
+{
+ int testresult = 0;
+ BIO *bio1 = NULL, *bio2 = NULL, *bio3 = NULL;
+ char buf[16];
+
+ if (!TEST_true(BIO_new_bio_dgram_pair(&bio1, 0, &bio2, 0))
+ || !TEST_int_eq(BIO_write(bio1, "hello", 5), 5)
+ || !TEST_int_eq(BIO_write(bio2, "world", 5), 5))
+ goto err;
+
+ /* Freeing one half must leave the other detached but usable. */
+ BIO_free(bio1);
+ bio1 = NULL;
+ if (!check_detached(bio2, BIO_R_BROKEN_PIPE))
+ goto err;
+
+ /* Which includes pairing it with a new peer. */
+ if (!TEST_ptr(bio3 = BIO_new(BIO_s_dgram_pair()))
+ || !TEST_true(BIO_destroy_bio_pair(bio2))
+ || !TEST_true(BIO_make_bio_pair(bio2, bio3))
+ || !TEST_int_eq(BIO_eof(bio2), 0)
+ || !TEST_int_eq(BIO_write(bio2, "again", 5), 5)
+ || !TEST_int_eq(BIO_read(bio3, buf, sizeof(buf)), 5)
+ || !TEST_mem_eq(buf, 5, "again", 5))
+ goto err;
+
+ /* Destroying the pair on the survivor must cope with a freed peer. */
+ BIO_free(bio3);
+ bio3 = NULL;
+ if (!TEST_true(BIO_destroy_bio_pair(bio2))
+ || !check_detached(bio2, BIO_R_UNINITIALIZED))
+ goto err;
+
+ testresult = 1;
+err:
+ BIO_free(bio1);
+ BIO_free(bio2);
+ BIO_free(bio3);
+ return testresult;
+}
+
+static int test_bio_dgram_pair_destroy(int idx)
+{
+ int testresult = 0;
+ BIO *bio1 = NULL, *bio2 = NULL;
+ BIO_ADDR *addr;
+ char buf[16];
+
+ if (!TEST_true(BIO_new_bio_dgram_pair(&bio1, 0, &bio2, 0))
+ || !TEST_ptr(addr = BIO_ADDR_new())
+ || !TEST_true(BIO_dgram_set0_local_addr(bio1, addr)))
+ goto err;
+
+ if (!TEST_int_eq(BIO_write(bio1, "hello", 5), 5)
+ || !TEST_int_eq(BIO_read(bio2, buf, sizeof(buf)), 5)
+ || !TEST_int_eq(BIO_write(bio2, "world", 5), 5)
+ || !TEST_int_eq(BIO_read(bio1, buf, sizeof(buf)), 5))
+ goto err;
+
+ /* Destroying the pair on either half must detach both cleanly. */
+ if (!TEST_true(BIO_destroy_bio_pair(idx == 0 ? bio1 : bio2))
+ || !check_detached(bio1, idx == 0 ? BIO_R_UNINITIALIZED : BIO_R_BROKEN_PIPE)
+ || !check_detached(bio2, idx == 0 ? BIO_R_BROKEN_PIPE : BIO_R_UNINITIALIZED)
+ || !TEST_true(BIO_destroy_bio_pair(bio1))
+ || !TEST_true(BIO_destroy_bio_pair(bio2)))
+ goto err;
+
+ /* Both halves are free to be paired again. */
+ if (!TEST_true(BIO_make_bio_pair(bio1, bio2))
+ || !TEST_int_eq(BIO_write(bio1, "again", 5), 5)
+ || !TEST_int_eq(BIO_read(bio2, buf, sizeof(buf)), 5)
+ || !TEST_mem_eq(buf, 5, "again", 5))
+ goto err;
+
+ testresult = 1;
+err:
+ BIO_free(bio1);
+ BIO_free(bio2);
+ return testresult;
+}
+
+#if defined(OPENSSL_THREADS) && !defined(CRYPTO_TDEBUG)
+#include "threadstest.h"
+
+static BIO *reader_bio;
+static int reader_reason, reader_eof, reader_write;
+
+/* Keeps receiving on its half until the peer is freed underneath it. */
+static void free_race_reader(void)
+{
+ char buf[16];
+ BIO_MSG msg;
+ size_t num_processed;
+ int r;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.data = buf;
+ msg.data_len = sizeof(buf);
+
+ for (;;) {
+ if (BIO_recvmmsg(reader_bio, &msg, sizeof(msg), 1, 0, &num_processed))
+ continue;
+ /* Writes look at the peer too, so keep them in the mix. */
+ ERR_clear_error();
+ r = BIO_write(reader_bio, "x", 1);
+ if (r < 0 && !BIO_should_retry(reader_bio))
+ break;
+ ERR_clear_error();
+ r = BIO_read(reader_bio, buf, sizeof(buf));
+ if (r < 0 && !BIO_should_retry(reader_bio))
+ break;
+ }
+
+ reader_reason = ERR_GET_REASON(ERR_peek_error());
+ reader_eof = BIO_eof(reader_bio);
+ reader_write = BIO_write(reader_bio, "x", 1);
+ ERR_clear_error();
+}
+
+static int test_bio_dgram_pair_free_race(void)
+{
+ int testresult = 0, written = 0, r;
+ BIO *bio1 = NULL, *bio2 = NULL;
+ thread_t thread;
+
+ if (!TEST_true(BIO_new_bio_dgram_pair(&bio1, 0, &bio2, 0)))
+ goto err;
+
+ reader_bio = bio2;
+ if (!TEST_true(run_thread(&thread, free_race_reader)))
+ goto err;
+
+ /* Keep the reader busy, then pull its peer out from under it. */
+ while (written < 1000) {
+ r = BIO_write(bio1, "ping", 4);
+ if (r == 4)
+ written++;
+ else if (!BIO_should_retry(bio1))
+ break;
+ }
+ BIO_free(bio1);
+ bio1 = NULL;
+
+ if (!TEST_true(wait_for_thread(thread))
+ || !TEST_int_eq(written, 1000)
+ || !TEST_int_eq(reader_reason, BIO_R_BROKEN_PIPE)
+ || !TEST_int_eq(reader_eof, 1)
+ || !TEST_int_lt(reader_write, 0))
+ goto err;
+
+ testresult = 1;
+err:
+ BIO_free(bio1);
+ BIO_free(bio2);
+ return testresult;
+}
+#endif
+
static int test_bio_dgram_mfail(void)
{
BIO *bio;
@@ -805,6 +1016,11 @@ int setup_tests(void)
ADD_ALL_TESTS(test_bio_dgram, OSSL_NELEM(bio_dgram_cases));
#if !defined(OPENSSL_NO_CHACHA)
ADD_ALL_TESTS(test_bio_dgram_pair, 3);
+#endif
+ ADD_TEST(test_bio_dgram_pair_free_half);
+ ADD_ALL_TESTS(test_bio_dgram_pair_destroy, 2);
+#if defined(OPENSSL_THREADS) && !defined(CRYPTO_TDEBUG)
+ ADD_TEST(test_bio_dgram_pair_free_race);
#endif
ADD_MFAIL_TEST(test_bio_dgram_mfail);
#endif