Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,19 @@ then
AM_CFLAGS="$AM_CFLAGS -DSHOW_SECRETS -DHAVE_SECRET_CALLBACK -DWOLFSSL_SSLKEYLOGFILE -DWOLFSSL_KEYLOG_EXPORT_WARNED"
fi

# TLS receive read-ahead: when enabled at runtime via wolfSSL_set_read_ahead(),
# the record-header read pulls in up to a full record in one recv(), avoiding a
# second syscall for the body. Opt-in (default: disabled).
AC_ARG_ENABLE([readahead],
[AS_HELP_STRING([--enable-readahead],[Enable TLS receive read-ahead support, activated at runtime with wolfSSL_set_read_ahead() (default: disabled)])],
[ ENABLED_READAHEAD=$enableval ],
[ ENABLED_READAHEAD=no ]
)
if test "$ENABLED_READAHEAD" = "yes"
then
AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_TLS_READ_AHEAD"
fi

# TLS v1.3 Draft 18 (Note: only final TLS v1.3 supported, here for backwards build compatibility)
AC_ARG_ENABLE([tls13-draft18],
[AS_HELP_STRING([--enable-tls13-draft18],[Enable wolfSSL TLS v1.3 Draft 18 (default: disabled)])],
Expand Down Expand Up @@ -13303,6 +13316,7 @@ echo " * Dual alg cert support: $ENABLED_DUAL_ALG_CERTS"
echo " * ERR Queues per Thread: $ENABLED_ERRORQUEUEPERTHREAD"
echo " * rwlock: $ENABLED_RWLOCK"
echo " * keylog export: $ENABLED_KEYLOG_EXPORT"
echo " * TLS receive read-ahead: $ENABLED_READAHEAD"
echo " * AutoSAR : $ENABLED_AUTOSAR"
echo " * ML-KEM standalone: $ENABLED_MLKEM_STANDALONE"
echo " * PQ/T hybrids: $ENABLED_PQC_HYBRIDS"
Expand Down
94 changes: 94 additions & 0 deletions doc/dox_comments/header_files/ssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -5486,6 +5486,21 @@ int wolfSSL_CTX_get_read_ahead(WOLFSSL_CTX* ctx);
\ingroup Setup

\brief This function sets the read ahead flag in the WOLFSSL_CTX structure.
When enabled, the record-header read pulls in up to a full TLS record in a
single recv(), so the body (and any following buffered records) is obtained
without a second syscall. The flag only changes I/O behaviour when the
library is built with read-ahead support (--enable-readahead /
WOLFSSL_TLS_READ_AHEAD); otherwise it is stored but inert.

\note With read-ahead enabled, undecrypted data can remain buffered
internally while the socket has no more data to read. wolfSSL_has_pending()
reports whether any such data is buffered, but a non-zero return does not
guarantee a full record is available: wolfSSL_read() may still return
WANT_READ when only a partial record is buffered. Event-driven applications
should therefore drain the connection by calling wolfSSL_read() until it
returns WANT_READ (or an error) before returning to select()/poll(), rather
than looping on wolfSSL_has_pending() alone; otherwise buffered data could
be missed or the loop could spin.

\return SSL_SUCCESS If ctx read ahead flag set.
\return SSL_FAILURE If ctx is NULL then SSL_FAILURE is returned.
Expand All @@ -5506,9 +5521,88 @@ int wolfSSL_CTX_get_read_ahead(WOLFSSL_CTX* ctx);
\sa wolfSSL_CTX_new
\sa wolfSSL_CTX_free
\sa wolfSSL_CTX_get_read_ahead
\sa wolfSSL_has_pending
*/
int wolfSSL_CTX_set_read_ahead(WOLFSSL_CTX* ctx, int v);

/*!
\ingroup Setup

\brief This function sets the read-ahead receive window size for contexts
created from this WOLFSSL_CTX. When read-ahead is enabled
(wolfSSL_CTX_set_read_ahead()), a single recv() pulls in up to \p len bytes:
- \p len of 0 resets the window to the one-record default. This is also
the window a freshly created context already carries, so the record body
is read together with its header without a second syscall.
- A \p len larger than one record lets a single recv() coalesce several
back-to-back records, one syscall instead of one per record.
- A \p len smaller than one record caps the receive buffer's footprint
when the peer's records are known to be small (e.g. 4 KB), saving heap
versus the one-record default.
\p len is only a speculative read window, not a hard limit: a record larger
than \p len is still received correctly, with the input buffer grown to the
record's actual size on demand (costing an extra syscall and reallocation
for that record) and then reallocated back down to the window once the
oversized record is consumed, so the retained footprint stays bounded by
\p len rather than by the largest record seen. The setting only affects I/O
when the library is built with read-ahead support (--enable-readahead /
WOLFSSL_TLS_READ_AHEAD); otherwise it is stored but inert. This is the
wolfSSL equivalent of OpenSSL's SSL_CTX_set_default_read_buffer_len(); unlike
OpenSSL it returns a status code (callers that ignore the return remain
source-compatible) and it honours sizes below one record, whereas OpenSSL
only ever enlarges the buffer. A \p len above WOLFSSL_MAX_READ_AHEAD_SZ
(16 MB) is clamped to that maximum. Because 0 and oversized values are both
normalised, wolfSSL_CTX_get_default_read_buffer_len() reports the effective
window (the one-record default rather than 0 when unset), not the raw
argument.

\note This is a memory-vs-syscall trade-off. While read-ahead is enabled the
input buffer is retained (bounded to \p len) for the connection's lifetime,
so a large \p len multiplied by many concurrent connections is persistent
memory, while a small \p len bounds per-connection footprint at the cost of
more syscalls for records that exceed it.

\return SSL_SUCCESS If the buffer length was set.
\return SSL_FAILURE If ctx is NULL.

\param ctx WOLFSSL_CTX structure to set the read-ahead buffer length on.
\param len read-ahead coalescing buffer size in bytes (0 = one record).

_Example_
\code
WOLFSSL_CTX* ctx;
// setup ctx
wolfSSL_CTX_set_read_ahead(ctx, 1);
// coalesce up to four max-size records per recv()
wolfSSL_CTX_set_default_read_buffer_len(ctx, 4 * 16384);
\endcode

\sa wolfSSL_CTX_set_read_ahead
\sa wolfSSL_set_default_read_buffer_len
\sa wolfSSL_has_pending
*/
int wolfSSL_CTX_set_default_read_buffer_len(WOLFSSL_CTX* ctx, size_t len);

/*!
\ingroup Setup

\brief This function sets the read-ahead coalescing buffer size on a single
WOLFSSL session, overriding the value inherited from its WOLFSSL_CTX. See
wolfSSL_CTX_set_default_read_buffer_len() for the full description, the
one-record default, and the memory-vs-syscall trade-off.

\return SSL_SUCCESS If the buffer length was set.
\return SSL_FAILURE If ssl is NULL.

\param ssl WOLFSSL structure to set the read-ahead buffer length on.
\param len read-ahead coalescing buffer size in bytes (0 = one record).

\sa wolfSSL_CTX_set_default_read_buffer_len
\sa wolfSSL_set_read_ahead
\sa wolfSSL_has_pending
*/
int wolfSSL_set_default_read_buffer_len(WOLFSSL* ssl, size_t len);

/*!
\ingroup Setup

Expand Down
14 changes: 14 additions & 0 deletions examples/benchmark/tls_bench.c
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,13 @@ static int bench_tls_client(info_t* info)
#endif
wolfSSL_SetIOReadCtx(cli_ssl, info);
wolfSSL_SetIOWriteCtx(cli_ssl, info);
#if defined(WOLFSSL_TLS_READ_AHEAD) && !defined(NO_FILESYSTEM) && \
!defined(NO_STDIO_FILESYSTEM)
/* Optional A/B benchmark toggle for TLS receive read-ahead. Gated on
* filesystem support since XGETENV is only defined there. */
if (XGETENV("WOLF_BENCH_READ_AHEAD") != NULL)
wolfSSL_set_read_ahead(cli_ssl, 1);
#endif

#if !defined(SINGLE_THREADED) && defined(WOLFSSL_DTLS)
/* synchronize with server */
Expand Down Expand Up @@ -1557,6 +1564,13 @@ static int bench_tls_server(info_t* info)

wolfSSL_SetIOReadCtx(srv_ssl, info);
wolfSSL_SetIOWriteCtx(srv_ssl, info);
#if defined(WOLFSSL_TLS_READ_AHEAD) && !defined(NO_FILESYSTEM) && \
!defined(NO_STDIO_FILESYSTEM)
/* Optional A/B benchmark toggle for TLS receive read-ahead. Gated on
* filesystem support since XGETENV is only defined there. */
if (XGETENV("WOLF_BENCH_READ_AHEAD") != NULL)
wolfSSL_set_read_ahead(srv_ssl, 1);
#endif
#ifndef NO_DH
wolfSSL_SetTmpDH(srv_ssl, dhp, sizeof(dhp), dhg, sizeof(dhg));
#endif
Expand Down
9 changes: 9 additions & 0 deletions examples/client/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,15 @@ static int ClientBenchmarkThroughput(WOLFSSL_CTX* ctx, char* host, word16 port,
if (ssl == NULL)
err_sys("unable to get SSL object");

#if defined(WOLFSSL_TLS_READ_AHEAD) && !defined(NO_FILESYSTEM) && \
!defined(NO_STDIO_FILESYSTEM)
/* Optional A/B toggle: enable TLS receive read-ahead for the throughput
* benchmark when WOLF_BENCH_READ_AHEAD is set in the environment. Gated on
* filesystem support since XGETENV is only defined there. */
if (XGETENV("WOLF_BENCH_READ_AHEAD") != NULL)
wolfSSL_set_read_ahead(ssl, 1);
#endif

tcp_connect(&sockfd, host, port, dtlsUDP, dtlsSCTP, ssl);
if (wolfSSL_set_fd(ssl, sockfd) != WOLFSSL_SUCCESS) {
err_sys("error in setting fd");
Expand Down
110 changes: 106 additions & 4 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -2650,6 +2650,15 @@ int InitSSL_Ctx(WOLFSSL_CTX* ctx, WOLFSSL_METHOD* method, void* heap)
}
ctx->timeout = WOLFSSL_SESSION_TIMEOUT;

#if defined(OPENSSL_EXTRA) || defined(WOLFSSL_TLS_READ_AHEAD)
/* Default the read-ahead window to one full record. Contexts (and the
* WOLFSSL objects that inherit it) then always carry a concrete window, so
* the receive path uses ssl->readAheadSz directly without a per-read
* fallback. A caller override replaces it; passing 0 resets it to this
* default (see wolfSSL_CTX_set_default_read_buffer_len()). */
ctx->readAheadSz = WOLFSSL_READ_AHEAD_SZ;
#endif

#ifdef WOLFSSL_DTLS
if (method->version.major == DTLS_MAJOR) {
ctx->minDowngrade = WOLFSSL_MIN_DTLS_DOWNGRADE;
Expand Down Expand Up @@ -7509,8 +7518,9 @@ int SetSSL_CTX(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup)
ssl->ConnectFilter_arg = ctx->ConnectFilter_arg;
#endif

#ifdef OPENSSL_EXTRA
#if defined(OPENSSL_EXTRA) || defined(WOLFSSL_TLS_READ_AHEAD)
ssl->readAhead = ctx->readAhead;
ssl->readAheadSz = ctx->readAheadSz;
#endif
#if defined(OPENSSL_EXTRA) && !defined(NO_BIO)
/* Don't change recv callback if currently using BIO's */
Expand Down Expand Up @@ -11619,6 +11629,41 @@ void ShrinkInputBuffer(WOLFSSL* ssl, int forcedFree)
ssl->buffers.clearOutputBuffer.length > 0))
return;

#ifdef WOLFSSL_TLS_READ_AHEAD
/* While read-ahead is enabled, retain a dynamic input buffer sized to the
* configured window rather than shrinking all the way back to the static
* buffer, so the speculative over-read is a bounded, mostly one-time
* allocation instead of per-record churn. A forced free (connection
* teardown) still reclaims everything. */
if (!forcedFree && ssl->readAhead) {
/* Already within the window: keep the buffer as-is. */
if (ssl->buffers.inputBuffer.bufferSize <= ssl->readAheadSz)
return;

/* The buffer grew past the window to receive an oversized record. When
* the window needs a dynamic buffer, reallocate down to it so the
* retained footprint tracks the window, not the largest record seen;
* when the window fits in the static buffer, fall through and shrink to
* static below.
*
* GrowInputBuffer(newBytes, usedLength) resizes the input buffer to
* newBytes + usedLength while preserving the usedLength bytes still
* pending, so requesting (readAheadSz - usedLength) new bytes yields a
* buffer of exactly readAheadSz. usedLength is <= STATIC_BUFFER_LEN <
* readAheadSz here (guaranteed above), so the subtraction stays
* positive. */
if (ssl->readAheadSz > STATIC_BUFFER_LEN) {
if (GrowInputBuffer(ssl, (int)ssl->readAheadSz - usedLength,
usedLength) != 0) {
/* Realloc failed: keep the current (larger) buffer rather than
* dropping the buffered data. */
WOLFSSL_MSG("read-ahead buffer shrink failed, keeping buffer");
}
return;
}
}
#endif

WOLFSSL_MSG("Shrinking input buffer");

if (!forcedFree && usedLength > 0) {
Expand Down Expand Up @@ -23189,12 +23234,16 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type)
return level;
}

static int GetInputData(WOLFSSL *ssl, word32 size)
static int GetInputData_ex(WOLFSSL *ssl, word32 size, word32 readAhead)
{
int inSz;
int maxLength;
int usedLength;
int dtlsExtra = 0;
int extra = 0;
#ifndef WOLFSSL_TLS_READ_AHEAD
(void)readAhead;
#endif

if (ssl->options.disableRead)
return WC_NO_ERR_TRACE(WANT_READ);
Expand Down Expand Up @@ -23231,10 +23280,27 @@ static int GetInputData(WOLFSSL *ssl, word32 size)
}

inSz = (int)(size - (word32)usedLength); /* from last partial read */

#ifdef WOLFSSL_TLS_READ_AHEAD
/* Request more than the minimum so that a single recv() can also pull
* in the record body (and possibly following records), avoiding a
* second syscall. 'size' remains the loop-termination minimum, so a
* blocking socket never waits for read-ahead bytes the peer may not
* send. Mirrors the DTLS dtlsExtra over-read above.
*
* The buffer is grown once to hold a full record and then kept (see the
* ssl->readAhead guard in ShrinkInputBuffer), so this is a one-time
* allocation per connection, not per-record churn. */
if (readAhead > size) {
extra = (int)(readAhead - size);
inSz += extra;
}
#endif
}

if (inSz > maxLength) {
if (GrowInputBuffer(ssl, (int)(size + (word32)dtlsExtra), usedLength) < 0)
if (GrowInputBuffer(ssl,
(int)(size + (word32)dtlsExtra + (word32)extra), usedLength) < 0)
return MEMORY_E;
}

Expand Down Expand Up @@ -23292,6 +23358,11 @@ static int GetInputData(WOLFSSL *ssl, word32 size)
return 0;
}

static int GetInputData(WOLFSSL *ssl, word32 size)
{
return GetInputData_ex(ssl, size, 0);
}

#if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY)
static WC_INLINE int VerifyMacEnc(WOLFSSL* ssl, const byte* input, word32 msgSz,
int content)
Expand Down Expand Up @@ -24123,7 +24194,28 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr)

/* get header or return error */
if (!ssl->options.dtls) {
if ((ret = GetInputData(ssl, (word32)readSz)) < 0)
word32 readAheadSz = 0;
#ifdef WOLFSSL_TLS_READ_AHEAD
/* When read-ahead is enabled, request more than the record
* header in a single recv() so the body (and possibly following
* records) can be pulled in without a second syscall. The window
* size is configurable via
* wolfSSL_CTX_set_default_read_buffer_len():
* - 0 (unset) requests one full record, the sensible default.
* - A larger value lets one recv() coalesce several back-to-back
* records.
* - A smaller (sub-record) value caps the receive buffer's
* footprint when the peer's records are known to be small.
* The value is only a speculative read window: a record larger
* than it is still received correctly, as the buffer is grown to
* the record's actual size on demand. */
if (ssl->readAhead) {
/* readAheadSz is always concrete (defaulted to
* WOLFSSL_READ_AHEAD_SZ at CTX init, never 0). */
readAheadSz = ssl->readAheadSz;
}
#endif
if ((ret = GetInputData_ex(ssl, (word32)readSz, readAheadSz)) < 0)
return ret;
} else {
#ifdef WOLFSSL_DTLS
Expand Down Expand Up @@ -24848,6 +24940,16 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr)
ssl->options.serverState ==
SERVER_FINISHED_COMPLETE &&
ssl->options.handShakeState != HANDSHAKE_DONE)))
#endif
#ifdef WOLFSSL_TLS_READ_AHEAD
/* With read-ahead, more than one record may be buffered. If
* application data was just decrypted, return it now so it
* is delivered to the caller before any following buffered
* record (e.g. a close_notify alert) is processed, which
* would otherwise discard the pending app data. The
* remaining records stay buffered for the next call. */
|| (ssl->curRL.type == application_data &&
ssl->buffers.clearOutputBuffer.length > 0)
#endif
) {
/* Shrink input buffer when we successfully finish record
Expand Down
Loading
Loading