Skip to content

Commit 6131636

Browse files
committed
Add TLS receive read-ahead support
Add WOLFSSL_TLS_READ_AHEAD (--enable-readahead), toggled at runtime via wolfSSL_set_read_ahead(). When enabled, the record-header read pulls a full record in one recv() so the body arrives without a second syscall. The receive window is configurable with wolfSSL_CTX/SSL_set_default_read_buffer_len() (OpenSSL-compatible): 0 keeps the one-record default, a larger value coalesces several records per recv(), a smaller value caps the per-connection buffer footprint. Records exceeding the window are still received correctly, the buffer grows on demand and is reallocated back down to the window afterwards so the retained footprint stays bounded. Includes docs, API tests, and a benchmark toggle.
1 parent c2b5fb7 commit 6131636

10 files changed

Lines changed: 736 additions & 11 deletions

File tree

configure.ac

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,6 +2397,19 @@ then
23972397
AM_CFLAGS="$AM_CFLAGS -DSHOW_SECRETS -DHAVE_SECRET_CALLBACK -DWOLFSSL_SSLKEYLOGFILE -DWOLFSSL_KEYLOG_EXPORT_WARNED"
23982398
fi
23992399

2400+
# TLS receive read-ahead: when enabled at runtime via wolfSSL_set_read_ahead(),
2401+
# the record-header read pulls in up to a full record in one recv(), avoiding a
2402+
# second syscall for the body. Opt-in (default: disabled).
2403+
AC_ARG_ENABLE([readahead],
2404+
[AS_HELP_STRING([--enable-readahead],[Enable TLS receive read-ahead support, activated at runtime with wolfSSL_set_read_ahead() (default: disabled)])],
2405+
[ ENABLED_READAHEAD=$enableval ],
2406+
[ ENABLED_READAHEAD=no ]
2407+
)
2408+
if test "$ENABLED_READAHEAD" = "yes"
2409+
then
2410+
AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_TLS_READ_AHEAD"
2411+
fi
2412+
24002413
# TLS v1.3 Draft 18 (Note: only final TLS v1.3 supported, here for backwards build compatibility)
24012414
AC_ARG_ENABLE([tls13-draft18],
24022415
[AS_HELP_STRING([--enable-tls13-draft18],[Enable wolfSSL TLS v1.3 Draft 18 (default: disabled)])],
@@ -13588,6 +13601,7 @@ echo " * FrodoKEM decapsulate: $ENABLED_FRODOKEM_DECAPSULATE"
1358813601
echo " * ERR Queues per Thread: $ENABLED_ERRORQUEUEPERTHREAD"
1358913602
echo " * rwlock: $ENABLED_RWLOCK"
1359013603
echo " * keylog export: $ENABLED_KEYLOG_EXPORT"
13604+
echo " * TLS receive read-ahead: $ENABLED_READAHEAD"
1359113605
echo " * AutoSAR : $ENABLED_AUTOSAR"
1359213606
echo " * ML-KEM standalone: $ENABLED_MLKEM_STANDALONE"
1359313607
echo " * PQ/T hybrids: $ENABLED_PQC_HYBRIDS"

doc/dox_comments/header_files/ssl.h

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5486,6 +5486,21 @@ int wolfSSL_CTX_get_read_ahead(WOLFSSL_CTX* ctx);
54865486
\ingroup Setup
54875487

54885488
\brief This function sets the read ahead flag in the WOLFSSL_CTX structure.
5489+
When enabled, the record-header read pulls in up to a full TLS record in a
5490+
single recv(), so the body (and any following buffered records) is obtained
5491+
without a second syscall. The flag only changes I/O behaviour when the
5492+
library is built with read-ahead support (--enable-readahead /
5493+
WOLFSSL_TLS_READ_AHEAD); otherwise it is stored but inert.
5494+
5495+
\note With read-ahead enabled, undecrypted data can remain buffered
5496+
internally while the socket has no more data to read. wolfSSL_has_pending()
5497+
reports whether any such data is buffered, but a non-zero return does not
5498+
guarantee a full record is available: wolfSSL_read() may still return
5499+
WANT_READ when only a partial record is buffered. Event-driven applications
5500+
should therefore drain the connection by calling wolfSSL_read() until it
5501+
returns WANT_READ (or an error) before returning to select()/poll(), rather
5502+
than looping on wolfSSL_has_pending() alone; otherwise buffered data could
5503+
be missed or the loop could spin.
54895504

54905505
\return SSL_SUCCESS If ctx read ahead flag set.
54915506
\return SSL_FAILURE If ctx is NULL then SSL_FAILURE is returned.
@@ -5506,9 +5521,88 @@ int wolfSSL_CTX_get_read_ahead(WOLFSSL_CTX* ctx);
55065521
\sa wolfSSL_CTX_new
55075522
\sa wolfSSL_CTX_free
55085523
\sa wolfSSL_CTX_get_read_ahead
5524+
\sa wolfSSL_has_pending
55095525
*/
55105526
int wolfSSL_CTX_set_read_ahead(WOLFSSL_CTX* ctx, int v);
55115527

5528+
/*!
5529+
\ingroup Setup
5530+
5531+
\brief This function sets the read-ahead receive window size for contexts
5532+
created from this WOLFSSL_CTX. When read-ahead is enabled
5533+
(wolfSSL_CTX_set_read_ahead()), a single recv() pulls in up to \p len bytes:
5534+
- \p len of 0 resets the window to the one-record default. This is also
5535+
the window a freshly created context already carries, so the record body
5536+
is read together with its header without a second syscall.
5537+
- A \p len larger than one record lets a single recv() coalesce several
5538+
back-to-back records, one syscall instead of one per record.
5539+
- A \p len smaller than one record caps the receive buffer's footprint
5540+
when the peer's records are known to be small (e.g. 4 KB), saving heap
5541+
versus the one-record default.
5542+
\p len is only a speculative read window, not a hard limit: a record larger
5543+
than \p len is still received correctly, with the input buffer grown to the
5544+
record's actual size on demand (costing an extra syscall and reallocation
5545+
for that record) and then reallocated back down to the window once the
5546+
oversized record is consumed, so the retained footprint stays bounded by
5547+
\p len rather than by the largest record seen. The setting only affects I/O
5548+
when the library is built with read-ahead support (--enable-readahead /
5549+
WOLFSSL_TLS_READ_AHEAD); otherwise it is stored but inert. This is the
5550+
wolfSSL equivalent of OpenSSL's SSL_CTX_set_default_read_buffer_len(); unlike
5551+
OpenSSL it returns a status code (callers that ignore the return remain
5552+
source-compatible) and it honours sizes below one record, whereas OpenSSL
5553+
only ever enlarges the buffer. A \p len above WOLFSSL_MAX_READ_AHEAD_SZ
5554+
(16 MB) is clamped to that maximum. Because 0 and oversized values are both
5555+
normalised, wolfSSL_CTX_get_default_read_buffer_len() reports the effective
5556+
window (the one-record default rather than 0 when unset), not the raw
5557+
argument.
5558+
5559+
\note This is a memory-vs-syscall trade-off. While read-ahead is enabled the
5560+
input buffer is retained (bounded to \p len) for the connection's lifetime,
5561+
so a large \p len multiplied by many concurrent connections is persistent
5562+
memory, while a small \p len bounds per-connection footprint at the cost of
5563+
more syscalls for records that exceed it.
5564+
5565+
\return SSL_SUCCESS If the buffer length was set.
5566+
\return SSL_FAILURE If ctx is NULL.
5567+
5568+
\param ctx WOLFSSL_CTX structure to set the read-ahead buffer length on.
5569+
\param len read-ahead coalescing buffer size in bytes (0 = one record).
5570+
5571+
_Example_
5572+
\code
5573+
WOLFSSL_CTX* ctx;
5574+
// setup ctx
5575+
wolfSSL_CTX_set_read_ahead(ctx, 1);
5576+
// coalesce up to four max-size records per recv()
5577+
wolfSSL_CTX_set_default_read_buffer_len(ctx, 4 * 16384);
5578+
\endcode
5579+
5580+
\sa wolfSSL_CTX_set_read_ahead
5581+
\sa wolfSSL_set_default_read_buffer_len
5582+
\sa wolfSSL_has_pending
5583+
*/
5584+
int wolfSSL_CTX_set_default_read_buffer_len(WOLFSSL_CTX* ctx, size_t len);
5585+
5586+
/*!
5587+
\ingroup Setup
5588+
5589+
\brief This function sets the read-ahead coalescing buffer size on a single
5590+
WOLFSSL session, overriding the value inherited from its WOLFSSL_CTX. See
5591+
wolfSSL_CTX_set_default_read_buffer_len() for the full description, the
5592+
one-record default, and the memory-vs-syscall trade-off.
5593+
5594+
\return SSL_SUCCESS If the buffer length was set.
5595+
\return SSL_FAILURE If ssl is NULL.
5596+
5597+
\param ssl WOLFSSL structure to set the read-ahead buffer length on.
5598+
\param len read-ahead coalescing buffer size in bytes (0 = one record).
5599+
5600+
\sa wolfSSL_CTX_set_default_read_buffer_len
5601+
\sa wolfSSL_set_read_ahead
5602+
\sa wolfSSL_has_pending
5603+
*/
5604+
int wolfSSL_set_default_read_buffer_len(WOLFSSL* ssl, size_t len);
5605+
55125606
/*!
55135607
\ingroup Setup
55145608

examples/benchmark/tls_bench.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,13 @@ static int bench_tls_client(info_t* info)
11001100
#endif
11011101
wolfSSL_SetIOReadCtx(cli_ssl, info);
11021102
wolfSSL_SetIOWriteCtx(cli_ssl, info);
1103+
#if defined(WOLFSSL_TLS_READ_AHEAD) && !defined(NO_FILESYSTEM) && \
1104+
!defined(NO_STDIO_FILESYSTEM)
1105+
/* Optional A/B benchmark toggle for TLS receive read-ahead. Gated on
1106+
* filesystem support since XGETENV is only defined there. */
1107+
if (XGETENV("WOLF_BENCH_READ_AHEAD") != NULL)
1108+
wolfSSL_set_read_ahead(cli_ssl, 1);
1109+
#endif
11031110

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

15581565
wolfSSL_SetIOReadCtx(srv_ssl, info);
15591566
wolfSSL_SetIOWriteCtx(srv_ssl, info);
1567+
#if defined(WOLFSSL_TLS_READ_AHEAD) && !defined(NO_FILESYSTEM) && \
1568+
!defined(NO_STDIO_FILESYSTEM)
1569+
/* Optional A/B benchmark toggle for TLS receive read-ahead. Gated on
1570+
* filesystem support since XGETENV is only defined there. */
1571+
if (XGETENV("WOLF_BENCH_READ_AHEAD") != NULL)
1572+
wolfSSL_set_read_ahead(srv_ssl, 1);
1573+
#endif
15601574
#ifndef NO_DH
15611575
wolfSSL_SetTmpDH(srv_ssl, dhp, sizeof(dhp), dhg, sizeof(dhg));
15621576
#endif

examples/client/client.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,15 @@ static int ClientBenchmarkThroughput(WOLFSSL_CTX* ctx, char* host, word16 port,
790790
if (ssl == NULL)
791791
err_sys("unable to get SSL object");
792792

793+
#if defined(WOLFSSL_TLS_READ_AHEAD) && !defined(NO_FILESYSTEM) && \
794+
!defined(NO_STDIO_FILESYSTEM)
795+
/* Optional A/B toggle: enable TLS receive read-ahead for the throughput
796+
* benchmark when WOLF_BENCH_READ_AHEAD is set in the environment. Gated on
797+
* filesystem support since XGETENV is only defined there. */
798+
if (XGETENV("WOLF_BENCH_READ_AHEAD") != NULL)
799+
wolfSSL_set_read_ahead(ssl, 1);
800+
#endif
801+
793802
tcp_connect(&sockfd, host, port, dtlsUDP, dtlsSCTP, ssl);
794803
if (wolfSSL_set_fd(ssl, sockfd) != WOLFSSL_SUCCESS) {
795804
err_sys("error in setting fd");

src/internal.c

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2650,6 +2650,15 @@ int InitSSL_Ctx(WOLFSSL_CTX* ctx, WOLFSSL_METHOD* method, void* heap)
26502650
}
26512651
ctx->timeout = WOLFSSL_SESSION_TIMEOUT;
26522652

2653+
#if defined(OPENSSL_EXTRA) || defined(WOLFSSL_TLS_READ_AHEAD)
2654+
/* Default the read-ahead window to one full record. Contexts (and the
2655+
* WOLFSSL objects that inherit it) then always carry a concrete window, so
2656+
* the receive path uses ssl->readAheadSz directly without a per-read
2657+
* fallback. A caller override replaces it; passing 0 resets it to this
2658+
* default (see wolfSSL_CTX_set_default_read_buffer_len()). */
2659+
ctx->readAheadSz = WOLFSSL_READ_AHEAD_SZ;
2660+
#endif
2661+
26532662
#ifdef WOLFSSL_DTLS
26542663
if (method->version.major == DTLS_MAJOR) {
26552664
ctx->minDowngrade = WOLFSSL_MIN_DTLS_DOWNGRADE;
@@ -7509,8 +7518,9 @@ int SetSSL_CTX(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup)
75097518
ssl->ConnectFilter_arg = ctx->ConnectFilter_arg;
75107519
#endif
75117520

7512-
#ifdef OPENSSL_EXTRA
7521+
#if defined(OPENSSL_EXTRA) || defined(WOLFSSL_TLS_READ_AHEAD)
75137522
ssl->readAhead = ctx->readAhead;
7523+
ssl->readAheadSz = ctx->readAheadSz;
75147524
#endif
75157525
#if defined(OPENSSL_EXTRA) && !defined(NO_BIO)
75167526
/* Don't change recv callback if currently using BIO's */
@@ -11619,6 +11629,41 @@ void ShrinkInputBuffer(WOLFSSL* ssl, int forcedFree)
1161911629
ssl->buffers.clearOutputBuffer.length > 0))
1162011630
return;
1162111631

11632+
#ifdef WOLFSSL_TLS_READ_AHEAD
11633+
/* While read-ahead is enabled, retain a dynamic input buffer sized to the
11634+
* configured window rather than shrinking all the way back to the static
11635+
* buffer, so the speculative over-read is a bounded, mostly one-time
11636+
* allocation instead of per-record churn. A forced free during connection
11637+
* teardown still reclaims everything. */
11638+
if (!forcedFree && ssl->readAhead) {
11639+
/* Already within the window: keep the buffer as-is. */
11640+
if (ssl->buffers.inputBuffer.bufferSize <= ssl->readAheadSz)
11641+
return;
11642+
11643+
/* The buffer grew past the window to receive an oversized record. When
11644+
* the window needs a dynamic buffer, reallocate down to it so the
11645+
* retained footprint tracks the window, not the largest record seen;
11646+
* when the window fits in the static buffer, fall through and shrink to
11647+
* static below.
11648+
*
11649+
* GrowInputBuffer(newBytes, usedLength) resizes the input buffer to
11650+
* newBytes + usedLength while preserving the usedLength bytes still
11651+
* pending, so requesting (readAheadSz - usedLength) new bytes yields a
11652+
* buffer of exactly readAheadSz. usedLength is <= STATIC_BUFFER_LEN <
11653+
* readAheadSz here (guaranteed above), so the subtraction stays
11654+
* positive. */
11655+
if (ssl->readAheadSz > STATIC_BUFFER_LEN) {
11656+
if (GrowInputBuffer(ssl, (int)ssl->readAheadSz - usedLength,
11657+
usedLength) != 0) {
11658+
/* Realloc failed: keep the current (larger) buffer rather than
11659+
* dropping the buffered data. */
11660+
WOLFSSL_MSG("read-ahead buffer shrink failed, keeping buffer");
11661+
}
11662+
return;
11663+
}
11664+
}
11665+
#endif
11666+
1162211667
WOLFSSL_MSG("Shrinking input buffer");
1162311668

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

23192-
static int GetInputData(WOLFSSL *ssl, word32 size)
23237+
static int GetInputData_ex(WOLFSSL *ssl, word32 size, word32 readAhead)
2319323238
{
2319423239
int inSz;
2319523240
int maxLength;
2319623241
int usedLength;
2319723242
int dtlsExtra = 0;
23243+
int extra = 0;
23244+
#ifndef WOLFSSL_TLS_READ_AHEAD
23245+
(void)readAhead;
23246+
#endif
2319823247

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

2323323282
inSz = (int)(size - (word32)usedLength); /* from last partial read */
23283+
23284+
#ifdef WOLFSSL_TLS_READ_AHEAD
23285+
/* Request more than the minimum so that a single recv() can also pull
23286+
* in the record body (and possibly following records), avoiding a
23287+
* second syscall. 'size' remains the loop-termination minimum, so a
23288+
* blocking socket never waits for read-ahead bytes the peer may not
23289+
* send. Mirrors the DTLS dtlsExtra over-read above.
23290+
*
23291+
* The buffer is grown once to hold a full record and then kept (see the
23292+
* ssl->readAhead guard in ShrinkInputBuffer), so this is a one-time
23293+
* allocation per connection, not per-record churn. */
23294+
if (readAhead > size) {
23295+
extra = (int)(readAhead - size);
23296+
inSz += extra;
23297+
}
23298+
#endif
2323423299
}
2323523300

2323623301
if (inSz > maxLength) {
23237-
if (GrowInputBuffer(ssl, (int)(size + (word32)dtlsExtra), usedLength) < 0)
23302+
if (GrowInputBuffer(ssl,
23303+
(int)(size + (word32)dtlsExtra + (word32)extra), usedLength) < 0)
2323823304
return MEMORY_E;
2323923305
}
2324023306

@@ -23292,6 +23358,11 @@ static int GetInputData(WOLFSSL *ssl, word32 size)
2329223358
return 0;
2329323359
}
2329423360

23361+
static int GetInputData(WOLFSSL *ssl, word32 size)
23362+
{
23363+
return GetInputData_ex(ssl, size, 0);
23364+
}
23365+
2329523366
#if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY)
2329623367
static WC_INLINE int VerifyMacEnc(WOLFSSL* ssl, const byte* input, word32 msgSz,
2329723368
int content)
@@ -24123,7 +24194,28 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr)
2412324194

2412424195
/* get header or return error */
2412524196
if (!ssl->options.dtls) {
24126-
if ((ret = GetInputData(ssl, (word32)readSz)) < 0)
24197+
word32 readAheadSz = 0;
24198+
#ifdef WOLFSSL_TLS_READ_AHEAD
24199+
/* When read-ahead is enabled, request more than the record
24200+
* header in a single recv() so the body (and possibly following
24201+
* records) can be pulled in without a second syscall. The window
24202+
* size is configurable via
24203+
* wolfSSL_CTX_set_default_read_buffer_len():
24204+
* - 0 (unset) requests one full record, the sensible default.
24205+
* - A larger value lets one recv() coalesce several back-to-back
24206+
* records.
24207+
* - A smaller (sub-record) value caps the receive buffer's
24208+
* footprint when the peer's records are known to be small.
24209+
* The value is only a speculative read window: a record larger
24210+
* than it is still received correctly, as the buffer is grown to
24211+
* the record's actual size on demand. */
24212+
if (ssl->readAhead) {
24213+
/* readAheadSz is always concrete (defaulted to
24214+
* WOLFSSL_READ_AHEAD_SZ at CTX init, never 0). */
24215+
readAheadSz = ssl->readAheadSz;
24216+
}
24217+
#endif
24218+
if ((ret = GetInputData_ex(ssl, (word32)readSz, readAheadSz)) < 0)
2412724219
return ret;
2412824220
} else {
2412924221
#ifdef WOLFSSL_DTLS
@@ -24848,6 +24940,16 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr)
2484824940
ssl->options.serverState ==
2484924941
SERVER_FINISHED_COMPLETE &&
2485024942
ssl->options.handShakeState != HANDSHAKE_DONE)))
24943+
#endif
24944+
#ifdef WOLFSSL_TLS_READ_AHEAD
24945+
/* With read-ahead, more than one record may be buffered. If
24946+
* application data was just decrypted, return it now so it
24947+
* is delivered to the caller before any following buffered
24948+
* record (e.g. a close_notify alert) is processed, which
24949+
* would otherwise discard the pending app data. The
24950+
* remaining records stay buffered for the next call. */
24951+
|| (ssl->curRL.type == application_data &&
24952+
ssl->buffers.clearOutputBuffer.length > 0)
2485124953
#endif
2485224954
) {
2485324955
/* Shrink input buffer when we successfully finish record

0 commit comments

Comments
 (0)