# ChaCha20-Poly1305 Linux Encrypt Loss Plan

Started: 2026-06-30

Goal: close the Linux `chacha20-poly1305/encrypt` losses by replacing the
current x86_64 two-pass AEAD composition with rscrypto-owned fused kernels,
starting with the short-message rows that are causing the worst damage. The
implementation target is not "match ring once"; it is to make rscrypto's
ChaCha20-Poly1305 seal path the best measured implementation in the benchmark
matrix without vendoring more assembly.

This is a performance and ownership project. Do not import another project's
ChaCha20-Poly1305 assembly. Existing external-derived AArch64 assembly remains
only until an owned replacement wins on the measured hot rows.

## Baseline

Source benchmark snapshot:

- `benchmark_results/2026-06-30/linux`
- Summary row in `benchmark_results/OVERVIEW.md`:
  `chacha20-poly1305` / `encrypt`: `0.99x` geomean across 99 rows, W/T/L
  `30/35/34`, pressure `ring` 27, `aws-lc-rs` 7.

The 99-row summary compares rscrypto against the fastest external
implementation for each platform and size. Ratio is
`fastest_external_time / rscrypto_time`; higher means rscrypto is faster.

### Loss Shape

| Platform | W/T/L | Geomean | Main pressure |
| --- | ---: | ---: | --- |
| AMD Zen4 | 0/1/10 | 0.766x | `ring` |
| AMD Zen5 | 5/1/5 | 0.847x | `ring`, `aws-lc-rs` |
| Intel Ice Lake | 0/6/5 | 0.827x | `ring` |
| Intel Sapphire Rapids | 4/2/5 | 0.850x | `ring`, `aws-lc-rs` |
| AWS Graviton3 | 1/10/0 | 1.045x | not a loss source |
| AWS Graviton4 | 1/10/0 | 1.054x | not a loss source |
| IBM Power10 | 6/1/4 | 1.009x | secondary |
| IBM z16/s390x | 10/1/0 | 1.824x | healthy |
| RISE RISC-V | 3/3/5 | 0.992x | secondary |

Worst rows:

| Platform | Size | Competitor | Ratio | rscrypto | Competitor |
| --- | ---: | --- | ---: | ---: | ---: |
| AMD Zen4 | 32 | `ring` | 0.528x | 312.59 ns | 165.12 ns |
| AMD Zen4 | 1 | `ring` | 0.532x | 297.28 ns | 158.18 ns |
| Intel Ice Lake | 32 | `ring` | 0.542x | 317.13 ns | 171.87 ns |
| AMD Zen5 | 32 | `ring` | 0.546x | 310.72 ns | 169.57 ns |
| AMD Zen5 | 64 | `ring` | 0.548x | 322.20 ns | 176.51 ns |
| Intel Sapphire Rapids | 32 | `ring` | 0.550x | 289.38 ns | 159.29 ns |
| AMD Zen5 | 1 | `ring` | 0.554x | 300.62 ns | 166.46 ns |
| Intel Ice Lake | 1 | `ring` | 0.561x | 296.18 ns | 166.05 ns |
| AMD Zen4 | 64 | `ring` | 0.567x | 299.20 ns | 169.72 ns |
| Intel Sapphire Rapids | 1 | `ring` | 0.584x | 266.84 ns | 155.82 ns |
| Intel Ice Lake | 64 | `ring` | 0.585x | 314.19 ns | 183.90 ns |
| AMD Zen5 | 256 | `ring` | 0.595x | 370.46 ns | 220.48 ns |

The loss is concentrated in x86_64 short messages: `1`, `32`, `64`, and
`256` bytes. Large rows are mostly ties or wins. Graviton is not the current
problem.

## Current Local Cause

The production x86_64 seal path composes standalone primitives:

1. `chacha20::xor_keystream(...)` encrypts the buffer in place.
2. `chacha20::poly1305_key_gen(...)` derives the Poly1305 key from block 0.
3. `poly1305::authenticate_aead(...)` authenticates AAD and the now-encrypted
   ciphertext.

This reads and processes the ciphertext after encryption. The current x86_64
Poly1305 AVX2 par4 backend is useful, but it is still a second pass. For tiny
messages, the second pass, dispatch, setup, and generic AEAD framing dominate
the actual work.

AArch64 release builds already have an integrated external-derived seal/open
assembly path for macOS/Linux non-empty buffers. That explains why Graviton is
mostly tied or ahead. The current Linux loss is not caused by missing AArch64
fusion.

## External Evidence

The relevant competitor shape is integrated AEAD, not merely better standalone
ChaCha20 or Poly1305:

- RFC 8439 defines the construction: ChaCha block 0 derives the one-time
  Poly1305 key, encryption starts at counter 1, and the tag authenticates AAD,
  ciphertext, and length block.
- AWS-LC/BoringSSL call integrated `chacha20_poly1305_seal(...)` when assembly
  is available, then fall back to separate ChaCha/tag code otherwise.
- `ring` ships dedicated ChaCha20-Poly1305 x86_64 assembly with specialized
  short-message and AVX2 paths.
- RustCrypto's `chacha20poly1305` path still has an explicit TODO to interleave
  encryption with Poly1305. That confirms the missing shape; it is not a model
  to copy for this project.

Useful references:

- RFC 8439: <https://www.rfc-editor.org/rfc/rfc8439>
- AWS-LC source: <https://docs.rs/crate/aws-lc-sys/latest/source/aws-lc/crypto/cipher_extra/e_chacha20poly1305.c>
- `ring` x86_64 source: <https://docs.rs/crate/ring/latest/source/crypto/cipher/asm/chacha20_poly1305_x86_64.pl>
- RustCrypto source: <https://docs.rs/chacha20poly1305/latest/src/chacha20poly1305/cipher.rs.html>
- Improved SIMD Implementation of Poly1305: <https://eprint.iacr.org/2019/842>
- HACL*/EverCrypt paper: <https://arxiv.org/abs/1904.04606>

## Assumptions

- Scope is Linux `chacha20-poly1305/encrypt`; decrypt is allowed to share
  internal helpers, but encrypt rows drive promotion decisions.
- The first production target is x86_64 because it owns the worst rows.
- Runtime dispatch may depend on public CPU features and public input length.
- Runtime dispatch must not depend on secret key bytes, plaintext bytes, or tag
  material.
- We can write rscrypto-owned Rust intrinsics or rscrypto-owned assembly.
- We do not vendor more assembly, generated code, or perlasm from BoringSSL,
  AWS-LC, `ring`, OpenSSL, Cloudflare, HACL*, or RustCrypto.
- Existing AArch64 external-derived integrated assembly stays until an owned
  AArch64 replacement wins or ties on measured AArch64 rows.
- Correct but slower fused candidates are rejected or kept behind `diag`; they
  are not promoted just because they are more elegant.

## Non-Goals

- Do not rewrite raw ChaCha20 before proving the AEAD-specific fused path needs
  it.
- Do not rewrite standalone Poly1305 before measuring whether AEAD fusion is
  the missing win.
- Do not add new public API surface for this work.
- Do not change RFC 8439 behavior, nonce rules, tag format, AAD padding, length
  encoding, or detached-tag semantics.
- Do not optimize benchmark harness overhead before the core kernel gap is
  closed. Competitors already pay combined-buffer append overhead and still
  win the short x86_64 rows.

## Step 1: Build The Owned x86_64 Short-Message Fused Path First

Objective: produce an rscrypto-owned short-message ChaCha20-Poly1305 seal
kernel that targets the exact loss rows: `1`, `32`, `64`, and `256` bytes.

Target input range:

- Initial gate: `1..=256` bytes.
- Optional extension: `257..=512` bytes only if measurements show a win.
- `0` bytes stays on the existing path until a dedicated empty-message fast
  path is measured. Empty-message AEAD has a different cost shape and should
  not complicate the first kernel.

Implementation shape:

- Compute ChaCha block 0 once and derive the Poly1305 one-time key.
- Generate ChaCha blocks starting at counter 1.
- XOR plaintext into ciphertext in place.
- Feed ciphertext directly into a low-overhead Poly1305 accumulator while it is
  hot.
- Avoid calling the generic `poly1305::authenticate_aead(...)` for the short
  fused path.
- Avoid heap allocation.
- Avoid storing an intermediate full keystream unless the first diagnostic
  prototype needs it to measure the shape.
- Prefer a scalar or low-setup Poly1305 accumulator for the first short path.
  AVX2 par4 setup is unlikely to pay for `1`, `32`, `64`, or `256` bytes.

Acceptable first prototype:

- A Rust-intrinsics implementation under `diag` that is correct and isolates
  the work.
- It may use stack scratch for the first measurement if that gets data quickly.
- It must not be promoted unless it removes enough setup and second-pass cost
  to beat current production on the target rows.

Production candidate requirements:

- Owned source only.
- `#[target_feature]` functions have explicit safety contracts.
- Any `unsafe` block has a numbered `// SAFETY:` comment.
- No secret-dependent branches.
- No secret-dependent table lookups.
- Tail handling is selected only by public plaintext length.
- Counter overflow behavior matches the existing bounded-length checks.

Files likely touched:

- `src/aead/chacha20poly1305.rs`
- `src/aead/chacha20poly1305/x86_64_fused.rs`
- `src/aead/mod.rs` only if exports or tests require it
- `benches/aead.rs` for `diag` rows
- `tests/chacha20poly1305.rs` or existing AEAD kernel equivalence tests

Verification:

```bash
cargo check --features 'aead,diag' --bench aead
cargo test --features 'aead,diag' --test chacha20poly1305
cargo test --features 'aead,diag' --test aead_wycheproof wycheproof_chacha20poly1305_open_vectors_match_expected_results
cargo test --features 'aead,diag' aead
```

Benchmark gates:

- Local x86_64 focused bench with `diag` rows for sizes `0`, `1`, `32`, `64`,
  `256`, `1024`.
- Linux CI matrix bench before production promotion.
- Required target-row speedup over current rscrypto:
  - `1`, `32`, `64`: at least `1.35x` on at least two x86_64 platforms, no
    x86_64 platform worse than `1.15x`.
  - `256`: at least `1.15x` on at least two x86_64 platforms, no x86_64
    platform worse than `0.98x`.
  - `1024`: no more than `2%` regression if the short path reaches that size.

Promotion decision:

- Promote only if the fastest-external W/T/L improves on the x86_64 short rows
  without creating new losses at `1024`.
- If correct but slower, keep only a small diagnostic entry if it teaches
  something measurable; otherwise delete it.

## Step 2: Put The Path In A Focused Module And Dispatch Before The Two-Pass Path

Objective: route production x86_64 short-message encrypt through the fused path
without perturbing other architectures or large-message behavior.

Module target:

```text
src/aead/chacha20poly1305/x86_64_fused.rs
```

Public-to-crate interface target:

```rust
#[cfg(target_arch = "x86_64")]
fn encrypt_in_place_fused_x86_64_short(
  key: &[u8; 32],
  nonce: &[u8; 12],
  aad: &[u8],
  buffer: &mut [u8],
) -> Option<Result<[u8; 16], SealError>>
```

The exact signature can change if the implementation needs a typed tag or
length object, but the dispatch semantics should stay the same:

- `None` means the fused path is not applicable and the caller should continue
  to the existing path.
- `Some(Ok(tag))` means encryption and authentication completed.
- `Some(Err(...))` means public length validation failed.

Dispatch order:

1. Existing public length bound check remains first.
2. AArch64 integrated release path remains before the owned composition path on
   AArch64.
3. x86_64 short fused path runs before `encrypt_in_place_owned_unchecked`.
4. Existing two-pass owned path remains the fallback.

Feature gates:

- `target_arch = "x86_64"`.
- `not(feature = "portable-only")`.
- Use existing `platform::caps()` or `platform::caps_static()` patterns.
- Start with the minimum feature set that actually wins. Do not require AVX2
  if an SSSE3/SSE2-shaped short path wins more broadly.

Diagnostic surface:

- Add forced `diag` entrypoints so benches can compare:
  - normal production dispatch;
  - forced current two-pass owned path;
  - forced short fused x86_64 path;
  - later forced AVX2 bulk fused path.

Benchmark labels:

```text
chacha20-poly1305/encrypt/rscrypto
chacha20-poly1305/encrypt/rscrypto-owned
chacha20-poly1305/encrypt/rscrypto-x86-short-fused
chacha20-poly1305/encrypt/rscrypto-x86-avx2-fused-bulk
```

Verification:

```bash
cargo check --features 'aead,diag' --bench aead
cargo test --features 'aead,diag' chacha20poly1305
cargo test --features 'aead,diag' aead_kernel_equivalence
```

Dispatch verification:

- Add tests or diagnostics proving `portable-only` never routes to the fused
  path.
- Add a diagnostic capability print or bench metadata row when useful.
- Use `cargo asm` or `cargo-show-asm` on release wrappers to confirm the short
  path does not accidentally call the generic AEAD authenticator.

Regression gates:

- Non-x86_64 public rows must be bit-identical and should not change.
- x86_64 rows outside the gated sizes must stay on the existing path until the
  bulk path is explicitly introduced.
- No new dependency.
- No new public API.

Promotion decision:

- This step is promoted only with Step 1 benchmark evidence.
- If dispatch overhead consumes the kernel win, collapse the wrapper or change
  the threshold. Do not keep ornamental dispatch layers.

## Step 3: Optimize The First Kernel For 1, 32, 64, And 256 Bytes

Objective: make the first production kernel excellent at the actual short rows,
not merely correct.

The fast path must treat the target sizes as the product surface:

- `1` byte: setup dominates. The kernel must avoid generic AEAD machinery.
- `32` bytes: worst measured row. This is the primary scorecard.
- `64` bytes: one full ChaCha message block, still setup-dominated.
- `256` bytes: first size where a small loop may matter.

Required internal measurements:

Add or reuse `diag` benches that split:

- Poly1305 key generation only.
- ChaCha block 0 plus one message block.
- Short fused seal without AAD.
- Short fused seal with benchmark AAD.
- Current two-pass seal.
- Current `poly1305::authenticate_aead(...)` alone on the post-encryption
  ciphertext.

Implementation priorities:

- Keep key schedule material in registers where practical.
- Avoid rebuilding constants inside every tiny branch if hoisting wins.
- Avoid full 8-block AVX2 setup for one-block and partial-block messages.
- Avoid AVX-512 for the short path unless measurements prove it helps. It is
  probably the wrong tool for this phase.
- Use public-length specialized tails if they reduce overhead.
- Keep stack scratch fixed-size and small if used.
- Ensure the Poly1305 length block is still processed exactly once.
- Authenticate AAD before ciphertext in RFC order, with correct 16-byte
  padding.
- Zeroize the Poly1305 key material and any stack scratch that carries key
  stream or one-time key bytes.

Correctness matrix:

- RFC 8439 known-answer tests.
- Wycheproof ChaCha20-Poly1305 open vectors.
- Existing encrypt/decrypt round trips.
- Differential tests against the existing portable path for:
  - AAD lengths `0`, `1`, `15`, `16`, `17`, `31`, `32`, `33`, benchmark AAD.
  - plaintext lengths `0..=512`, plus `1024`.
  - unaligned buffer starts if existing test infrastructure can exercise them.
  - repeated nonces only inside test vectors where expected by the harness.
- Forced-kernel tests under `diag` so the short fused path is tested even on
  machines where production dispatch would not pick it.

Side-channel checks:

- Audit public versus secret branches.
- Audit generated assembly for accidental table lookups or secret-dependent
  control flow.
- Ensure failure behavior for encrypt remains only length-error based.
- Keep decrypt out of this step unless a shared helper makes it impossible to
  avoid; decrypt has tag-verification semantics and should not be rushed into
  an encrypt-loss fix.

Benchmark gates:

- Beat current rscrypto on x86_64 `1`, `32`, `64`, and `256` byte rows.
- Convert the worst `ring` rows from losses into at least ties where possible:
  target ratio `>=0.95x` against fastest external for `1`, `32`, and `64`.
- Do not regress `1024` or larger production rows.

Decision if the kernel is correct but short rows still lose:

1. Inspect assembly for missed inlining, spills, and redundant loads.
2. Split `1..=64` from `65..=256` if one shape cannot serve both.
3. Try a narrower SSSE3/SSE2 short kernel before reaching for AVX2.
4. If Rust intrinsics cannot produce acceptable code, write a small
   rscrypto-owned assembly kernel for the short path only.
5. Delete failed shapes that add dispatch complexity without measurable wins.

## Step 4: After The Short Path Wins, Build The AVX2 Fused Bulk Path

Objective: reduce memory traffic and generic composition overhead for medium
and large x86_64 messages after the short losses are under control.

Start condition:

- Step 1 through Step 3 must already show production-quality wins on short
  x86_64 rows.
- Do not start this step while the `32` byte row is still around `0.55x`.

Target input range:

- Initial candidate: `>=1024` bytes.
- Tune thresholds for `512`, `1024`, `4096`, `16384`, `65536`, `262144`,
  `1048576`.
- Leave `1..=256` on the short fused path if it wins.

Implementation shape:

- Generate ChaCha blocks in 4-block or 8-block batches.
- XOR plaintext to ciphertext.
- Feed ciphertext blocks to Poly1305 without a separate full-buffer pass.
- Reuse the existing AVX2 ChaCha and Poly1305 implementation knowledge, but do
  not force the current standalone function boundaries if they create stores,
  reloads, or transposes that a fused AEAD kernel can avoid.
- Use Poly1305 par4 where it wins.
- Cascade tails to the short fused path or existing scalar path based on
  measurement.

Design options to benchmark:

| Candidate | Rationale | Reject if |
| --- | --- | --- |
| 4-block fused AVX2 | Matches Poly1305 par4 shape | Loses throughput at `4096+` |
| 8-block ChaCha + par4 Poly1305 | Reuses current AVX2 ChaCha batch size | Register pressure/spills erase win |
| Hybrid bulk + short tail | Keeps short kernel responsible for tail | Tail transition overhead hurts `1024` |
| Full custom assembly | Maximum scheduling control | Rust intrinsics already tie/win |

Verification:

```bash
cargo check --features 'aead,diag' --bench aead
cargo test --features 'aead,diag' chacha20poly1305
cargo test --features 'aead,diag' aead_kernel_equivalence
cargo test --features aead --test aead_wycheproof wycheproof_chacha20poly1305_open_vectors_match_expected_results
```

Benchmark gates:

- No regression in `1`, `32`, `64`, `256`.
- `1024`: remain tie or win versus fastest external.
- `4096+`: improve geomean versus current rscrypto on x86_64.
- No x86_64 platform loses more than `2%` at any size routed to the bulk path.
- Linux matrix fastest-external W/T/L improves from the baseline `30/35/34`.

Promotion decision:

- Promote only if the full Linux x86_64 matrix improves. A single-host AVX2 win
  is not enough.
- If the bulk path only helps large rows that are already wins, keep it out of
  production unless it also reduces maintenance pressure or enables deleting
  slower code.
- If AVX-512 is considered, do it after AVX2 fusion. Current evidence does not
  justify starting there.

## Step 5: Do Not Spend First Effort On AArch64 Assembly Removal

Objective: keep attention on the current loss source while preserving the
larger assembly-ownership goal.

Current state:

- AArch64 macOS/Linux release builds use external-derived integrated
  ChaCha20-Poly1305 seal/open assembly for non-empty buffers.
- Linux Graviton3 and Graviton4 are ties or small wins in the current encrypt
  summary.
- Previous owned AArch64 fused attempts lost to the imported integrated path
  and are recorded in `docs/tasks/asm.md`.

Policy for this plan:

- Do not remove or replace the AArch64 integrated assembly as part of the
  x86_64 short-fusion work.
- Do not spend paid Graviton runs on another incremental AArch64 glue attempt.
- Keep existing AArch64 production behavior unless a separate owned kernel is
  designed and measured.

Future AArch64 replacement criteria:

- New attempt must be a different shape from the rejected fused8/glue attempts.
- Candidate options:
  - generated rscrypto-owned NEON kernel from a small checked-in generator;
  - hand-scheduled rscrypto-owned NEON seal/open kernel;
  - SVE2 path if target coverage and benchmark upside justify it;
  - owned Rust NEON path only if assembly is not required to tie/win.
- Must beat or tie the imported integrated assembly on Apple Silicon and Linux
  Graviton before replacing production.
- Must update `docs/tasks/asm.md` only after measured replacement or deletion.

Verification for any future AArch64 slice:

```bash
cargo test --features 'aead,diag' chacha20poly1305
cargo test --features aead --test aead_wycheproof wycheproof_chacha20poly1305_open_vectors_match_expected_results
scripts/check/asm-ledger.sh
```

Benchmark gates:

- Graviton3 and Graviton4 encrypt/decrypt rows must not regress.
- Apple Silicon local rows must not regress.
- The owned path must be faster or effectively flat before imported assembly
  deletion is allowed.

Decision:

- For the current Linux encrypt-loss project, AArch64 is explicitly deferred.
- The only AArch64 work allowed here is ensuring x86_64 changes do not affect
  AArch64 dispatch or correctness.

## Step 6: Do Not Start From RustCrypto's Shape

Objective: avoid copying a design that has the same structural limitation we
are trying to remove.

RustCrypto is useful evidence because its current implementation documents the
missing interleave as a TODO. It is not the implementation model for this
project.

Allowed RustCrypto use:

- Compare correctness behavior and vectors.
- Review API-level semantics if an edge case is unclear.
- Use its references to Poly1305 SIMD papers as research pointers.
- Compare benchmark rows as a sanity check.

Disallowed RustCrypto use:

- Do not copy the two-pass encrypt-then-MAC structure into new rscrypto code.
- Do not use RustCrypto's generic trait shape as the internal fused-kernel
  boundary.
- Do not add abstraction layers to mimic external crates.
- Do not treat "RustCrypto also does this" as evidence that the design is fast.

Better models:

- RFC 8439 for semantic correctness.
- Existing rscrypto portable and SIMD kernels for local style and dispatch.
- `ring` and AWS-LC only for high-level strategy: integrated AEAD, short path,
  AVX2/bulk path, measured thresholds.
- Poly1305 SIMD papers for arithmetic options, not for wholesale code import.

Verification:

- Every new internal boundary must be justified by measured performance or
  correctness isolation.
- Any abstraction that cannot be explained as removing measurable complexity is
  deleted before promotion.
- Review final code for ornamental layers after benchmark gates pass.

## End-To-End Work Order

1. Baseline extraction -> verify:
   regenerate the 99-row fastest-external report from
   `benchmark_results/2026-06-30/linux` and save the command/output in the PR
   or notebook entry.

2. Diagnostic bench surface -> verify:
   `cargo check --features 'aead,diag' --bench aead` and focused bench output
   includes forced current path and forced short fused path labels.

3. Short fused prototype -> verify:
   forced-kernel differential tests pass for plaintext lengths `0..=512`, AAD
   boundary lengths, and RFC/Wycheproof vectors.

4. Short fused tuning -> verify:
   x86_64 local benchmarks show target-row wins for `1`, `32`, `64`, and
   `256`.

5. Short fused production dispatch -> verify:
   public `rscrypto` rows improve on Linux x86_64 and fallback rows are
   unchanged.

6. Linux matrix run -> verify:
   fastest-external W/T/L improves from `30/35/34`, with specific movement in
   the x86_64 short-message losses.

7. Bulk fused AVX2 prototype -> verify:
   no short-row regression and medium/large x86_64 geomean improves.

8. Bulk production dispatch -> verify:
   threshold table is based on matrix measurements, not a single host.

9. Documentation and ownership closeout -> verify:
   this task file records accepted/rejected candidates, `docs/tasks/asm.md` is
   updated only if assembly ownership changes, and `git diff --check` is clean.

## Work Log

### 2026-06-30 Diagnostic Baseline And First x86_64 Short Prototype

Baseline report tooling added:

```bash
python3 scripts/bench/aead-gap-report.py --root benchmark_results/2026-06-30/linux --algorithm chacha20-poly1305 --operation encrypt --top 16
```

Verified against the plan snapshot:

- rows: `99`
- W/T/L: `30/35/34`
- geomean: `0.990x`
- x86_64 W/T/L: `9/10/25`
- loss pressure: `ring` 27, `aws-lc-rs` 7
- worst rows match the plan table, starting with AMD Zen4 32B at `0.528x`

First diagnostic candidate added, not promoted:

- `src/aead/chacha20poly1305/x86_64_fused.rs`
- `diag_chacha20poly1305_encrypt_in_place_x86_64_short_fused(...)`
- bench label: `chacha20-poly1305/encrypt/rscrypto-x86-short-fused`
- applies only to public plaintext lengths `1..=256`
- returns `None` for `0` and `257+`, so diagnostics cannot silently fall back
- authenticates AAD first, encrypts ciphertext, then feeds ciphertext into a
  scalar Poly1305 AEAD accumulator without calling
  `poly1305::authenticate_aead(...)`
- for exactly 256 bytes, tries the existing owned x86 SSSE3+AVX four-block
  ChaCha20 XOR kernel before feeding the hot ciphertext into the scalar
  accumulator; otherwise uses scalar ChaCha blocks
- branches are only on public length and public CPU capabilities
- one-time Poly1305 key, ChaCha block 0, and scalar keystream scratch are
  zeroized

Validation run on an AArch64 macOS host:

```bash
python3 -m py_compile scripts/bench/aead-gap-report.py
cargo fmt --all --check
cargo check --features 'aead,diag' --bench aead
cargo check --features 'aead,diag,sha2' --bench aead_kernels
cargo test --features 'aead,diag' --test chacha20poly1305 -- --nocapture
cargo test --features 'aead,diag' --test aead_kernel_equivalence -- --nocapture
cargo test --features aead --test aead_wycheproof wycheproof_chacha20poly1305_open_vectors_match_expected_results -- --nocapture
cargo check --target x86_64-apple-darwin --features 'aead,diag' --test chacha20poly1305
cargo check --target x86_64-unknown-linux-gnu --no-default-features --features 'chacha20poly1305,diag' --lib
```

The full x86_64 Linux bench/test cross-check with dev-dependencies was blocked
locally because `x86_64-linux-gnu-gcc` is not installed; `alloca` and
`aws-lc-sys` build scripts need that C toolchain. The narrower x86_64 Linux
library check typechecked the new Rust path. Cross-target checks emit existing
`apple-m1`/`x87` warning noise from this host.

Promotion decision: do not route production `rscrypto` rows to this candidate
yet. It needs native x86_64 Linux benchmarks for `1`, `32`, `64`, and `256`
before any production threshold is defensible.

### 2026-06-30 GitHub x86_64 Diagnostic Bench Result

Run:

```text
https://github.com/loadingalias/rscrypto/actions/runs/28482685782
```

Inputs:

- `targets=aead_diag`
- `platforms=zen4,spr,icl,zen5`
- `filter=chacha20-poly1305/encrypt/(rscrypto|rscrypto-owned|rscrypto-x86-short-fused)/(0|1|32|64|256|1024)`
- `quick=false`

All four x86_64 Linux jobs passed at commit
`cb9c54757fec66141a26ca41704ce76380ca4e1e`.

Result: do not promote this diagnostic candidate. Compared with the fastest
external rows from the `2026-06-30` full baseline, the diagnostic short-fused
candidate was `0/0/16` W/T/L over its measured `1`, `32`, `64`, and `256` byte
rows, with `0.571x` geomean and `0.572x` median. Substituting it into the full
x86_64 baseline would keep the overall x86_64 W/T/L at `9/10/25` and slightly
reduce geomean from `0.822x` to `0.816x`.

The only useful signal is narrow overhead reduction at `1` and `32` bytes on
some x86_64 lanes, but it is still far behind `ring` and regresses larger short
rows on Zen4, Ice Lake, and Sapphire Rapids. Stop investing in this Rust
scaffold; the next candidate needs a real fused x86_64 path that removes the
scalar Poly1305 bottleneck and avoids the extra pass shape.

### 2026-06-30 Full x86_64 Linux Assembly Candidate

Implemented the next candidate as a real integrated Linux x86_64 seal backend,
not a Rust fusion around scalar Poly1305:

- added generated AWS-LC/BoringSSL-derived x86_64 assembly under
  `src/aead/chacha20poly1305/asm/rscrypto_chacha20_poly1305_x86_64_linux.s`
- added the Rust ABI wrapper in
  `src/aead/chacha20poly1305/x86_64_asm.rs`
- production seal dispatch is Linux x86_64 only and gated on `AVX2 | BMI2`,
  `not(debug_assertions)`, and `not(feature = "portable-only")`
- decrypt/open routing remains unchanged
- empty plaintext still falls back to the existing owned path
- removed the failed `x86_64_fused.rs` diagnostic scaffold and its scalar
  `AeadScalar` helper
- replaced the diagnostic bench row with
  `chacha20-poly1305/encrypt/rscrypto-x86-asm`

Validation on an AArch64 macOS host:

```bash
cargo fmt --all --check
cargo check --features 'aead,diag' --bench aead
cargo test --features 'aead,diag' --test chacha20poly1305
cargo check --target x86_64-unknown-linux-gnu --no-default-features --features 'std,chacha20poly1305,diag' --lib
cargo check --release --target x86_64-unknown-linux-gnu --no-default-features --features 'std,chacha20poly1305' --lib
cargo check --target x86_64-unknown-linux-gnu --no-default-features --features 'std,aead,diag' --lib
cargo check --release --target x86_64-unknown-linux-gnu --no-default-features --features 'std,aead' --lib
RUSTFLAGS='-C target-cpu=x86-64' cargo build --target x86_64-unknown-linux-gnu --no-default-features --features 'std,chacha20poly1305,diag' --lib
RUSTFLAGS='-C target-cpu=x86-64' cargo build --release --target x86_64-unknown-linux-gnu --no-default-features --features 'std,chacha20poly1305' --lib
RUSTFLAGS='-C target-cpu=x86-64' cargo build --release --target x86_64-unknown-linux-gnu --no-default-features --features 'std,aead' --lib
just check
```

The x86_64 Linux integration test target is still blocked locally by the missing
`x86_64-linux-gnu-gcc` cross C toolchain required by dev-dependency build
scripts. The library checks above parse and typecheck the `global_asm!` backend
for the Linux x86_64 target. The explicit `RUSTFLAGS` build commands are needed
on this AArch64 host because the local default `-C target-cpu=native` resolves
to `apple-m1`, which is not a valid x86_64 codegen CPU.

Promotion decision: benchmark this candidate on GitHub before any broader claim.
It is the first candidate that structurally attacks both the Poly1305 pass and
ChaCha pass overhead, but W/T/L movement is unproven until native Linux bench
artifacts land.

### 2026-07-01 x86_64 Assembly Promotion Bench Result

Run:

```text
https://github.com/loadingalias/rscrypto/actions/runs/28487281083
```

Inputs:

- `targets=aead_diag`
- `platforms=zen4,spr,icl,zen5`
- `filter=chacha20-poly1305/encrypt/(rscrypto|rscrypto-owned|rscrypto-x86-asm)/(1|32|64|256|1024|4096|16384)`
- `quick=false`

Result: promote the x86_64 assembly backend selectively, not globally. The
full x86_64 projection moved from `9/10/25` W/T/L at `0.822x` geomean to a
modeled `9/24/11` at `0.995x` geomean when production dispatch follows the
measured CPU/size policy below. The naive global assembly route would have
improved geomean to `0.973x`, but it introduced avoidable large-message
regressions on Sapphire Rapids and Zen5.

Measured subset summary:

| Route | W/T/L | Geomean | Median |
| --- | ---: | ---: | ---: |
| Old production subset | `3/7/18` | `0.735x` | `0.668x` |
| Global x86_64 assembly production | `0/21/7` | `0.967x` | `0.993x` |
| Forced diagnostic x86_64 assembly | `0/19/9` | `0.960x` | `0.991x` |
| Owned fallback | `2/5/21` | `0.713x` | `0.635x` |

Promoted production policy:

| CPU lane | x86_64 assembly route | Reason |
| --- | --- | --- |
| Sapphire Rapids | `1..=256` bytes | fixes the short-message shape, regresses beyond that |
| AMD Zen5 | `1..=1024` bytes | holds short/mid rows, loses the large rows |
| AMD Zen4 | all sampled non-empty sizes | measured as ties/wins across `1..=16384` |
| Intel Ice Lake | all sampled non-empty sizes | measured as ties across `1..=16384` |
| Other AVX2+BMI2 x86_64 Linux CPUs | all non-empty sizes | best current default until per-CPU data says otherwise |
| Empty plaintext | existing owned path | assembly path is not measured or needed for setup-only empty AEAD |

Safety and side-channel decision:

- Dispatch uses only public CPU capability bits and public plaintext length.
- Forced diagnostic `rscrypto-x86-asm` remains unrestricted so future bench
  runs can keep measuring the backend itself instead of the production policy.
- The production `rscrypto` row uses the measured policy above; it must not
  route Sapphire Rapids or Zen5 large rows through the assembly path unless a
  later native Linux run changes those thresholds.

## Required Benchmark Commands

Focused local development:

```bash
cargo bench --features 'aead,diag' --bench aead -- 'chacha20-poly1305/encrypt/(rscrypto|rscrypto-owned|rscrypto-x86-asm)/(1|32|64|256|1024|4096|16384)' --warm-up-time 1 --measurement-time 2 --sample-size 20 --noplot
```

Production comparison:

```bash
just bench crate=aead bench=chacha20-poly1305/encrypt
```

When CI artifacts are available, extract and compare:

```bash
python3 scripts/bench/aead-gap-report.py --root benchmark_results/<date>/linux --algorithm chacha20-poly1305 --operation encrypt
```

`scripts/bench/aead-gap-report.py` does not exist at plan start. Add it before
the first production-routing PR, modeled after `scripts/bench/blake3-gap-report.py`.
It must compute the same fastest-external 99-row summary used by the baseline,
including W/T/L, geomean, per-platform breakdown, per-size breakdown, pressure,
and worst rows.

## Required Correctness Commands

```bash
cargo check --features 'aead,diag' --bench aead
cargo test --features 'aead,diag' chacha20poly1305
cargo test --features 'aead,diag' aead_kernel_equivalence
cargo test --features aead --test chacha20poly1305
cargo test --features aead --test aead_wycheproof wycheproof_chacha20poly1305_open_vectors_match_expected_results
just check
```

Run broader validation before final merge:

```bash
just test --all
just check-all
```

Miri is useful for portable/shared logic. It is not expected to execute x86_64
target-feature intrinsics:

```bash
just test-miri
```

## Constant-Time And Safety Requirements

- Branches on public length and public CPU capability are allowed.
- Branches on key bytes, plaintext bytes, ciphertext bytes, Poly1305
  accumulator limbs because of secret data, or tag bytes are not allowed.
- Table lookups indexed by secret data are not allowed.
- `unsafe` must be isolated to intrinsic loads/stores, target-feature calls, or
  assembly ABI boundaries.
- Every `unsafe` block must have a numbered `// SAFETY:` comment.
- Every `#[target_feature]` function must have a wrapper that proves the CPU
  feature gate.
- In-place aliasing rules must be documented for any assembly or pointer-heavy
  intrinsic code.
- Stack scratch containing one-time Poly1305 key, keystream, or derived state
  must be zeroized if it survives beyond immediate register use.
- Decrypt/open behavior must remain fail-closed and zeroize plaintext on
  verification failure. Do not refactor decrypt as part of an encrypt win
  unless tests and side-channel review cover it fully.

## Threshold Policy

Thresholds must be data-backed.

Initial expected thresholds:

| Range | Expected route | Reason |
| --- | --- | --- |
| `0` | existing path or measured empty fast path | empty AEAD has distinct setup-only shape |
| `1..=64` | short fused scalar/SSSE3-style path | worst measured rows |
| `65..=256` | short fused path | still setup-dominated |
| `257..=512` | measured | include only if it wins |
| `>=1024` | existing path until AVX2 bulk fusion wins | current large rows are mostly healthy |

Do not ship a threshold because it feels plausible. Ship it because the Linux
matrix says it improves fastest-external W/T/L without hiding regressions.

## Rejection Criteria

Reject or delete a candidate when any of these are true:

- It is correct but slower than current rscrypto on the target rows.
- It wins one platform and creates new losses on another x86_64 platform.
- It requires AVX-512 to improve rows that AVX2 or SSSE3 can handle.
- It adds a new abstraction layer that does not remove measurable complexity.
- It only improves rows where rscrypto already wins and does not help the loss
  count.
- It depends on vendored assembly or generated source from another project.
- It makes AArch64 behavior worse while trying to fix x86_64.

## Done Criteria

This task is complete when all of these are true:

- Linux `chacha20-poly1305/encrypt` fastest-external W/T/L improves materially
  from `30/35/34`.
- The worst x86_64 short-message rows move from hard losses toward ties or
  wins.
- No AArch64, s390x, POWER, or RISC-V production regression is introduced.
- The promoted code is rscrypto-owned.
- Correctness tests, Wycheproof vectors, forced-kernel differential tests, and
  `just check` pass.
- Benchmark evidence records exact platform, command, commit, and before/after
  rows.
- `docs/tasks/asm.md` is updated only if assembly inventory or ownership
  changes.
- Failed candidates are either deleted or clearly recorded here with measured
  reasons.
