AES-GCM seal/open scatter paths redundantly zero-initialize the full GCM128_CONTEXT on every call (~2.7% seal throughput at QUIC packet sizes)
- Dominant language
- Assembly
- Stars
- 830
- Forks
- 212
- Avg merge
- 2d 22h
- Merged PRs (30d)
- 61
Description
## Summary
`aead_aes_gcm_seal_scatter_impl` and `aead_aes_gcm_open_gather_impl` (`crypto/fipsmodule/cipher/e_aes.c`) do the following on **every** AEAD call:
```c
GCM128_CONTEXT gcm;
OPENSSL_memset(&gcm, 0, sizeof(gcm)); // ~360 bytes
OPENSSL_memcpy(&gcm.gcm_key, &gcm_ctx->gcm_key, sizeof(gcm.gcm_key)); // ~290 bytes
CRYPTO_gcm128_setiv(&gcm, key, nonce, nonce_len);
```
The full-struct memset appears to be redundant: `CRYPTO_gcm128_setiv` already initializes all per-message state (`Yi`, `Xi`, `len.aad`, `len.msg`, `ares`, `mres`, and `EK0` via the block call), and the only remaining field, `EKi`, is scratch keystream that is always written before being read (its reads are guarded by `mres`/`ares`, which `setiv` sets to 0). `gcm_key` is overwritten by the memcpy on the next line.
That is ~650 bytes of redundant memory traffic per sealed/opened message, which is measurable for small-message workloads — e.g. QUIC, where every packet is one seal/open and internet-MTU packets are ~1200-1450 bytes.
## Measurements
AES-128-GCM `EVP_AEAD_CTX_seal_scatter` via aws-lc-rs `seal_in_place_separate_tag`, Apple M1 Max, `aws-lc-sys` 0.42.0 symbol prefix, interleaved A/B (baseline vs memset removed), medians:
| message size | before | after | delta |
|---|---|---|---|
| 1200 B | 5.59 GB/s | 5.74 GB/s | **+2.7%** (3/3 pairs) |
| 1450 B | 5.44 GB/s | 5.59 GB/s | **+2.7%** (6/6 pairs) |
| 8950 B | 8.05 GB/s | 8.03 GB/s | ~0 (fixed cost amortized) |
The delta pattern (visible at small sizes, gone at large) matches a fixed per-call cost of roughly 7 ns. The open/gather path has the identical structure and should see the same benefit.
Context for why we noticed: profiling s2n-quic on Linux shows AES-GCM at 46-48% of endpoint CPU, so per-call overheads in this path are material for QUIC fleets running at ~1500 MTU.
## Caveats
- Numbers above are from an arm64 (M1) machine; we have not yet validated on Graviton or x86_64 (including the `gcm_setiv_avx512` early-return path, which is upstream of the removed memset and unaffected structurally).
- If the zero-init is intentionally defensive (e.g. for MSAN cleanliness or as a hedge against future fields being read before write), a narrower fix would be to memset only `EKi`, which still avoids most of the per-call cost.
All aws-lc-rs AEAD KATs and downstream s2n-quic crypto tests pass with the memset removed. Happy to send a PR — the change is ~4 lines plus comments in the two call sites.
cc @andrewhop
Contributor guide
Research direction
Read crypto/fipsmodule/cipher/e_aes.c at aead_aes_gcm_seal_scatter_impl and aead_aes_gcm_open_gather_impl, then inspect CRYPTO_gcm128_setiv and the GCM128_CONTEXT fields it initializes. Run the existing AES-GCM AEAD KATs and downstream s2n-quic crypto tests. Done means the redundant per-call initialization is removed or narrowed without changing behavior or test results.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- cryptography, performance
- Issue type
- Refactor
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100