Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .claude/CHACHA20_MATRYOSHKA_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ChaCha20 SIMD via the `U32x16` ARX lane — matryoshka usage + open work

> Handoff doc. What this branch shipped, what's deferred, and the **matryoshka**
> plan for *using* the `U32x16` lane to accelerate ChaCha20 without owning any
> crypto algorithm. Operator-ratified design (2026-07-11 session).

## The doctrine (why this shape)

- **RustCrypto owns the algorithm; ndarray owns the SIMD.** We do NOT hand-roll
ChaCha20/Poly1305/the XChaCha20-Poly1305 AEAD. Rolling your own AEAD (HChaCha20
+ Poly1305 + framing) is the footgun; it is forbidden.
- The `encryption` crate (`crates/encryption`, RustCrypto wrappers) stays
**unchanged**. See its `aead.rs` module doc for why the AEAD is not re-wired.
- The only crypto-algorithm SIMD we accelerate is the **ChaCha20 keystream ARX
core**, and even that is done by feeding RustCrypto's *own* backend a new
SIMD lane — never by re-implementing the cipher.

## DONE on this branch (the lane is ready)

`ndarray::simd::U32x16` now carries the full ARX triple — `Add` + `BitXor` +
`rotate_left` — on every tier, so a ChaCha20 backend can ride it:

| tier | `U32x16` backing | `rotate_left` | verified |
|---|---|---|---|
| avx512 (server) | `__m512i` | `_mm512_rolv_epi32` (VPROLVD) | executed, parity-green |
| wasm128 (browser) | `[U32x4; 4]` (`v128`) | `v128_or(u32x4_shl, u32x4_shr)` | full-lib wasm build + **node parity** |
| avx2 | `[u32; 16]` polyfill (native 2×`__m256i` = deferred TD-SIMD-3) | `u32::rotate_left` loop | compiles; == reference |
| scalar / nightly | `[u32; 16]` / `core::simd` | `u32::rotate_left` / shift-or | completeness tier + reference |

CI: `wasm_simd` job (`.github/workflows/ci.yaml` + `scripts/wasm-parity.sh` +
`crates/wasm-simd-parity/`) builds the real `ndarray::simd` wasm types and runs a
lane-by-lane parity selfcheck under **node** — the standing guard for the wasm
tier (invisible to the x86 `cargo test`). Extend its per-lane blocks as lanes land.

The stand-alone `src/simd_crypto.rs` (`chacha20_block`/`chacha20_keystream`,
scalar+AVX-512+wasm128, RustCrypto-parity-proven) is the *interim* primitive; the
matryoshka below **supersedes it** (RustCrypto supplies the rounds, so the
from-scratch cipher retires once the fork lands).

## DEFERRED — TO-DO (next session)

1. **Native neon `U32x16 = [U32x4; 4]`.** `simd_neon.rs` has native
`U32x4(uint32x4_t)` with add/sub/min/max; add `bitxor` (`veorq_u32`) +
`rotate_left` (shift-or via `vshlq_u32` with variable count vectors, `n%32`
guard), then compose `U32x16([U32x4; 4])` with `impl Add`/`impl BitXor`/
`rotate_left` (fan over 4 lanes, mirror the wasm shape in `simd_wasm.rs`), fix
`F32x16::to_bits/from_bits` to `from_array`/`to_array`, and swap the `simd.rs`
aarch64 arm's scalar `U32x16` re-export for the native one.
2. **aarch64 cross parity CI.** Generalize `wasm-simd-parity` into a shared
`simd-parity` harness (or add a sibling) run under **qemu** via `cross` in a
new `neon_simd` CI job — same selfcheck, closing NEON's identical x86-suite
blind spot (its `F32x16`/`I8x16` are unverified in CI today too).
3. **avx2 native `U32x16`** (2×`__m256i`) — the TD-SIMD-3 lowering; optional, the
scalar polyfill is correct meanwhile.

## The MATRYOSHKA — how the lane gets USED (the finalization)

Goal: ChaCha20 (and thus the whole `encryption` AEAD stack) accelerated on
server (AVX-512) + browser (wasm128), with **essentially zero owned crypto code**
and a **one-file** delta over RustCrypto that is trivial to re-sync on security
updates.

**Structure (nesting):**

```
RustCrypto chacha20 (rounds / constants / counter / StreamBackend / TESTS — VERBATIM, vetted)
└─ one backend's round body expressed over → ndarray::simd::U32x16 (Add / BitXor / rotate_left — generic, NOT crypto)
└─ dispatched by ndarray's polyfill → avx512 / wasm128 / neon / scalar
```

**Steps:**

1. **Fork `chacha20`** (the RustCrypto stream-cipher crate; `AdaWorldAPI/chacha20`,
or vendor). Its backends live in `src/backends/{soft,sse2,avx2,neon}.rs`, each a
`struct Backend<R>` impl of `StreamBackend` with `gen_ks_block` /
`gen_par_ks_blocks`. **Clone `avx2.rs` once**; keep soft/sse2/avx2/neon
untouched.
2. **Rewire the clone over `ndarray::simd::U32x16`** — the word-sliced ChaCha
round (`state[a] = state[a] + state[b]; state[d] = (state[d] ^ state[a]).rotate_left(16);`
…) written against `U32x16`, `ParBlocksSize = U16`. `ndarray` lowers it to
AVX-512 (server) / wasm128 (browser) / NEON / scalar automatically. The
carried file has **no `unsafe`, no intrinsics** — all of that lives once in
`ndarray::simd` (audited once; only AMX+F16 are byte-asm, neither on this path).
3. **Dispatch:** add the generic backend to the fork's `lib.rs` selection.
Per the ndarray model this is **compile-time** (`cfg(target_feature)`), not
`is_x86_feature_detected!` — server built `x86-64-v4`, browser built `+simd128`.
Non-portable per-target binaries by design (SIGILL/validation-fail on a CPU
built-for-but-absent) — matches how the servers/browsers are actually built.
4. **`[patch]` the fork in** (the pattern already used in MedCare-rs for
`encryption`): `chacha20poly1305 → encryption → ogar-encryption → consumers`
all accelerate **transitively**, with **zero** change to the AEAD or any
consumer, because the AEAD just calls `apply_keystream` which now dispatches
to the new backend. HChaCha20 + Poly1305 + framing stay 100% RustCrypto.
5. **Gate bit-exact vs RustCrypto's own `soft` backend** + run its stock test
vectors — the same "reference + KAT, then vectorize with parity" discipline.
6. **Retire `src/simd_crypto.rs`** — the rounds now come from RustCrypto; only
the `U32x16` lane (not crypto) is ours.

**Maintenance win:** a RustCrypto security advisory almost never touches the ARX
lane ops (fixes land in framing/counter/AEAD — all pristine upstream), so the
one-file delta re-applies with ~zero conflict: bump the vendored `chacha20`,
re-apply the one backend file, run the stock vectors + the soft parity. And if
the backend is ever **upstreamed** (RustCrypto would plausibly take an avx512 /
wasm128 backend), the fork evaporates → maintenance goes to zero. AMX has no
ChaCha/Poly backend (it is a matrix engine); the upstream targets are avx512 +
wasm128 (NEON already exists upstream).
21 changes: 21 additions & 0 deletions .claude/blackboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,24 @@ index was transiently stale this session (returned empty for
`simd_int_ops.rs`, `vnni_gemm.rs`, `bf16_gemm_f32`); Bash ground-truth
confirmed all present. Orchestrator should `cargo fmt`/`clippy`/`test`
centrally (edits were edit-only, no compile performed here).

---

## 2026-07-11 — U32x16 ARX lane + ChaCha20 matryoshka (see .claude/CHACHA20_MATRYOSHKA_PLAN.md)

**DONE:** `ndarray::simd::U32x16` full ARX triple (Add/BitXor/**rotate_left**) on
every tier — avx512 native `_mm512_rolv_epi32`, native wasm `[U32x4;4]`, avx2/
scalar/nightly. Node-run wasm parity CI gate (`wasm_simd` job + `scripts/wasm-parity.sh`
+ `crates/wasm-simd-parity/`, workspace-excluded, tests the real types, no drift).
Interim `src/simd_crypto.rs` (chacha20 scalar+avx512+wasm128, RustCrypto-parity-proven)
is superseded by the matryoshka once the fork lands.

**TO-DO (deferred, token limit):**
1. Native neon `U32x16 = [U32x4;4]` (extend `U32x4(uint32x4_t)` w/ xor+rotl, compose,
fix F32x16 to_bits, swap simd.rs aarch64 arm).
2. aarch64 cross (qemu) parity CI job — generalize `wasm-simd-parity` to a shared
`simd-parity` harness; closes NEON's x86-suite blind spot.
3. avx2 native `U32x16` (2×__m256i, TD-SIMD-3) — optional.
4. **Matryoshka execution:** fork `chacha20`, clone `avx2.rs` backend → rewire over
`ndarray::simd::U32x16`, `[patch]` into the encryption stack (transitive accel),
gate vs RustCrypto `soft`, retire `simd_crypto.rs`. Full plan in the doc above.
24 changes: 24 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,29 @@ jobs:
# crates that have no business existing in the nostd build.
cargo rustc -p ndarray "--target=${{ matrix.target }}" --no-default-features --features portable-atomic-critical-section

wasm_simd:
# The x86 `cargo test` suite never touches `simd_wasm.rs` (it is cfg'd to
# wasm32), so wasm SIMD correctness is otherwise unverified. Build the real
# `ndarray::simd` wasm types (+simd128) via the excluded `wasm-simd-parity`
# cdylib and run its numeric selfcheck under node — both integration (the
# lib compiles for wasm32+simd128) and parity (each lane == scalar). The
# standing version of the one-off node check from commit 500d57e6.
runs-on: ubuntu-latest
name: wasm-simd/parity-node
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
# rust-toolchain.toml pins 1.95.0 — install the wasm target for the pinned
# toolchain too (dtolnay installs for `stable`, which may differ).
- run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: wasm SIMD parity (build + node run)
run: ./scripts/wasm-parity.sh
Comment on lines +115 to +136

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Harden the new job’s GitHub token permissions.

Line 125 enables checkout credential persistence, and the job inherits broad default permissions. A compromised build script could read the persisted token; this job only needs read access to repository contents. Set persist-credentials: false and grant only contents: read.

Proposed CI hardening
   wasm_simd:
+    permissions:
+      contents: read
     steps:
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
wasm_simd:
# The x86 `cargo test` suite never touches `simd_wasm.rs` (it is cfg'd to
# wasm32), so wasm SIMD correctness is otherwise unverified. Build the real
# `ndarray::simd` wasm types (+simd128) via the excluded `wasm-simd-parity`
# cdylib and run its numeric selfcheck under node — both integration (the
# lib compiles for wasm32+simd128) and parity (each lane == scalar). The
# standing version of the one-off node check from commit 500d57e6.
runs-on: ubuntu-latest
name: wasm-simd/parity-node
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
# rust-toolchain.toml pins 1.95.0 — install the wasm target for the pinned
# toolchain too (dtolnay installs for `stable`, which may differ).
- run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: wasm SIMD parity (build + node run)
run: ./scripts/wasm-parity.sh
wasm_simd:
permissions:
contents: read
# The x86 `cargo test` suite never touches `simd_wasm.rs` (it is cfg'd to
# wasm32), so wasm SIMD correctness is otherwise unverified. Build the real
# `ndarray::simd` wasm types (+simd128) via the excluded `wasm-simd-parity`
# cdylib and run its numeric selfcheck under node — both integration (the
# lib compiles for wasm32+simd128) and parity (each lane == scalar). The
# standing version of the one-off node check from commit 500d57e6.
runs-on: ubuntu-latest
name: wasm-simd/parity-node
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
# rust-toolchain.toml pins 1.95.0 — install the wasm target for the pinned
# toolchain too (dtolnay installs for `stable`, which may differ).
- run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: wasm SIMD parity (build + node run)
run: ./scripts/wasm-parity.sh
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 125-125: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 115-136: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yaml around lines 115 - 136, Harden the wasm_simd job
by setting job-level permissions to contents: read and disabling credential
persistence on its actions/checkout step with persist-credentials: false. Keep
the existing checkout, toolchain, Node setup, and parity script steps unchanged.

Source: Linters/SAST tools


tests:
runs-on: ubuntu-latest
needs: pass-msrv
Expand Down Expand Up @@ -335,6 +358,7 @@ jobs:
- clippy
- format
- nostd
- wasm_simd
- tests
- native-backend
- hpc-stream-parallel
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ approx = { workspace = true, default-features = true }
itertools = { workspace = true }
ndarray-gen = { workspace = true }
criterion = { version = "0.5", features = ["html_reports"] }
# The vetted RustCrypto ChaCha20 stream cipher — used ONLY as the independent
# oracle for the `chacha20_rustcrypto_parity` integration test that pins
# `ndarray::simd::chacha20_keystream` byte-for-byte against a trusted reference.
# Same version chacha20poly1305 0.10 pulls transitively (see Cargo.lock).
chacha20 = "0.9"

[[bench]]
name = "append"
Expand Down Expand Up @@ -402,7 +407,7 @@ members = [
"ndarray-rand",
"crates/*",
]
exclude = ["crates/burn"]
exclude = ["crates/burn", "crates/wasm-simd-parity"]

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep the excluded parity crate under format and Clippy coverage.

Excluding crates/wasm-simd-parity removes its Rust source from workspace-wide checks, while the new script only runs cargo build. Therefore, the required cargo fmt and cargo clippy -- -D warnings checks do not cover this crate.

Either keep it in the workspace or add standalone format and Clippy steps to the wasm_simd job.

As per coding guidelines, Rust code must be formatted with cargo fmt and pass cargo clippy -- -D warnings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` at line 410, Update the workspace exclusion change so
crates/wasm-simd-parity remains covered by cargo fmt and cargo clippy -- -D
warnings: either remove it from the exclude list or add equivalent standalone
format and Clippy checks to the wasm_simd job. Preserve the existing wasm_simd
build behavior.

Source: Coding guidelines

default-members = [
".",
"ndarray-rand",
Expand Down
13 changes: 13 additions & 0 deletions crates/encryption/src/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
//! is no counter state to persist or synchronise between a browser tab
//! and a server. Callers normally use [`crate::envelope`], which manages
//! nonce generation and layout; this module is the raw primitive.
//!
//! ## Deliberately NOT hand-composed from `ndarray::simd::chacha20_keystream`
//!
//! `ndarray` now ships a hardware-accelerated ChaCha20 keystream
//! (`ndarray::simd::chacha20_keystream`, AVX-512 / wasm128, byte-parity-proven
//! against RustCrypto). It is tempting to swap this AEAD's keystream to that
//! primitive for speed — **do not.** XChaCha20-Poly1305 is not just a keystream:
//! it is HChaCha20 subkey derivation + the Poly1305 one-time-key + MAC framing +
//! the AAD/length encoding. Re-wiring only the keystream means re-implementing
//! that authenticated composition by hand, which is exactly the "roll your own
//! AEAD" footgun the stack forbids. The vetted RustCrypto `XChaCha20Poly1305`
//! construction stays here. The accelerated primitive is for *raw-stream* use
//! sites whose caller already owns a vetted MAC/framing — not this module.

use chacha20poly1305::aead::{Aead, KeyInit, Payload};
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
Expand Down
Loading
Loading