blob: 48918ffb1987f429cdd79b9b727e6806378ea422 [file] [edit]
/*
* 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
*/
#include <string.h>
#include <openssl/ssl.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include "internal/sockets.h"
#include "internal/thread_arch.h"
#include "helpers/ssltestlib.h"
#include "testutil.h"
static char *cert = NULL;
static char *privkey = NULL;
#define NUM_CLIENTS 3
#define CLIENT_TO_SERVER_MSG "Hello from client"
#define SERVER_TO_CLIENT_MSG "Hello from server"
#define POLL_TIMEOUT_SEC 5
#define MAX_POLL_RETRIES 50
/*
* Per-thread state for server connection handlers
*/
struct server_thread_args {
SSL *conn; /* Server-side connection for this thread */
int thread_idx;
CRYPTO_THREAD *thread;
int result; /* 1 = success, 0 = failure */
};
/*
* Per-thread state for client connection handlers
*/
struct client_thread_args {
SSL *conn; /* Client-side connection for this thread */
int thread_idx;
CRYPTO_THREAD *thread;
int result; /* 1 = success, 0 = failure */
};
/*
* Thread function: waits for data on a server connection using SSL_poll,
* reads the message from client, and sends a response.
*
* Uses a retry loop because SSL_poll() returning SSL_POLL_EVENT_R doesn't
* guarantee SSL_read_ex() will succeed - there may be encrypted records
* but not yet a complete application data message.
*/
static unsigned int server_conn_thread(void *arg)
{
struct server_thread_args *ta = (struct server_thread_args *)arg;
SSL_POLL_ITEM item;
struct timeval timeout;
size_t result_count, readbytes, written;
char buf[256] = { 0 };
int ret, err, retries;
ta->result = 0;
readbytes = 0;
item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
item.desc.value.ssl = ta->conn;
item.events = SSL_POLL_EVENT_R;
item.revents = 0;
timeout.tv_sec = POLL_TIMEOUT_SEC;
timeout.tv_usec = 0;
/* Poll and receive message from client */
for (retries = 0; retries < MAX_POLL_RETRIES; retries++) {
item.revents = 0;
if (!TEST_true(SSL_poll(&item, 1, sizeof(item), &timeout, 0, &result_count)))
return 0;
if (result_count == 0 || (item.revents & SSL_POLL_EVENT_R) == 0)
continue;
ret = SSL_read_ex(ta->conn, buf, sizeof(buf) - 1, &readbytes);
if (ret == 1)
break;
err = SSL_get_error(ta->conn, ret);
if (!TEST_int_eq(err, SSL_ERROR_WANT_READ))
return 0;
}
if (!TEST_int_lt(retries, MAX_POLL_RETRIES))
return 0;
buf[readbytes] = '\0';
if (!TEST_str_eq(buf, CLIENT_TO_SERVER_MSG))
return 0;
if (!TEST_true(SSL_write_ex(ta->conn, SERVER_TO_CLIENT_MSG,
strlen(SERVER_TO_CLIENT_MSG), &written)))
return 0;
ta->result = 1;
return 0;
}
/*
* Thread function: sends a message to the server and waits for the response.
*
* Uses SSL_poll to wait for readable data from server, then verifies the response.
*/
static unsigned int client_conn_thread(void *arg)
{
struct client_thread_args *ta = (struct client_thread_args *)arg;
SSL_POLL_ITEM item;
struct timeval timeout;
size_t result_count, written;
size_t readbytes = 0;
char buf[256] = { 0 };
int ret, err, retries;
ta->result = 0;
/* Send message to server */
if (!TEST_true(SSL_write_ex(ta->conn, CLIENT_TO_SERVER_MSG,
strlen(CLIENT_TO_SERVER_MSG), &written)))
return 0;
/* Wait for and receive response from server */
item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
item.desc.value.ssl = ta->conn;
item.events = SSL_POLL_EVENT_R;
item.revents = 0;
timeout.tv_sec = POLL_TIMEOUT_SEC;
timeout.tv_usec = 0;
for (retries = 0; retries < MAX_POLL_RETRIES; retries++) {
item.revents = 0;
if (!TEST_true(SSL_poll(&item, 1, sizeof(item), &timeout, 0, &result_count)))
return 0;
if (result_count == 0 || (item.revents & SSL_POLL_EVENT_R) == 0)
continue;
ret = SSL_read_ex(ta->conn, buf, sizeof(buf) - 1, &readbytes);
if (ret == 1)
break;
err = SSL_get_error(ta->conn, ret);
if (!TEST_int_eq(err, SSL_ERROR_WANT_READ))
return 0;
}
if (!TEST_int_lt(retries, MAX_POLL_RETRIES))
return 0;
buf[readbytes] = '\0';
if (!TEST_str_eq(buf, SERVER_TO_CLIENT_MSG))
return 0;
ta->result = 1;
return 0;
}
/*
* Helper: create DTLS listener with real UDP socket
*/
static int create_listener(SSL_CTX *ctx, SSL **listener, BIO_ADDR **addr, int *fd)
{
BIO *bio = NULL;
struct in_addr ina;
union BIO_sock_info_u info;
int ret = 0;
*listener = NULL;
*addr = NULL;
*fd = -1;
ina.s_addr = htonl(INADDR_LOOPBACK);
*fd = BIO_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, 0);
if (!TEST_int_ge(*fd, 0))
goto err;
if (!TEST_true(BIO_socket_nbio(*fd, 1)))
goto err;
if (!TEST_ptr(*addr = BIO_ADDR_new()))
goto err;
if (!TEST_true(BIO_ADDR_rawmake(*addr, AF_INET, &ina, sizeof(ina), 0)))
goto err;
if (!TEST_true(BIO_bind(*fd, *addr, 0)))
goto err;
info.addr = *addr;
if (!TEST_true(BIO_sock_info(*fd, BIO_SOCK_INFO_ADDRESS, &info)))
goto err;
if (!TEST_ptr(bio = BIO_new_dgram(*fd, BIO_NOCLOSE)))
goto err;
if (!TEST_ptr(*listener = SSL_new_listener(ctx, 0)))
goto err;
SSL_set_bio(*listener, bio, bio);
bio = NULL;
if (!TEST_int_eq(SSL_listen(*listener), 1))
goto err;
ret = 1;
err:
BIO_free(bio);
if (ret == 0) {
SSL_free(*listener);
BIO_ADDR_free(*addr);
if (*fd >= 0)
BIO_closesocket(*fd);
*listener = NULL;
*addr = NULL;
*fd = -1;
}
return ret;
}
/*
* Helper: create DTLS client connected to server address
*/
static int create_client(SSL_CTX *ctx, const BIO_ADDR *server_addr,
SSL **client, int *fd)
{
BIO *bio = NULL;
int ret = 0;
*client = NULL;
*fd = -1;
*fd = BIO_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, 0);
if (!TEST_int_ge(*fd, 0))
goto err;
if (!TEST_true(BIO_socket_nbio(*fd, 1)))
goto err;
if (!TEST_ptr(bio = BIO_new_dgram(*fd, BIO_NOCLOSE)))
goto err;
if (!TEST_true(BIO_dgram_set_peer(bio, server_addr)))
goto err;
if (!TEST_ptr(*client = SSL_new(ctx)))
goto err;
SSL_set_bio(*client, bio, bio);
bio = NULL;
ret = 1;
err:
BIO_free(bio);
if (ret == 0) {
SSL_free(*client);
if (*fd >= 0)
BIO_closesocket(*fd);
*client = NULL;
*fd = -1;
}
return ret;
}
/*
* Helper: drive handshake between client and listener, return accepted connection.
*/
static int do_handshake(SSL *client, SSL *listener, SSL **server_conn)
{
SSL_POLL_ITEM poll_item;
struct timeval poll_timeout;
size_t poll_result;
int retc, err_code;
int abortctr = 0;
*server_conn = NULL;
SSL_set_connect_state(client);
while (*server_conn == NULL) {
if (!TEST_int_le(++abortctr, 100))
return 0;
retc = SSL_connect(client);
err_code = SSL_get_error(client, retc);
if (retc <= 0
&& err_code != SSL_ERROR_WANT_READ
&& err_code != SSL_ERROR_WANT_WRITE)
return 0;
poll_item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
poll_item.desc.value.ssl = listener;
poll_item.events = SSL_POLL_EVENT_IC;
poll_item.revents = 0;
poll_timeout.tv_sec = 0;
poll_timeout.tv_usec = 100000;
if (!TEST_true(SSL_poll(&poll_item, 1, sizeof(poll_item),
&poll_timeout, 0, &poll_result)))
return 0;
if (poll_result > 0 && (poll_item.revents & SSL_POLL_EVENT_IC) != 0)
*server_conn = SSL_accept_connection(listener, SSL_ACCEPT_CONNECTION_NO_BLOCK);
}
if (!TEST_ptr(*server_conn))
return 0;
if (!TEST_true(create_ssl_connection(*server_conn, client, SSL_ERROR_NONE)))
return 0;
return 1;
}
/*
* Main test: multiple threads polling on different DTLS connections
*/
static int test_dtls_multithread(void)
{
SSL_CTX *sctx = NULL, *cctx = NULL;
SSL *listener = NULL;
SSL *clients[NUM_CLIENTS] = { NULL };
SSL *server_conns[NUM_CLIENTS] = { NULL };
int client_fds[NUM_CLIENTS] = { -1, -1, -1 };
struct server_thread_args server_args[NUM_CLIENTS];
struct client_thread_args client_args[NUM_CLIENTS];
BIO_ADDR *server_addr = NULL;
int server_fd = -1;
int testresult = 0;
int i;
memset(server_args, 0, sizeof(server_args));
memset(client_args, 0, sizeof(client_args));
if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
DTLS_client_method(),
0, 0,
&sctx, &cctx, cert, privkey)))
goto err;
if (!TEST_true(create_listener(sctx, &listener, &server_addr, &server_fd)))
goto err;
/*
* do_handshake() below drives both ends from this thread, so the server
* side must not block: a blocking read would wait for a client which only
* this thread can advance. Connections accepted from the listener inherit
* this. The worker threads which follow use SSL_poll() and expect
* SSL_ERROR_WANT_READ, so they need it too.
*/
if (!TEST_true(SSL_set_blocking_mode(listener, 0)))
goto err;
for (i = 0; i < NUM_CLIENTS; i++) {
if (!TEST_true(create_client(cctx, server_addr, &clients[i], &client_fds[i])))
goto err;
if (!TEST_true(do_handshake(clients[i], listener, &server_conns[i])))
goto err;
}
/* Start server threads - each will poll and wait for data */
for (i = 0; i < NUM_CLIENTS; i++) {
server_args[i].conn = server_conns[i];
server_args[i].thread_idx = i;
server_args[i].result = 0;
server_args[i].thread = ossl_crypto_thread_native_start(
server_conn_thread, &server_args[i], 1);
if (!TEST_ptr(server_args[i].thread))
goto err;
}
/* Start client threads - each will send data and wait for response */
for (i = 0; i < NUM_CLIENTS; i++) {
client_args[i].conn = clients[i];
client_args[i].thread_idx = i;
client_args[i].result = 0;
client_args[i].thread = ossl_crypto_thread_native_start(
client_conn_thread, &client_args[i], 1);
if (!TEST_ptr(client_args[i].thread))
goto err;
}
/* Wait for all server threads to complete */
for (i = 0; i < NUM_CLIENTS; i++) {
ossl_crypto_thread_native_join(server_args[i].thread, NULL);
ossl_crypto_thread_native_clean(server_args[i].thread);
server_args[i].thread = NULL;
if (!TEST_int_eq(server_args[i].result, 1))
goto err;
}
/* Wait for all client threads to complete */
for (i = 0; i < NUM_CLIENTS; i++) {
ossl_crypto_thread_native_join(client_args[i].thread, NULL);
ossl_crypto_thread_native_clean(client_args[i].thread);
client_args[i].thread = NULL;
if (!TEST_int_eq(client_args[i].result, 1))
goto err;
}
testresult = 1;
err:
/* Clean up any remaining server threads */
for (i = 0; i < NUM_CLIENTS; i++) {
if (server_args[i].thread != NULL) {
ossl_crypto_thread_native_join(server_args[i].thread, NULL);
ossl_crypto_thread_native_clean(server_args[i].thread);
}
}
/* Clean up any remaining client threads */
for (i = 0; i < NUM_CLIENTS; i++) {
if (client_args[i].thread != NULL) {
ossl_crypto_thread_native_join(client_args[i].thread, NULL);
ossl_crypto_thread_native_clean(client_args[i].thread);
}
}
for (i = 0; i < NUM_CLIENTS; i++) {
SSL_free(clients[i]);
SSL_free(server_conns[i]);
if (client_fds[i] >= 0)
BIO_closesocket(client_fds[i]);
}
SSL_free(listener);
BIO_ADDR_free(server_addr);
if (server_fd >= 0)
BIO_closesocket(server_fd);
SSL_CTX_free(sctx);
SSL_CTX_free(cctx);
return testresult;
}
/*
* Per-thread state for the blocking accept
*/
struct accept_thread_args {
SSL *listener;
SSL *conn; /* connection the accept returned */
CRYPTO_THREAD *thread;
int result; /* 1 = success, 0 = failure */
};
/*
* Thread function: block in SSL_accept_connection() until a connection turns
* up. This is the accept path which ticks the listener itself, so no other
* thread needs to drive it.
*/
static unsigned int blocking_accept_thread(void *arg)
{
struct accept_thread_args *ta = (struct accept_thread_args *)arg;
ta->conn = SSL_accept_connection(ta->listener, 0);
ta->result = (ta->conn != NULL);
return 1;
}
/*
* Test that a blocking SSL_accept_connection() waits for a connection and
* returns it.
*
* The listener demultiplexes one socket to many connections, so it cannot
* block inside a read: doing so would stall every other connection, and the
* demux lock is held across it. Blocking accept therefore has to wait for
* readiness rather than for a datagram, which is what this exercises - a
* client is only created once the accepting thread is already in the call.
*
* Note that this cannot distinguish waiting from spinning: the accept returns
* the connection either way, and the difference is CPU consumed rather than
* anything observable through the API. It is a test that the blocking path
* works at all, which was previously only covered for the failure case of
* having no BIO set.
*/
static int test_dtls_blocking_accept(void)
{
SSL_CTX *sctx = NULL, *cctx = NULL;
SSL *listener = NULL, *client = NULL;
struct accept_thread_args accept_args;
BIO_ADDR *server_addr = NULL;
int server_fd = -1, client_fd = -1;
int testresult = 0;
int i, ret, err;
memset(&accept_args, 0, sizeof(accept_args));
if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
DTLS_client_method(), 0, 0, &sctx, &cctx, cert, privkey)))
goto err;
if (!TEST_true(create_listener(sctx, &listener, &server_addr, &server_fd)))
goto err;
/*
* This test needs the blocking accept, so ask for it rather than relying
* on the default.
*/
if (!TEST_true(SSL_set_blocking_mode(listener, 1)))
goto err;
/*
* Create the client's socket first, but do not connect with it yet. There
* is no way to cancel a blocking SSL_accept_connection(), so a thread
* parked in one is released only by a connection arriving - which means
* nothing between starting the thread and the exchange completing may bail
* out, or the join below would wait for ever. Anything which can fail is
* therefore done up front. The accept still has to wait, since no datagram
* is sent until SSL_connect() below.
*/
if (!TEST_true(create_client(cctx, server_addr, &client, &client_fd)))
goto err;
accept_args.listener = listener;
accept_args.thread = ossl_crypto_thread_native_start(blocking_accept_thread,
&accept_args, 1);
if (!TEST_ptr(accept_args.thread))
goto err;
/*
* Drive the client's side of the cookie exchange. The accepting thread
* ticks the listener, so this only has to keep the client moving.
*/
SSL_set_connect_state(client);
for (i = 0; i < 200 && accept_args.result == 0; i++) {
ret = SSL_connect(client);
err = SSL_get_error(client, ret);
if (ret <= 0 && err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
TEST_error("SSL_connect failed (err %d)", err);
goto err;
}
OSSL_sleep(10);
}
ossl_crypto_thread_native_join(accept_args.thread, NULL);
ossl_crypto_thread_native_clean(accept_args.thread);
accept_args.thread = NULL;
if (!TEST_int_eq(accept_args.result, 1) || !TEST_ptr(accept_args.conn))
goto err;
testresult = 1;
err:
if (accept_args.thread != NULL) {
ossl_crypto_thread_native_join(accept_args.thread, NULL);
ossl_crypto_thread_native_clean(accept_args.thread);
}
SSL_free(accept_args.conn);
SSL_free(client);
SSL_free(listener);
BIO_ADDR_free(server_addr);
if (server_fd >= 0)
BIO_closesocket(server_fd);
if (client_fd >= 0)
BIO_closesocket(client_fd);
SSL_CTX_free(sctx);
SSL_CTX_free(cctx);
return testresult;
}
/*
* How long the client waits, in milliseconds, after its handshake completes
* before sending anything. The server thread has to still be inside its
* blocking read when the write finally happens, or the test proves nothing.
*
* A machine slow enough to get there late does not make the test fail: the read
* then finds the datagram already queued and returns it, which is a pass with
* nothing demonstrated. Only the absence of blocking turns it into a failure.
*/
#define BLOCKING_READ_QUIET_MS 250
/*
* Per-thread state for the blocking read
*/
struct read_thread_args {
SSL *listener;
SSL *conn; /* connection the accept returned */
CRYPTO_THREAD *thread;
char buf[256];
size_t readbytes;
int nonblocking_handshake; /* handshake without blocking mode */
int result; /* 1 = success, 0 = failure */
};
/*
* Helper: complete the handshake with the connection in non-blocking mode,
* driving the listener by hand, so that the blocking read which follows is the
* only thing relying on the emulation.
*/
static int nonblocking_handshake(struct read_thread_args *ta)
{
int i, ret = -1, err;
if (!TEST_true(SSL_set_blocking_mode(ta->conn, 0)))
return 0;
for (i = 0; i < 200; i++) {
ret = SSL_accept(ta->conn);
if (ret == 1)
break;
err = SSL_get_error(ta->conn, ret);
if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
TEST_error("SSL_accept failed (err %d)", err);
return 0;
}
/* Nothing else will service the listener while we are in here. */
if (!TEST_true(SSL_handle_events(ta->listener)))
return 0;
OSSL_sleep(10);
}
if (!TEST_int_eq(ret, 1))
return 0;
return TEST_true(SSL_set_blocking_mode(ta->conn, 1));
}
/*
* Thread function: accept a connection, complete its handshake and read from
* it, so that the thread never has to handle WANT_READ.
*/
static unsigned int blocking_read_thread(void *arg)
{
struct read_thread_args *ta = (struct read_thread_args *)arg;
ta->conn = SSL_accept_connection(ta->listener, 0);
if (!TEST_ptr(ta->conn))
return 0;
/*
* A listener-created connection has no BIO of its own to block in - it
* reads from a queue the listener demultiplexes into - so without the
* emulation these calls return immediately with WANT_READ instead of
* waiting.
*/
if (ta->nonblocking_handshake) {
if (!nonblocking_handshake(ta))
return 0;
} else if (!TEST_int_gt(SSL_accept(ta->conn), 0)) {
return 0;
}
if (!TEST_true(SSL_read_ex(ta->conn, ta->buf, sizeof(ta->buf) - 1,
&ta->readbytes)))
return 0;
ta->result = 1;
return 0;
}
/*
* Test that a blocking listener connection waits for a datagram rather than
* reporting WANT_READ.
*
* The client stays silent for BLOCKING_READ_QUIET_MS after its own handshake
* completes, so by the time it writes, the server thread is already parked in
* SSL_read_ex() with nothing to return. A non-blocking connection reports
* WANT_READ immediately, so the absence of the emulation shows up as a failed
* assertion rather than as a hang.
*
* idx 0 blocks in the handshake as well as in the read. idx 1 handshakes in
* non-blocking mode and only then switches the connection to blocking, which
* leaves the read as the sole assertion the emulation has to satisfy - without
* it, idx 0 fails at SSL_accept() and never reaches the read, so on its own it
* would not tell us the read path works.
*
* Only the client is driven from this thread: the accepting thread ticks the
* listener itself, which is what lets a blocked connection make progress at
* all.
*/
static int test_dtls_blocking_read(int idx)
{
SSL_CTX *sctx = NULL, *cctx = NULL;
SSL *listener = NULL, *client = NULL;
struct read_thread_args read_args;
BIO_ADDR *server_addr = NULL;
int server_fd = -1, client_fd = -1;
int testresult = 0;
size_t written;
int i, ret = -1, err;
memset(&read_args, 0, sizeof(read_args));
if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
DTLS_client_method(), 0, 0, &sctx, &cctx, cert, privkey)))
goto err;
if (!TEST_true(create_listener(sctx, &listener, &server_addr, &server_fd)))
goto err;
/* Blocking is the default, but this test depends on it, so be explicit. */
if (!TEST_true(SSL_set_blocking_mode(listener, 1)))
goto err;
/*
* As in test_dtls_blocking_accept, create the client's socket before the
* thread which will park in the blocking accept, because nothing can
* release that thread except a connection arriving. Nothing is sent until
* SSL_connect() below, so the accept still waits.
*/
if (!TEST_true(create_client(cctx, server_addr, &client, &client_fd)))
goto err;
read_args.listener = listener;
read_args.nonblocking_handshake = idx;
read_args.thread = ossl_crypto_thread_native_start(blocking_read_thread,
&read_args, 1);
if (!TEST_ptr(read_args.thread))
goto err;
/* Drive the client's handshake to completion. */
SSL_set_connect_state(client);
for (i = 0; i < 200; i++) {
ret = SSL_connect(client);
if (ret == 1)
break;
err = SSL_get_error(client, ret);
if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
TEST_error("SSL_connect failed (err %d)", err);
goto err;
}
OSSL_sleep(10);
}
if (!TEST_int_eq(ret, 1))
goto err;
/* Let the server thread reach its read and find nothing there. */
OSSL_sleep(BLOCKING_READ_QUIET_MS);
if (!TEST_true(SSL_write_ex(client, CLIENT_TO_SERVER_MSG,
strlen(CLIENT_TO_SERVER_MSG), &written)))
goto err;
ossl_crypto_thread_native_join(read_args.thread, NULL);
ossl_crypto_thread_native_clean(read_args.thread);
read_args.thread = NULL;
if (!TEST_int_eq(read_args.result, 1))
goto err;
read_args.buf[read_args.readbytes] = '\0';
if (!TEST_str_eq(read_args.buf, CLIENT_TO_SERVER_MSG))
goto err;
testresult = 1;
err:
if (read_args.thread != NULL) {
ossl_crypto_thread_native_join(read_args.thread, NULL);
ossl_crypto_thread_native_clean(read_args.thread);
}
SSL_free(read_args.conn);
SSL_free(client);
SSL_free(listener);
BIO_ADDR_free(server_addr);
if (server_fd >= 0)
BIO_closesocket(server_fd);
if (client_fd >= 0)
BIO_closesocket(client_fd);
SSL_CTX_free(sctx);
SSL_CTX_free(cctx);
return testresult;
}
OPT_TEST_DECLARE_USAGE("certfile privkeyfile\n")
int setup_tests(void)
{
if (!test_skip_common_options()) {
TEST_error("Error parsing test options\n");
return 0;
}
if (!TEST_ptr(cert = test_get_argument(0))
|| !TEST_ptr(privkey = test_get_argument(1)))
return 0;
ADD_TEST(test_dtls_multithread);
ADD_TEST(test_dtls_blocking_accept);
ADD_ALL_TESTS(test_dtls_blocking_read, 2);
return 1;
}