Skip to content

Zero-copy encrypt of AEAD application data#10899

Open
julek-wolfssl wants to merge 2 commits into
wolfSSL:masterfrom
julek-wolfssl:julek-dev/zero-copy-record-encrypt
Open

Zero-copy encrypt of AEAD application data#10899
julek-wolfssl wants to merge 2 commits into
wolfSSL:masterfrom
julek-wolfssl:julek-dev/zero-copy-record-encrypt

Conversation

@julek-wolfssl

Copy link
Copy Markdown
Member

Summary

BuildMessage copied the application data into the output buffer and encrypted it in place. For AEAD suites the cipher input is exactly the plaintext, so Encrypt can read it straight from the caller's buffer and write the record to the output buffer, skipping the copy.

  • EncryptDo AES-GCM/CCM: input == out keeps the in-place record layout [explicit IV | plaintext | tag space]; input != out means input is the plaintext itself. ChaCha20-Poly1305 already reads only the plaintext from input. Other suites keep the copy: CBC/stream append MAC/pad to the cipher input, CID appends the real content type.
  • Disabled for WOLFSSL_ASYNC_CRYPT and WOLFSSL_THREADED_CRYPT, where encryption can run after BuildMessage returns, and for debug hooks that expect the in-place layout. Define WOLFSSL_BUILD_MSG_NO_ZERO_COPY to force the old behavior.
  • Measured with examples/benchmark/dtls_bench -z (DTLS 1.2, 1300 byte records, aesni + intelasm): +2.9% AES-256-GCM, +1.0% ChaCha20-Poly1305. The copy was ~3% of send-path CPU in perf.

Also fixes two bugs found while benchmarking with a larger STATIC_BUFFER_LEN:

  • The STATIC_BUFFER_LEN bounds check had an unbalanced paren and compared enum constants in a preprocessor #if, where they evaluate to 0, so any user supplied value failed to compile. Keep the lower bound as a static assert. Drop the upper bound: values past the largest record are never used by the record layer but can be useful for memory layout reasons.
  • ShrinkInputBuffer could run in the middle of record processing via FreeHandshakeResources on the first DTLS app data record, moving the record data and invalidating the idx based bookkeeping. The stale curStartIdx made the more-messages-per-record check dispatch a spurious second message over the AEAD tag bytes, handing garbage app data to the caller and corrupting the buffer state for the next read. Only reachable when the remaining record data fits in the static buffer, which it never does with the default STATIC_BUFFER_LEN. Only shrink between records; the deferred shrink happens at the next wolfSSL_read.

Test plan

  • CI passes
  • Benchmark with examples/benchmark/dtls_bench -z shows no regression
  • DTLS app-data read tests pass with a larger STATIC_BUFFER_LEN

BuildMessage copied the application data into the output buffer and
encrypted it in place. For AEAD suites the cipher input is exactly the
plaintext, so Encrypt can read it straight from the caller's buffer and
write the record to the output buffer, skipping the copy.

- EncryptDo AES-GCM/CCM: input == out keeps the in-place record layout
  [explicit IV | plaintext | tag space]; input != out means input is
  the plaintext itself. ChaCha20-Poly1305 already reads only the
  plaintext from input. Other suites keep the copy: CBC/stream append
  MAC/pad to the cipher input, CID appends the real content type.
- Disabled for WOLFSSL_ASYNC_CRYPT and WOLFSSL_THREADED_CRYPT, where
  encryption can run after BuildMessage returns, and for debug hooks
  that expect the in-place layout. Define WOLFSSL_BUILD_MSG_NO_ZERO_COPY
  to force the old behavior.

Measured with examples/benchmark/dtls_bench -z (DTLS 1.2, 1300 byte
records, aesni + intelasm): +2.9% AES-256-GCM, +1.0% ChaCha20-Poly1305.
The copy was ~3% of send-path CPU in perf.

Also fix two bugs found while benchmarking with a larger
STATIC_BUFFER_LEN:

- The STATIC_BUFFER_LEN bounds check had an unbalanced paren and
  compared enum constants in a preprocessor #if, where they evaluate to
  0, so any user supplied value failed to compile. Keep the lower bound
  as a static assert. Drop the upper bound: values past the largest
  record are never used by the record layer but can be useful for
  memory layout reasons.

- ShrinkInputBuffer could run in the middle of record processing via
  FreeHandshakeResources on the first DTLS app data record, moving the
  record data and invalidating the idx based bookkeeping. The stale
  curStartIdx made the more-messages-per-record check dispatch a
  spurious second message over the AEAD tag bytes, handing garbage app
  data to the caller and corrupting the buffer state for the next read.
  Only reachable when the remaining record data fits in the static
  buffer, which it never does with the default STATIC_BUFFER_LEN. Only
  shrink between records; the deferred shrink happens at the next
  wolfSSL_read.
Copilot AI review requested due to automatic review settings July 14, 2026 10:10
@julek-wolfssl julek-wolfssl self-assigned this Jul 14, 2026

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.

Pull request overview

This PR optimizes the TLS 1.2 record send path for AEAD application-data by enabling a zero-copy mode where encryption reads plaintext directly from the caller’s buffer and writes the record ciphertext into the output buffer, avoiding an intermediate memcpy. It also includes two correctness fixes discovered during benchmarking with larger STATIC_BUFFER_LEN values, affecting compile-time validation and DTLS record processing safety.

Changes:

  • Add conditional “zero-copy” AEAD encryption in BuildMessage() for AES-GCM/CCM and ChaCha20-Poly1305 (with config-based exclusions and an opt-out macro).
  • Fix STATIC_BUFFER_LEN validation by replacing a broken preprocessor check with a compile-time static assertion and removing the (unnecessary) upper bound.
  • Prevent ShrinkInputBuffer() from moving the input buffer during in-flight record processing by gating shrinking on processReply state.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
wolfssl/internal.h Replaces invalid #if validation of STATIC_BUFFER_LEN with wc_static_assert() to ensure a usable minimum buffer size.
src/internal.c Implements AEAD zero-copy encryption plumbing in BuildMessage()/EncryptDo() and hardens ShrinkInputBuffer() against mid-record buffer moves.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@julek-wolfssl julek-wolfssl marked this pull request as ready for review July 14, 2026 10:30
@github-actions

Copy link
Copy Markdown

retest this please

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

gcc-arm-cortex-m3

  • FLASH: .text +64 B (+0.1%, 122,569 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m4

  • FLASH: .text +64 B (+0.0%, 200,448 B / 262,144 B, total: 76% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .text +64 B (+0.0%, 771,276 B / 1,048,576 B, total: 74% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .text +128 B (+0.0%, 325,184 B / 1,048,576 B, total: 31% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +64 B (+0.1%, 123,341 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m7

check-source-text requires macros that are used but never defined in
the tree to be listed in .wolfssl_known_macro_extras.
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.

2 participants