Skip to content

Size the pre-master secret buffer by the key exchanges, and fix a DHE-PSK overflow - #11345

Open
Frauschi wants to merge 3 commits into
wolfSSL:masterfrom
Frauschi:tls_mem_4
Open

Frauschi wants to merge 3 commits into
wolfSSL:masterfrom
Frauschi:tls_mem_4

Conversation

@Frauschi

@Frauschi Frauschi commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

One of six independent branches that cut allocations and per-object memory in the TLS layer. This one also carries a memory-safety fix.

The fix

A DHE-PSK exchange stores a two byte length, the DH shared secret, another two byte length and the PSK in the pre-master secret buffer. Sized with ENCRYPT_LEN, that is two bytes short for the largest DH parameters the build accepts together with a maximum length PSK, and the last two bytes of the key were written past the allocation on both peers. It is reached before authentication completes, from parameters the peer supplies.

test_dhe_psk_max_premaster() drives the handshake that produces the largest secret: 4096 bit parameters on the server and a MAX_PSK_KEY_LEN PSK. The client asks for an ECC named group so FFDHE negotiation does not substitute a smaller group, and the negotiated key size is checked to confirm it did not. Against the old sizing the assertion fails and a sanitizer build reports a heap buffer overflow in MakePSKPreMasterSecret().

What else changes

The buffer is part of the Arrays struct. It used to be its own allocation - made eagerly for TLS 1.3 and the sniffer, lazily elsewhere, and freed by CleanPreMaster() or FreeArrays() - for a buffer whose lifetime is exactly that of the Arrays it belongs to. It is now a member, so the two are created, wiped and released together, and nothing tracks or frees it separately.

It is sized by what the build can actually produce. ENCRYPT_LEN also has to cover an RSA signature, so it follows the largest RSA key the build supports: a build with RSA 4096 certificates reserved 512 bytes for a secret ECDHE fills 32 to 98 bytes of. MAX_PREMASTER_SZ takes the largest secret any key exchange in the build can produce - the RSA pre-master, the DH parameters a peer may send, an ECDHE secret plus the 32 byte ML-KEM half of a hybrid key share, the length prefixes and key the PSK suites append, the TLS 1.3 handshake secret that reuses the buffer - with the master secret length as a floor.

A build with DH is unchanged, since the peer chooses the parameter size. A build without it, the usual TLS 1.3 configuration, holds 98 rather than 512 bytes per handshake. A build with both DH and PSK gains two bytes, which is the overflow above.

WOLFSSL_MAX_GEN_PREMASTER_SZ lets an integration that knows what its GenPreMasterCb writes opt down; it defaults to ENCRYPT_LEN so no existing callback can be overrun, and the capacity handed to the callback is unchanged.

Behaviour changes worth flagging

wolfSSL_[CTX_]SetMaxDhKey_Sz() now returns BAD_FUNC_ARG for a ceiling above what the build's math supports, rather than accepting it and silently reducing it. This is load-bearing: without it an application could raise maxDhKeySz above the buffer size and reopen the overflow. An application that defensively calls wolfSSL_CTX_SetMaxDhKey_Sz(ctx, 4096) on a build whose math tops out lower now gets an error where it previously got success. The check sits after the crypto-policy block so a request that violates both is still reported as the policy violation.

The minimum setters are deliberately unchanged: a floor above what the build supports is a tightening the caller asked for, it simply refuses every key a peer can offer, and it cannot overrun anything.

wolfSSL_dtls_import() treats the serialized DH sizes the same way: the exported ceiling is clamped, a negotiated size above what this build supports fails the import, and the floor carries over as-is, matching the minimum setters.

Memory impact

Measured on 64-bit builds. Arrays is one allocation per handshake; on master the pre-master secret was a second one beside it.

TLS 1.3 without DH or PSK (--enable-tls13 --disable-dh --disable-psk), the usual TLS 1.3 configuration:

master this branch
sizeof(Arrays) 208 300
separate pre-master buffer 512 none
total per handshake 720 bytes in 2 allocations 300 bytes in 1

That is 420 bytes and one allocation/free pair saved per handshake, because MAX_PREMASTER_SZ comes out at 98 where ENCRYPT_LEN was 512.

Default build (DH enabled, so the peer chooses the parameter size and the buffer cannot shrink):

master this branch
total per handshake 720 bytes in 2 allocations 712 bytes in 1

No meaningful size change, but still one fewer allocation and free per handshake, and the buffer is now wiped together with the arrays rather than needing its own CleanPreMaster() path.

@Frauschi Frauschi self-assigned this Sep 1, 2026
@github-actions

github-actions Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report

gcc-arm-cortex-m3

  • FLASH: .text -80 B (-0.1%, 127,845 B / 262,144 B, total: 49% used)

gcc-arm-cortex-m4-dtls13

  • FLASH: .text -64 B (-0.0%, 190,916 B / 1,048,576 B, total: 18% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .text -128 B (-0.0%, 788,892 B / 1,048,576 B, total: 75% used)

gcc-arm-cortex-m4-pq

  • FLASH: .text -64 B (-0.0%, 306,864 B / 1,048,576 B, total: 29% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .text -128 B (-0.0%, 337,008 B / 1,048,576 B, total: 32% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text -64 B (-0.0%, 128,637 B / 262,144 B, total: 49% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text -128 B (-0.1%, 245,790 B / 262,144 B, total: 94% used)

gcc-arm-cortex-m7

  • FLASH: .text -64 B (-0.0%, 206,572 B / 262,144 B, total: 79% used)

gcc-arm-cortex-m7-pq

  • FLASH: .text -128 B (-0.0%, 307,760 B / 1,048,576 B, total: 29% used)

gcc-arm-cortex-m7-tls13

@Frauschi

Frauschi commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Jenkins retest this please - history lost.

@Frauschi
Frauschi requested review from wolfSSL-Fenrir-bot and removed request for wolfSSL-Fenrir-bot September 2, 2026 07:19

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #11345

Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src

Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread src/keys.c Outdated
Comment thread tests/api/test_dtls.c Outdated
Comment thread src/keys.c Outdated
Comment thread tests/api/test_dtls.c Outdated

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #11345

Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread tests/api.c Outdated
Comment thread tests/api.c Outdated
@Frauschi

Frauschi commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Jenkins retest this please

@Frauschi
Frauschi requested review from wolfSSL-Fenrir-bot and removed request for wolfSSL-Fenrir-bot September 3, 2026 06:29
@Frauschi Frauschi assigned wolfSSL-Bot and unassigned Frauschi Sep 3, 2026
@Frauschi
Frauschi requested review from wolfSSL-Fenrir-bot and removed request for wolfSSL-Fenrir-bot September 7, 2026 10:59

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #11345

Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread src/internal.c Outdated
Comment thread src/internal.c Outdated

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #11345

Scan targets checked: wolfcrypt-rs-bugs, wolfssl-bugs, wolfssl-src

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

@wolfSSL-Fenrir-bot
wolfSSL-Fenrir-bot dismissed their stale review September 7, 2026 13:57

Fenrir's latest completed scan found no issues; clearing the prior automated change request.

@wolfSSL-Fenrir-bot
wolfSSL-Fenrir-bot dismissed stale reviews from themself September 7, 2026 13:57

Fenrir's latest completed scan found no issues; clearing the prior automated change request.

@Frauschi

Frauschi commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

Jenkins retest this please - flaky test

@Frauschi
Frauschi force-pushed the tls_mem_4 branch 2 times, most recently from 64b24b7 to 5421c4a Compare September 24, 2026 17:52
Copilot AI balanced review requested due to automatic review settings October 1, 2026 07:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Embedded storage causes a use-after-free with async Intel QAT, and the DH import-floor behavior contradicts the stated contract.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Resizes and embeds the TLS pre-master secret buffer to fix a DHE-PSK overflow while reducing handshake allocations.

Changes:

  • Derives buffer capacity from enabled key exchanges.
  • Embeds and consistently wipes the buffer with Arrays.
  • Adds boundary, import-policy, and regression tests.
File Description
wolfssl/​internal.h Defines sizing and embeds the buffer.
src/​internal.c Updates allocation, DH/PSK handling, and imports.
src/​keys.c Updates derivation buffers and cleanup.
src/​ssl.c Resets embedded secret storage.
src/​ssl_api_dtls.c Bounds and clears multicast secrets.
src/​ssl_api_pk.c Enforces supported DH ceilings.
src/​sniffer.c Updates capacities and secret cleanup.
src/​tls.c Updates key-share bounds.
src/​tls13.c Fully wipes TLS 1.3 secrets.
tests/​api.c Updates secret-zeroization assertions.
tests/​api/​test_dtls.c Tests import and secret boundaries.
tests/​api/​test_dtls.h Registers the DTLS test.
tests/​api/​test_ssl_pk.c Tests DH ceiling validation.
tests/​api/​test_tls.c Adds the DHE-PSK overflow regression.
tests/​api/​test_tls.h Registers the TLS test.
tests/​api/​test_tls13.c Updates manual array cleanup.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread wolfssl/internal.h
Comment thread src/internal.c
@philljj philljj self-assigned this Oct 1, 2026
Comment thread src/tls13.c Outdated
@philljj philljj assigned Frauschi and unassigned wolfSSL-Bot Oct 1, 2026
The pre-master secret buffer was its own allocation, made eagerly for TLS 1.3
and the sniffer and lazily in SendClientKeyExchange, DoClientKeyExchange and
wolfSSL_set_secret() otherwise, then freed by CleanPreMaster() or FreeArrays().
That is one allocation and one free per handshake for a buffer whose lifetime
is exactly that of the Arrays it belongs to.

Reserve ENCRYPT_LEN bytes at the end of the Arrays allocation instead and point
preMasterSecret at them. The call sites that used to allocate now only reset
the size and clear the buffer, CleanPreMaster() wipes it in place, and
FreeArrays() wipes both in one call. A TLS 1.2 only build reserves the buffer
when the arrays are created rather than when the key exchange runs, which
holds ENCRYPT_LEN bytes for longer but costs no extra peak, as both were live
together before.

test_tls13_fragmented_session_ticket() releases the arrays by hand, because
FreeArrays() is hidden in shared library builds, so it releases the one
allocation now.
The pre-master secret buffer was sized with ENCRYPT_LEN. That constant also has
to cover an RSA signature, so it follows the largest RSA key the build supports
and a build with RSA 4096 certificates reserved 512 bytes for a secret that
ECDHE fills 32 to 98 bytes of.

MAX_PREMASTER_SZ takes the largest secret any key exchange in the build can
produce: the RSA pre-master, the DH parameters the peer may send, an ECDHE
secret plus the 32 byte ML-KEM half of a hybrid key share, and the length
prefixes and key that the PSK suites append, with the master secret length as
a floor.

A build with DH is unchanged, since the peer chooses the parameter size there.
A build without it, which is the usual TLS 1.3 configuration, holds 98 rather
than 512 bytes per handshake. A build with both DH and PSK gains two bytes:
a DHE-PSK exchange writes a two byte length, the DH secret, another two byte
length and the key, which is two more than ENCRYPT_LEN reserved for the
largest DH parameters and the longest PSK.

The buffer becomes the first member of Arrays, so async Intel QAT builds
allocate Arrays as DYNAMIC_TYPE_SECRET. A QAT key agreement reallocs its
output buffer into NUMA memory, and IntelQaRealloc() now finds the Arrays
header there: a non-NUMA one makes it free the whole struct, a NUMA one lets
it write in place, as it did for the standalone DYNAMIC_TYPE_SECRET buffer.
A static assert keeps the buffer at offset 0.
A DHE-PSK exchange stores a two byte length, the DH shared secret, another two
byte length and the PSK in the pre-master secret buffer. Sized with
ENCRYPT_LEN, as it was before MAX_PREMASTER_SZ, that came up two bytes short
for the largest DH parameters the build accepts together with a maximum length
PSK, and the last two bytes of the key were written past the allocation on both
peers.

The test asserts the sizes add up, then runs the handshake that produces the
largest secret: 4096 bit parameters loaded on the server and a PSK of
MAX_PSK_KEY_LEN bytes. The client asks for an ECC named group so the FFDHE
negotiation does not replace the server's parameters with a smaller group, and
the negotiated key size is checked to confirm it did not.

Against the old sizing the assertion fails, and the handshake reports a heap
buffer overflow in MakePSKPreMasterSecret() under a sanitizer build.
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.

6 participants