Skip to content

Add TLS receive read-ahead support#10944

Open
Frauschi wants to merge 1 commit into
wolfSSL:masterfrom
Frauschi:tls-read-ahead
Open

Add TLS receive read-ahead support#10944
Frauschi wants to merge 1 commit into
wolfSSL:masterfrom
Frauschi:tls-read-ahead

Conversation

@Frauschi

Copy link
Copy Markdown
Contributor

Description

Adds an optional TLS receive read-ahead mode that cuts the number of recv() syscalls on the TLS receive path.

Without read-ahead, reading one TLS record needs at least two socket reads: one for the 5-byte record header (to learn the record length), then one (or more) for the body. With read-ahead enabled, the header read instead requests a full record's worth of data in a single recv(), so the body — and any following records the peer already sent — arrive without a second syscall.

The feature is:

  • Off by default, compiled in with --enable-readahead (WOLFSSL_TLS_READ_AHEAD).
  • Toggled at runtime per WOLFSSL / WOLFSSL_CTX via wolfSSL_set_read_ahead() (the existing OpenSSL-compatible flag).
  • Tunable via wolfSSL_CTX_set_default_read_buffer_len() / wolfSSL_set_default_read_buffer_len() (OpenSSL-compatible), which set the read-ahead window.

New API

int  wolfSSL_CTX_set_default_read_buffer_len(WOLFSSL_CTX* ctx, size_t len);
int  wolfSSL_set_default_read_buffer_len(WOLFSSL* ssl, size_t len);
long wolfSSL_CTX_get_default_read_buffer_len(WOLFSSL_CTX* ctx);
long wolfSSL_get_default_read_buffer_len(const WOLFSSL* ssl);

The existing wolfSSL_[CTX_]set/get_read_ahead() flag functions are now also
available under WOLFSSL_TLS_READ_AHEAD (previously OPENSSL_EXTRA only).

Behavior and design

  • Window semantics (set_default_read_buffer_len):
    • 0 selects the one-record default (WOLFSSL_READ_AHEAD_SZ, one max TLS record) — this is also the value a fresh context carries.
    • A value larger than one record lets a single recv() coalesce several back-to-back records.
    • A value smaller than one record caps per-connection buffer footprint when records are known to be small.
  • Speculative, not a hard limit: a record larger than the window is still received correctly — the input buffer grows on demand, then is reallocated back down to the window once the oversized record is consumed, so persistent footprint stays bounded by the window rather than by the largest record seen.
  • Bounded input: the window is clamped to WOLFSSL_MAX_READ_AHEAD_SZ (16 MB) so a caller-supplied size can never overflow the receive-path arithmetic.
  • Blocking sockets never stall: the record-completion minimum is unchanged; read-ahead only enlarges the single speculative read, so a blocking socket never waits for read-ahead bytes the peer may not send.
  • wolfSSL_has_pending() reports buffered-but-undecrypted data so event-driven (select/poll) apps drain everything before returning to the poll loop. As with OpenSSL's SSL_has_pending, a non-zero return does not guarantee a full record — apps should read until WANT_READ.

OpenSSL compatibility

SSL_CTX/SSL_set_default_read_buffer_len() and SSL_[CTX_]set/get_read_ahead() map to these functions, so existing OpenSSL code compiles unchanged. The wolfSSL setters return a status code (OpenSSL returns void; callers that ignore the return remain source-compatible) and additionally honour sub-record window sizes, whereas OpenSSL only ever enlarges the buffer.

Benchmarks

curl 100 MB HTTP/1.1 downloads through wolfSSL against httpd 2.4.68 (scorecard.py, 10 samples each; aarch64-apple-darwin, curl 8.22.0-DEV / wolfSSL 5.9.2). Format is throughput [cpu%/rss].

Config single serial (50) parallel (50x50)
Baseline (read-ahead off) 1.77 GB/s [83.5%/13MB] 2.53 GB/s [85.3%/15MB] 4.20 GB/s [99.6%/21MB]
Read-ahead 1.76 GB/s [81.8%/14MB] 2.55 GB/s [83.3%/15MB] 5.04 GB/s [99.4%/25MB]
  • Parallel (high concurrency): ~+20% throughput (4.20 -> ~5.03 GB/s), where eliminating a syscall per record matters most.
  • Single / serial: unchanged within run-to-run noise — already bound by crypto/copy cost, not syscall count.
  • Memory: parallel RSS rises 21 -> 25 MB (the retained per-connection read-ahead buffers); the increase is bounded by the configured window.

The A/B toggle used here is wired into examples/benchmark/tls_bench.c and examples/client/client.c behind the WOLF_BENCH_READ_AHEAD environment variable (active when built with read-ahead and filesystem support).

Testing

New API tests in tests/api.c:

  • test_wolfSSL_read_ahead — confirms the record header + body are fetched in a single recv() (fewer callbacks than the two-read default).
  • test_wolfSSL_read_ahead_coalesced — an app-data record coalesced with a following close_notify is delivered before the close, and stays visible to wolfSSL_has_pending().
  • test_wolfSSL_read_ahead_buffer_len — window sizing, coalescing, retained footprint, and shrink-back after an oversized record.
  • test_wolfSSL_read_ahead_ctx_inherit — CTX-level accessors, CTX->SSL inheritance, NULL-argument handling, 0-resets-to-default, and the oversized-window clamp.

Verified building and passing under --enable-readahead (feature on) and --enable-opensslextra (accessors compiled, read-ahead I/O off, tests skip); the client and benchmark examples compile in both configurations.

Configuration

  • --enable-readahead -> WOLFSSL_TLS_READ_AHEAD
  • No effect on default builds — the feature is inert unless both compiled in and enabled at runtime.

Backward compatibility

No public API signatures changed. The wolfSSL_[CTX_]set/get_read_ahead() declarations are now guarded by OPENSSL_EXTRA || WOLFSSL_TLS_READ_AHEAD to match their definitions and the underlying struct members; existing OPENSSL_EXTRA builds are unaffected.

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.
@Frauschi Frauschi self-assigned this Jul 17, 2026
@github-actions

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant