Commit b05333b7e9 for openssl.org

commit b05333b7e9a47aec2fe4a28915be0497a7480c44
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Wed Jun 10 13:04:07 2026 +0200

    Add BIO bss_dgram test

    Assisted-by: Claude:claude-opus-4-8

    Reviewed-by: Richard Levitte <levitte@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    MergeDate: Thu Jul  9 17:39:21 2026
    (Merged from https://github.com/openssl/openssl/pull/30788)

diff --git a/crypto/bio/bio_local.h b/crypto/bio/bio_local.h
index 4e2e974039..87227e4f8f 100644
--- a/crypto/bio/bio_local.h
+++ b/crypto/bio/bio_local.h
@@ -74,6 +74,7 @@ struct bio_addrinfo_st {
 #include "internal/cryptlib.h"
 #include "internal/bio.h"
 #include "internal/refcount.h"
+#include "internal/time.h"

 typedef struct bio_f_buffer_ctx_struct {
     /*-
@@ -174,6 +175,20 @@ typedef struct bio_accept_st {
     BIO *bio_chain;
 } BIO_ACCEPT;

+#ifndef OPENSSL_NO_DGRAM
+typedef struct bio_dgram_data_st {
+    BIO_ADDR peer;
+    BIO_ADDR local_addr;
+    unsigned int connected;
+    unsigned int _errno;
+    unsigned int mtu;
+    OSSL_TIME next_timeout;
+    OSSL_TIME socket_timeout;
+    unsigned int peekmode;
+    char local_addr_enabled;
+} bio_dgram_data;
+#endif
+
 #define BIO_CONN_S_BEFORE 1
 #define BIO_CONN_S_GET_ADDR 2
 #define BIO_CONN_S_CREATE_SOCKET 3
diff --git a/crypto/bio/bss_dgram.c b/crypto/bio/bss_dgram.c
index 0a1479e834..1d98239511 100644
--- a/crypto/bio/bss_dgram.c
+++ b/crypto/bio/bss_dgram.c
@@ -14,7 +14,6 @@
 #include <stdio.h>
 #include <errno.h>

-#include "internal/time.h"
 #include "bio_local.h"
 #ifndef OPENSSL_NO_DGRAM

@@ -213,21 +212,7 @@ static const BIO_METHOD methods_dgramp_sctp = {
     NULL, /* sendmmsg */
     NULL, /* recvmmsg */
 };
-#endif
-
-typedef struct bio_dgram_data_st {
-    BIO_ADDR peer;
-    BIO_ADDR local_addr;
-    unsigned int connected;
-    unsigned int _errno;
-    unsigned int mtu;
-    OSSL_TIME next_timeout;
-    OSSL_TIME socket_timeout;
-    unsigned int peekmode;
-    char local_addr_enabled;
-} bio_dgram_data;

-#ifndef OPENSSL_NO_SCTP
 typedef struct bio_dgram_sctp_save_message_st {
     BIO *bio;
     char *data;
diff --git a/test/unit/build.info b/test/unit/build.info
index ae53ad8d61..6e300d992c 100644
--- a/test/unit/build.info
+++ b/test/unit/build.info
@@ -22,6 +22,13 @@ WRAP[crypto/bio/test_bss_conn]=BIO_lookup BIO_socket BIO_connect \
 INCLUDE[crypto/bio/test_bss_conn]=../../include ../../crypto/bio
 DEPEND[crypto/bio/test_bss_conn]=../../libcrypto.a

+PROGRAMS{noinst}=crypto/bio/test_bss_dgram
+SOURCE[crypto/bio/test_bss_dgram]=crypto/bio/test_bss_dgram.c
+WRAP[crypto/bio/test_bss_dgram]=recvfrom sendto write getsockname \
+    getpeername BIO_closesocket BIO_socket_nbio
+INCLUDE[crypto/bio/test_bss_dgram]=../../include ../../crypto/bio
+DEPEND[crypto/bio/test_bss_dgram]=../../libcrypto.a
+
 PROGRAMS{noinst}=crypto/bio/test_bss_fd
 SOURCE[crypto/bio/test_bss_fd]=crypto/bio/test_bss_fd.c
 INCLUDE[crypto/bio/test_bss_fd]=../../include ../../crypto/bio
diff --git a/test/unit/crypto/bio/test_bss_dgram.c b/test/unit/crypto/bio/test_bss_dgram.c
new file mode 100644
index 0000000000..4a39ebba5d
--- /dev/null
+++ b/test/unit/crypto/bio/test_bss_dgram.c
@@ -0,0 +1,772 @@
+/*
+ * 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
+ */
+
+#if defined(OPENSSL_NO_SOCK) || defined(OPENSSL_NO_DGRAM)
+
+int main(void)
+{
+    return 0;
+}
+
+#else
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <string.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <cmocka.h>
+#include "bio_local.h"
+#include <openssl/bio.h>
+
+#define FAKE_SOCKET 42
+
+/* prototypes for __wrap_* (required by -Wmissing-prototypes) */
+ssize_t __wrap_recvfrom(int fd, void *buf, size_t len, int flags,
+    struct sockaddr *src, socklen_t *slen);
+ssize_t __wrap_sendto(int fd, const void *buf, size_t len, int flags,
+    const struct sockaddr *dst, socklen_t slen);
+ssize_t __wrap_write(int fd, const void *buf, size_t count);
+int __wrap_getsockname(int fd, struct sockaddr *addr, socklen_t *slen);
+int __wrap_getpeername(int fd, struct sockaddr *addr, socklen_t *slen);
+int __wrap_BIO_closesocket(int fd);
+int __wrap_BIO_socket_nbio(int fd, int mode);
+
+/*
+ * Shared sockaddrs handed back by the address-returning mocks.  group_setup
+ * fills g_sin (AF_INET loopback); g_sin6 is an AF_INET6 loopback and
+ * g_sin6_v4m an IPv4-mapped IPv6 address (::ffff:127.0.0.1).
+ */
+static struct sockaddr_in g_sin;
+static struct sockaddr_in6 g_sin6;
+static struct sockaddr_in6 g_sin6_v4m;
+
+/* wraps */
+
+ssize_t __wrap_recvfrom(int fd, void *buf, size_t len, int flags,
+    struct sockaddr *src, socklen_t *slen)
+{
+    ssize_t rc;
+
+    function_called();
+    check_expected(fd);
+    check_expected_ptr(buf);
+    check_expected(len);
+    check_expected(flags);
+    rc = mock_type(ssize_t);
+    if (rc >= 0 && src != NULL && slen != NULL) {
+        const struct sockaddr *sa = mock_ptr_type(const struct sockaddr *);
+
+        if (sa != NULL && *slen >= sizeof(g_sin)) {
+            memcpy(src, sa, sizeof(g_sin));
+            *slen = sizeof(g_sin);
+        }
+    }
+    if (rc < 0)
+        errno = mock_type(int);
+    return rc;
+}
+
+ssize_t __wrap_sendto(int fd, const void *buf, size_t len, int flags,
+    const struct sockaddr *dst, socklen_t slen)
+{
+    ssize_t rc;
+
+    function_called();
+    check_expected(fd);
+    check_expected_ptr(buf);
+    check_expected(len);
+    check_expected(flags);
+    check_expected(slen);
+    (void)dst;
+    rc = mock_type(ssize_t);
+    if (rc < 0)
+        errno = mock_type(int);
+    return rc;
+}
+
+ssize_t __wrap_write(int fd, const void *buf, size_t count)
+{
+    function_called();
+    check_expected(fd);
+    check_expected_ptr(buf);
+    check_expected(count);
+    return mock_type(ssize_t);
+}
+
+int __wrap_getsockname(int fd, struct sockaddr *addr, socklen_t *slen)
+{
+    int rc;
+
+    function_called();
+    check_expected(fd);
+    rc = mock_type(int);
+    if (rc == 0 && addr != NULL && slen != NULL) {
+        const struct sockaddr *sa = mock_ptr_type(const struct sockaddr *);
+        socklen_t sl = (socklen_t)mock_type(int);
+
+        if (sa != NULL && *slen >= sl) {
+            memcpy(addr, sa, sl);
+            *slen = sl;
+        }
+    }
+    return rc;
+}
+
+int __wrap_getpeername(int fd, struct sockaddr *addr, socklen_t *slen)
+{
+    int rc;
+
+    function_called();
+    check_expected(fd);
+    rc = mock_type(int);
+    if (rc == 0 && addr != NULL && slen != NULL && *slen >= sizeof(g_sin)) {
+        memcpy(addr, &g_sin, sizeof(g_sin));
+        *slen = sizeof(g_sin);
+    }
+    return rc;
+}
+
+int __wrap_BIO_closesocket(int fd)
+{
+    function_called();
+    check_expected(fd);
+    return mock_type(int);
+}
+
+int __wrap_BIO_socket_nbio(int fd, int mode)
+{
+    function_called();
+    check_expected(fd);
+    check_expected(mode);
+    return mock_type(int);
+}
+
+/* expectations */
+
+static void expect_recvfrom(int fd, const void *buf, size_t len, int flags,
+    ssize_t rc, const struct sockaddr *src, int errnoval)
+{
+    expect_function_call(__wrap_recvfrom);
+    expect_value(__wrap_recvfrom, fd, fd);
+    expect_value(__wrap_recvfrom, buf, buf);
+    expect_value(__wrap_recvfrom, len, len);
+    expect_value(__wrap_recvfrom, flags, flags);
+    will_return(__wrap_recvfrom, rc);
+    if (rc >= 0)
+        will_return(__wrap_recvfrom, src);
+    else
+        will_return(__wrap_recvfrom, errnoval);
+}
+
+static void expect_sendto(int fd, const void *buf, size_t len, int flags,
+    socklen_t slen, ssize_t rc, int errnoval)
+{
+    expect_function_call(__wrap_sendto);
+    expect_value(__wrap_sendto, fd, fd);
+    expect_value(__wrap_sendto, buf, buf);
+    expect_value(__wrap_sendto, len, len);
+    expect_value(__wrap_sendto, flags, flags);
+    expect_value(__wrap_sendto, slen, slen);
+    will_return(__wrap_sendto, rc);
+    if (rc < 0)
+        will_return(__wrap_sendto, errnoval);
+}
+
+static void expect_write(int fd, const void *buf, size_t count, ssize_t rc)
+{
+    expect_function_call(__wrap_write);
+    expect_value(__wrap_write, fd, fd);
+    expect_value(__wrap_write, buf, buf);
+    expect_value(__wrap_write, count, count);
+    will_return(__wrap_write, rc);
+}
+
+static void expect_getsockname(int fd, int rc)
+{
+    expect_function_call(__wrap_getsockname);
+    expect_value(__wrap_getsockname, fd, fd);
+    will_return(__wrap_getsockname, rc);
+    if (rc == 0) {
+        will_return(__wrap_getsockname, (const struct sockaddr *)&g_sin);
+        will_return(__wrap_getsockname, (int)sizeof(g_sin));
+    }
+}
+
+static void expect_getpeername(int fd, int rc)
+{
+    expect_function_call(__wrap_getpeername);
+    expect_value(__wrap_getpeername, fd, fd);
+    will_return(__wrap_getpeername, rc);
+}
+
+static void expect_BIO_closesocket(int fd, int rc)
+{
+    expect_function_call(__wrap_BIO_closesocket);
+    expect_value(__wrap_BIO_closesocket, fd, fd);
+    will_return(__wrap_BIO_closesocket, rc);
+}
+
+static void expect_BIO_socket_nbio(int fd, int mode, int rc)
+{
+    expect_function_call(__wrap_BIO_socket_nbio);
+    expect_value(__wrap_BIO_socket_nbio, fd, fd);
+    expect_value(__wrap_BIO_socket_nbio, mode, mode);
+    will_return(__wrap_BIO_socket_nbio, rc);
+}
+
+/* helpers */
+
+static bio_dgram_data *get_data(BIO *bio)
+{
+    return (bio_dgram_data *)bio->ptr;
+}
+
+static void make_peer(BIO *bio, unsigned short port)
+{
+    struct sockaddr_in sa;
+
+    memset(&sa, 0, sizeof(sa));
+    sa.sin_family = AF_INET;
+    sa.sin_port = htons(port);
+    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+    BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_PEER, 0, &sa);
+}
+
+/*
+ * Detach the fake socket before teardown so BIO_free does not call a real
+ * close: BIO_NOCLOSE already guards it, but resetting num keeps any stray
+ * dgram_clear path inert.
+ */
+static void reset_for_teardown(BIO *bio)
+{
+    bio->num = (int)INVALID_SOCKET;
+    bio->shutdown = BIO_NOCLOSE;
+}
+
+/* setup / teardown */
+
+static int setup(void **state)
+{
+    BIO *bio = BIO_new(BIO_s_datagram());
+
+    assert_non_null(bio);
+    *state = bio;
+    return 0;
+}
+
+static int teardown(void **state)
+{
+    if (*state != NULL)
+        BIO_free(*state);
+    return 0;
+}
+
+/* I/O tests attach the fake socket up front so the BIO is init'ed. */
+static int setup_io(void **state)
+{
+    BIO *bio;
+
+    if (setup(state) != 0)
+        return -1;
+    bio = *state;
+    expect_getsockname(FAKE_SOCKET, 0);
+    expect_getpeername(FAKE_SOCKET, -1);
+    BIO_set_fd(bio, FAKE_SOCKET, BIO_NOCLOSE);
+    return 0;
+}
+
+static int teardown_io(void **state)
+{
+    if (*state != NULL)
+        reset_for_teardown(*state);
+    return teardown(state);
+}
+
+static int group_setup(void **state)
+{
+    struct in6_addr loop6 = IN6ADDR_LOOPBACK_INIT;
+
+    (void)state;
+    memset(&g_sin, 0, sizeof(g_sin));
+    g_sin.sin_family = AF_INET;
+    g_sin.sin_port = htons(443);
+    g_sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+    memset(&g_sin6, 0, sizeof(g_sin6));
+    g_sin6.sin6_family = AF_INET6;
+    g_sin6.sin6_port = htons(443);
+    g_sin6.sin6_addr = loop6;
+
+    /* ::ffff:127.0.0.1 -> IPv4-mapped, exercises the v4mapped MTU branch */
+    memset(&g_sin6_v4m, 0, sizeof(g_sin6_v4m));
+    g_sin6_v4m.sin6_family = AF_INET6;
+    g_sin6_v4m.sin6_port = htons(443);
+    g_sin6_v4m.sin6_addr.s6_addr[10] = 0xff;
+    g_sin6_v4m.sin6_addr.s6_addr[11] = 0xff;
+    g_sin6_v4m.sin6_addr.s6_addr[12] = 127;
+    g_sin6_v4m.sin6_addr.s6_addr[15] = 1;
+    return 0;
+}
+
+/* BIO_new_dgram */
+static void test_new_dgram(void **state)
+{
+    int out = -1;
+    BIO *bio;
+
+    (void)state;
+    expect_getsockname(FAKE_SOCKET, 0);
+    expect_getpeername(FAKE_SOCKET, -1);
+    bio = BIO_new_dgram(FAKE_SOCKET, BIO_NOCLOSE);
+    assert_non_null(bio);
+    assert_int_equal(BIO_get_fd(bio, &out), FAKE_SOCKET);
+    assert_int_equal(out, FAKE_SOCKET);
+    reset_for_teardown(bio);
+    BIO_free(bio);
+}
+
+static void test_dgram_new_defaults(void **state)
+{
+    BIO *bio = *state;
+
+    assert_non_null(get_data(bio));
+    assert_int_equal(bio->num, 0);
+    assert_int_equal(bio->init, 0);
+}
+
+/* BIO_C_SET_FD / BIO_C_GET_FD */
+
+static void test_set_fd_unconnected(void **state)
+{
+    BIO *bio = *state;
+    bio_dgram_data *data;
+    int out = -1;
+
+    expect_getsockname(FAKE_SOCKET, 0);
+    expect_getpeername(FAKE_SOCKET, -1);
+    BIO_set_fd(bio, FAKE_SOCKET, BIO_NOCLOSE);
+
+    data = get_data(bio);
+    assert_int_equal(data->connected, 0);
+    assert_int_equal(bio->init, 1);
+    assert_int_equal(BIO_get_fd(bio, &out), FAKE_SOCKET);
+    assert_int_equal(out, FAKE_SOCKET);
+    reset_for_teardown(bio);
+}
+
+static void test_set_fd_connected(void **state)
+{
+    /* getpeername succeeds: peer is recorded and connected is set */
+    BIO *bio = *state;
+    bio_dgram_data *data;
+
+    expect_getsockname(FAKE_SOCKET, 0);
+    expect_getpeername(FAKE_SOCKET, 0);
+    BIO_set_fd(bio, FAKE_SOCKET, BIO_NOCLOSE);
+
+    data = get_data(bio);
+    assert_int_equal(data->connected, 1);
+    assert_int_equal(BIO_ADDR_family(&data->peer), AF_INET);
+    reset_for_teardown(bio);
+}
+
+static void test_get_fd_uninit(void **state)
+{
+    assert_int_equal(BIO_ctrl(*state, BIO_C_GET_FD, 0, NULL), -1);
+}
+
+/* dgram_read */
+
+static void test_dgram_read_noop(void **state)
+{
+    /* outl == 0: recvfrom is never reached */
+    BIO *bio = *state;
+    char buf[1];
+
+    assert_int_equal(BIO_read(bio, buf, 0), 0);
+}
+
+static void test_dgram_read_success(void **state)
+{
+    BIO *bio = *state;
+    char buf[16] = { 0 };
+
+    expect_recvfrom(FAKE_SOCKET, buf, sizeof(buf), 0, 4, NULL, 0);
+    assert_int_equal(BIO_read(bio, buf, sizeof(buf)), 4);
+    assert_false(BIO_should_retry(bio));
+}
+
+static void test_dgram_read_sets_peer(void **state)
+{
+    /* unconnected receive records the source address as the peer */
+    BIO *bio = *state;
+    bio_dgram_data *data = get_data(bio);
+    char buf[16] = { 0 };
+
+    expect_recvfrom(FAKE_SOCKET, buf, sizeof(buf), 0, 4,
+        (const struct sockaddr *)&g_sin, 0);
+    assert_int_equal(BIO_read(bio, buf, sizeof(buf)), 4);
+    assert_int_equal(BIO_ADDR_family(&data->peer), AF_INET);
+}
+
+static void test_dgram_read_peek(void **state)
+{
+    /* peekmode passes MSG_PEEK to recvfrom */
+    BIO *bio = *state;
+    char buf[16] = { 0 };
+
+    get_data(bio)->peekmode = 1;
+    expect_recvfrom(FAKE_SOCKET, buf, sizeof(buf), MSG_PEEK, 4, NULL, 0);
+    assert_int_equal(BIO_read(bio, buf, sizeof(buf)), 4);
+}
+
+static void test_dgram_read_retry(void **state)
+{
+    BIO *bio = *state;
+    char buf[16] = { 0 };
+
+    expect_recvfrom(FAKE_SOCKET, buf, sizeof(buf), 0, -1, NULL, EAGAIN);
+    assert_true(BIO_read(bio, buf, sizeof(buf)) <= 0);
+    assert_true(BIO_should_read(bio));
+}
+
+static void test_dgram_read_error(void **state)
+{
+    BIO *bio = *state;
+    char buf[16] = { 0 };
+
+    expect_recvfrom(FAKE_SOCKET, buf, sizeof(buf), 0, -1, NULL, ECONNREFUSED);
+    assert_true(BIO_read(bio, buf, sizeof(buf)) <= 0);
+    assert_false(BIO_should_retry(bio));
+}
+
+/* dgram_write */
+
+static void test_dgram_write_unconnected(void **state)
+{
+    BIO *bio = *state;
+    const char buf[] = "hello";
+
+    make_peer(bio, 4433);
+    expect_sendto(FAKE_SOCKET, buf, 5, 0,
+        (socklen_t)sizeof(struct sockaddr_in), 5, 0);
+    assert_int_equal(BIO_write(bio, buf, 5), 5);
+    assert_false(BIO_should_retry(bio));
+}
+
+static void test_dgram_write_connected(void **state)
+{
+    BIO *bio = *state;
+    bio_dgram_data *data = get_data(bio);
+    const char buf[] = "hello";
+
+    data->connected = 1;
+    expect_write(FAKE_SOCKET, buf, 5, 5);
+    assert_int_equal(BIO_write(bio, buf, 5), 5);
+    data->connected = 0;
+}
+
+static void test_dgram_write_retry(void **state)
+{
+    BIO *bio = *state;
+    const char buf[] = "hello";
+
+    make_peer(bio, 4433);
+    expect_sendto(FAKE_SOCKET, buf, 5, 0,
+        (socklen_t)sizeof(struct sockaddr_in), -1, EAGAIN);
+    assert_true(BIO_write(bio, buf, 5) <= 0);
+    assert_true(BIO_should_write(bio));
+}
+
+static void test_dgram_write_error(void **state)
+{
+    BIO *bio = *state;
+    const char buf[] = "hello";
+
+    make_peer(bio, 4433);
+    expect_sendto(FAKE_SOCKET, buf, 5, 0,
+        (socklen_t)sizeof(struct sockaddr_in), -1, ECONNREFUSED);
+    assert_true(BIO_write(bio, buf, 5) <= 0);
+    assert_false(BIO_should_retry(bio));
+}
+
+/* dgram_ctrl */
+
+static void test_ctrl_reset(void **state)
+{
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_RESET, 0, NULL), 0);
+}
+
+static void test_ctrl_info(void **state)
+{
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_INFO, 0, NULL), 0);
+}
+
+static void test_ctrl_pending_wpending(void **state)
+{
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_PENDING, 0, NULL), 0);
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_WPENDING, 0, NULL), 0);
+}
+
+static void test_ctrl_dup_flush(void **state)
+{
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_DUP, 0, NULL), 1);
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_FLUSH, 0, NULL), 1);
+}
+
+static void test_ctrl_get_set_close(void **state)
+{
+    BIO *bio = *state;
+
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_GET_CLOSE, 0, NULL), BIO_NOCLOSE);
+    BIO_ctrl(bio, BIO_CTRL_SET_CLOSE, BIO_CLOSE, NULL);
+    assert_int_equal(bio->shutdown, BIO_CLOSE);
+    bio->shutdown = BIO_NOCLOSE;
+}
+
+static void test_ctrl_connect(void **state)
+{
+    BIO *bio = *state;
+    bio_dgram_data *data = get_data(bio);
+    struct sockaddr_in sa;
+
+    memset(&sa, 0, sizeof(sa));
+    sa.sin_family = AF_INET;
+    sa.sin_port = htons(4433);
+    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+    BIO_ctrl(bio, BIO_CTRL_DGRAM_CONNECT, 0, &sa);
+    assert_int_equal(BIO_ADDR_family(&data->peer), AF_INET);
+    assert_int_equal(data->peer.s_in.sin_port, htons(4433));
+}
+
+static void test_ctrl_set_get_peer(void **state)
+{
+    BIO *bio = *state;
+    BIO_ADDR got;
+
+    make_peer(bio, 4433);
+    memset(&got, 0, sizeof(got));
+    BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_PEER, sizeof(got), &got);
+    assert_int_equal(got.s_in.sin_family, AF_INET);
+    assert_int_equal(got.s_in.sin_port, htons(4433));
+}
+
+static void test_ctrl_set_connected(void **state)
+{
+    BIO *bio = *state;
+    bio_dgram_data *data = get_data(bio);
+    struct sockaddr_in sa;
+
+    memset(&sa, 0, sizeof(sa));
+    sa.sin_family = AF_INET;
+    sa.sin_port = htons(4433);
+    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+    BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &sa);
+    assert_int_equal(data->connected, 1);
+    assert_int_equal(BIO_ADDR_family(&data->peer), AF_INET);
+
+    BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, NULL);
+    assert_int_equal(data->connected, 0);
+    assert_int_equal(BIO_ADDR_family(&data->peer), AF_UNSPEC);
+}
+
+static void test_ctrl_detect_peer_addr_from_data(void **state)
+{
+    /* peer already known: returned without touching getpeername */
+    BIO *bio = *state;
+    BIO_ADDR got;
+
+    make_peer(bio, 4433);
+    memset(&got, 0, sizeof(got));
+    assert_true(
+        BIO_ctrl(bio, BIO_CTRL_DGRAM_DETECT_PEER_ADDR, sizeof(got), &got) > 0);
+    assert_int_equal(got.s_in.sin_family, AF_INET);
+}
+
+static void test_ctrl_detect_peer_addr_via_getpeername(void **state)
+{
+    /* peer unset: dgram_ctrl falls back to getpeername */
+    BIO *bio = *state;
+    BIO_ADDR got;
+
+    memset(&got, 0, sizeof(got));
+    expect_getpeername(FAKE_SOCKET, 0);
+    assert_true(
+        BIO_ctrl(bio, BIO_CTRL_DGRAM_DETECT_PEER_ADDR, sizeof(got), &got) > 0);
+    assert_int_equal(got.s_in.sin_family, AF_INET);
+}
+
+static void test_ctrl_set_get_mtu(void **state)
+{
+    BIO *bio = *state;
+
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_MTU, 1400, NULL), 1400);
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_MTU, 0, NULL), 1400);
+}
+
+static void test_ctrl_fallback_mtu_ipv4(void **state)
+{
+    /* AF_INET peer: 576 payload minus 28 bytes IP+UDP overhead */
+    BIO *bio = *state;
+
+    make_peer(bio, 4433);
+    assert_int_equal(
+        BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_FALLBACK_MTU, 0, NULL), 576 - 28);
+}
+
+#if OPENSSL_USE_IPV6
+static void test_ctrl_fallback_mtu_ipv6(void **state)
+{
+    /* AF_INET6 non-mapped peer: 1280 minus 48 bytes overhead */
+    BIO *bio = *state;
+
+    BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_PEER, 0, &g_sin6);
+    assert_int_equal(
+        BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_FALLBACK_MTU, 0, NULL), 1280 - 48);
+}
+
+#ifdef IN6_IS_ADDR_V4MAPPED
+static void test_ctrl_fallback_mtu_ipv6_v4mapped(void **state)
+{
+    /* v4-mapped AF_INET6 peer: treated as IPv4, 576 minus 28 overhead */
+    BIO *bio = *state;
+
+    BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_PEER, 0, &g_sin6_v4m);
+    assert_int_equal(
+        BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_FALLBACK_MTU, 0, NULL), 576 - 28);
+}
+#endif
+#endif
+
+#if defined(OPENSSL_SYS_LINUX) && defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO)
+static void test_ctrl_mtu_discover_getsockname_fails(void **state)
+{
+    /* getsockname fails before any setsockopt: ret 0 */
+    BIO *bio = *state;
+
+    expect_getsockname(FAKE_SOCKET, -1);
+    assert_int_equal(
+        BIO_ctrl(bio, BIO_CTRL_DGRAM_MTU_DISCOVER, 0, NULL), 0);
+}
+#endif
+
+static void test_ctrl_nbio(void **state)
+{
+    BIO *bio = *state;
+
+    expect_BIO_socket_nbio(FAKE_SOCKET, 1, 1);
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_NBIO, 1, NULL), 1);
+}
+
+static void test_ctrl_set_next_timeout(void **state)
+{
+    BIO *bio = *state;
+    struct timeval tv = { 1, 0 };
+
+    assert_int_equal(
+        BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT, 0, &tv), 1);
+}
+
+/* dgram_clear / dgram_free */
+
+static void test_free_closes_when_shutdown(void **state)
+{
+    BIO *bio = BIO_new(BIO_s_datagram());
+
+    (void)state;
+    assert_non_null(bio);
+    expect_getsockname(FAKE_SOCKET, 0);
+    expect_getpeername(FAKE_SOCKET, -1);
+    BIO_set_fd(bio, FAKE_SOCKET, BIO_CLOSE);
+
+    expect_BIO_closesocket(FAKE_SOCKET, 0);
+    BIO_free(bio);
+}
+
+static void test_free_no_close_when_noclose(void **state)
+{
+    BIO *bio = BIO_new(BIO_s_datagram());
+
+    (void)state;
+    assert_non_null(bio);
+    expect_getsockname(FAKE_SOCKET, 0);
+    expect_getpeername(FAKE_SOCKET, -1);
+    BIO_set_fd(bio, FAKE_SOCKET, BIO_NOCLOSE);
+    BIO_free(bio);
+}
+
+/* main */
+
+#define DG_TEST(name) \
+    cmocka_unit_test_setup_teardown(name, setup, teardown)
+
+#define DG_TEST_IO(name) \
+    cmocka_unit_test_setup_teardown(name, setup_io, teardown_io)
+
+int main(void)
+{
+    const struct CMUnitTest tests[] = {
+        /* BIO_new_dgram */
+        DG_TEST(test_new_dgram),
+        DG_TEST(test_dgram_new_defaults),
+        /* SET_FD / GET_FD */
+        DG_TEST(test_set_fd_unconnected),
+        DG_TEST(test_set_fd_connected),
+        DG_TEST(test_get_fd_uninit),
+        /* dgram_read */
+        DG_TEST_IO(test_dgram_read_noop),
+        DG_TEST_IO(test_dgram_read_success),
+        DG_TEST_IO(test_dgram_read_sets_peer),
+        DG_TEST_IO(test_dgram_read_peek),
+        DG_TEST_IO(test_dgram_read_retry),
+        DG_TEST_IO(test_dgram_read_error),
+        /* dgram_write */
+        DG_TEST_IO(test_dgram_write_unconnected),
+        DG_TEST_IO(test_dgram_write_connected),
+        DG_TEST_IO(test_dgram_write_retry),
+        DG_TEST_IO(test_dgram_write_error),
+        /* dgram_ctrl */
+        DG_TEST_IO(test_ctrl_reset),
+        DG_TEST_IO(test_ctrl_info),
+        DG_TEST_IO(test_ctrl_pending_wpending),
+        DG_TEST_IO(test_ctrl_dup_flush),
+        DG_TEST_IO(test_ctrl_get_set_close),
+        DG_TEST_IO(test_ctrl_connect),
+        DG_TEST_IO(test_ctrl_set_get_peer),
+        DG_TEST_IO(test_ctrl_set_connected),
+        DG_TEST_IO(test_ctrl_detect_peer_addr_from_data),
+        DG_TEST_IO(test_ctrl_detect_peer_addr_via_getpeername),
+        DG_TEST_IO(test_ctrl_set_get_mtu),
+        DG_TEST_IO(test_ctrl_fallback_mtu_ipv4),
+#if OPENSSL_USE_IPV6
+        DG_TEST_IO(test_ctrl_fallback_mtu_ipv6),
+#ifdef IN6_IS_ADDR_V4MAPPED
+        DG_TEST_IO(test_ctrl_fallback_mtu_ipv6_v4mapped),
+#endif
+#endif
+#if defined(OPENSSL_SYS_LINUX) && defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO)
+        DG_TEST_IO(test_ctrl_mtu_discover_getsockname_fails),
+#endif
+        DG_TEST_IO(test_ctrl_nbio),
+        DG_TEST_IO(test_ctrl_set_next_timeout),
+        /* dgram_clear / dgram_free */
+        DG_TEST(test_free_closes_when_shutdown),
+        DG_TEST(test_free_no_close_when_noclose),
+    };
+
+    cmocka_set_message_output(CM_OUTPUT_TAP);
+
+    return cmocka_run_group_tests(tests, group_setup, NULL);
+}
+
+#endif /* OPENSSL_NO_SOCK || OPENSSL_NO_DGRAM */