Skip to content

crypto: accelerate OpenSSL GHASH on C906 - #527

Merged
dormancygrace merged 9 commits into
mainfrom
codex/openssl-ghash
Oct 27, 2026
Merged

dormancygrace merged 9 commits into
mainfrom
codex/openssl-ghash

Conversation

@dormancygrace

@dormancygrace dormancygrace commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Add opt-in C906 GHASH using the shared BSD-3-Clause five-block generator. Four vector lanes plus a scalar block process the prefix; the remaining 0–4 whole blocks reuse the expanded scalar key. OpenSSL retains AAD, padding, lengths, tags and operation boundaries.

Dispatch requires at least 3072 B, NANOKVM_OPENSSL_GHASH=auto, the startup all-CPU XTheadVector probe, and current-thread vector permission. This does not make all OpenSSL GHASH constant-time: single-block gmult (including tag work), short inputs, processes without the opt-in and unavailable vector paths retain the data-indexed 4-bit tables. Eligible complete calls, including their scalar remainder, use the C906 arithmetic. The downloader first requests ChaCha, so GHASH acceleration applies on its second generic-cipher attempt.

Each eligible call rebuilds the key (four scalar multiplies and the 576-byte vector table) and queries thread permission; in GHASH_CHUNK processing this repeats per 3 KiB. Context sizes remain 448 B for GCM and 984 B for the AES-GCM provider, without a persistent expanded-key cache.

Historical measurements from the pinned pre-cleanup library: 16 KiB GHASH 42.79 versus 28.55 MiB/CPU-s; full software AES-128/256-GCM opening improves 10.0%/8.2%, or 3.4%/3.7% with AES DMA already enabled. At 1400 B, AES-128 open / AES-256 seal software-only medians differ by −2.0%/−1.4%, within the recorded trial spread; no material small-record regression was established. No new GHASH throughput or HTTPS gain is claimed for the scalar-tail revision.

Current validation:

  • Fast tests apply all six patches with zero fuzz to the SHA-256-verified pinned OpenSSL archive and use its real generated headers.
  • 3520 independent GHASH arithmetic cases cover 40 keys, 11 lengths, 8 offsets, initial Xi, all remainder sizes and guards. QEMU runs explicitly translated RVV 1.0 operations, with five negative controls; original XTheadVector encodings are verified separately against GNU binutils.
  • The same 3520 cases pass on the physical C906 with the original vector instructions and real thread gate.
  • A clean C906/O3 build preserves all 5808 public exports. Generic, vector/software-AES and vector/DMA profiles each pass 972 AES, 408 AEAD, 2160 GCM, 288 CTR, 20,736 tail/output/IV and 864 SHA checks. Another 28 EVP SHA-224/256 continuation answers match Python hashlib with DMA on/off.
  • Dispatcher sanitizer/negative controls and downloader race tests pass. scalar-tail-validation.json records exact patch, library and helper identities.

The previously recorded OpenSSH seccomp and TLS download qualifications retain their original identities; they were not repeated for this change. Stacked on AES #511 and SHA-256 #508; SHA-1/DES/TDES/SM3/SM4 offloads and a combined-record kernel GCM API are excluded.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Build outputs

Compared with the build of main at 6c78048; codex/openssl-aes is at 1db3b62, so outputs changed since then are listed too.

4 changed, 1 added, 0 removed of 595 outputs.

directory files
changed server server/NanoKVM-Server
changed tools tools/openssl/usr/lib/libcrypto.so.3
tools/openssl/usr/lib/ossl-modules/legacy.so
tools/openssl/usr/share/nanokvm/openssl/source.json
added tools tools/openssl/usr/share/licenses/nanokvm-openssl/C906-BSD-3-Clause.txt
Hashes: outputs-sha256 artifact of this run.

@dormancygrace dormancygrace left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed at ce4fbf5. I found no correctness defect.

What I checked

  • ABI: 144/208-byte frames are 16-byte aligned; ra and s0–s11 are saved.
  • Key buffer: the 896-byte kp (320 + 576) is filled exactly, and entry 8 = {H[0], H[1]} matches gcm_init_4bit.
  • Length guard: done != len keeps gcm_ghash_4bit (a do-while loop) from getting a zero length.
  • Multiplier: every mul operand is masked below 2^32, so the C906 multiplier stays on its fixed short path.
  • Dispatch: the same vendor-key-11 / secure_getenv / per-call prctl scheme as ChaCha. native-path-results.json shows hits only with the opt-in, and none under generic, vectors off or seccomp.
  • imageCurlEnv(): keeps operator overrides for both variables.
  • Data: I recomputed the 320 medians from native-results.jsonl and found no mismatch. GHASH at 16 KiB goes 28.55 → 42.79 MiB/s.
  • End to end: I ran the real gcm_c906.c and the generated assembly against a bit-serial SP800-38D GHASH:
    • 40 keys including all-ones and H=1, lengths 3072–16400, offsets 0–7: 1980/1980 pass.
    • Two deliberately broken variants fail.
    • Caveat: QEMU has no XTheadVector, so I mapped the th.v* words to RVV 1.0. This checks the logic, not the 0.7.1 encodings; those rest on the generator's binutils check.

Requests

  1. Where it runs. curlImageArgs(..., true) pins ChaCha on the first attempt, so this path runs only in the second, generic attempt. With the opt-in default off, most GHASH work also stays on the 4-bit tables:

    • every gmult (tags, single blocks);
    • every input under 3072 B (handshake, SSH and SRTP records, and small TLS records);
    • every process without the opt-in;
    • the len % 80 leftover after each vector prefix.

    Please state in the README and the PR description that GHASH stays table-based (not constant-time) outside this path. A cheap improvement: kp is already expanded in vector_prefix, so the leftover blocks could go through ossl_ghash_blocks_c906 before the cleanse instead of gcm_ghash_4bit. The scalar C906 GHASH (no vector unit, no prctl) could also become a constant-time default. Per the go-crypto-c906 README it runs at about the speed of the 4-bit code, but that is a kernel figure, not one measured inside OpenSSL.

  2. CI. generate-ghash.py --check runs the Python model and checks encodings, and test-dispatch.py builds the glue for x86 with stub assembly. Nothing checks the key built from the 4-bit table, the Xi byte swaps or the 320/576 layout. verify-path.c counts entries with one all-zero key and never checks output. A harness like mine, run under qemu with the generated file, would close this. I can share it.

  3. Expectation. The gain is 1.5×, not the ~3× I projected on #511: OpenSSL's 4-bit baseline (28.5 MiB/s) is about twice the kernel's generic figure I assumed. Software GCM open improves +10.0% / +8.2% (AES-128/256), and with AES DMA +3.4% / +3.7%. The README table has no small-record row. native-medians.csv shows 1400 B at −2.0% / −1.4% (AES-128 open / AES-256 seal), within the trial spread. Please add that row and say there is no small-record regression.

  4. Nit. The key is rebuilt on every eligible call, which with GHASH_CHUNK means once per 3 KiB: 4 multiplies, a 576-byte table and one prctl. This is probably why 3072 B reaches 39.7 MiB/s against 42.8 at 16 KiB. It is a reasonable trade for no context growth, but please document it.

@dormancygrace

Copy link
Copy Markdown
Owner Author

Proposed change for the leftover blocks (request 1 of my review), tested.

vector_prefix already has the scalar key for H in kp, so the len % 80 remainder can go through ossl_ghash_blocks_c906 before the cleanse. For lengths that are multiples of 16 (every GHASH caller), the 4-bit table is then not used inside an eligible call. Please regenerate 0010-ghash-c906.patch from this diff of crypto/modes/gcm_c906.c:

--- gcm_c906.c	2026-10-27 11:39:30.579465213 +0200
+++ gcm_tail.c	2026-10-27 12:09:10.396073109 +0200
@@ -60,7 +60,7 @@
 {
     u64 kp[112], powers[5][2], acc[2];
     unsigned int pos = 320, i, q, j, lane;
-    size_t groups = len / 80;
+    size_t groups = len / 80, blocks = len % 80 / 16;
 
     u64 h0 = table[8].hi, h1 = table[8].lo, carry = h0 >> 63;
     u64 lo = (h1 << 1) ^ carry;
@@ -89,12 +89,16 @@
     acc[0] = __builtin_bswap64(xi[1]);
     acc[1] = __builtin_bswap64(xi[0]);
     ossl_ghash_blocks_c906_v(acc, kp, in, groups);
+    /* The scalar key in kp covers the remaining whole blocks, so they stay
+     * constant-time instead of returning to the 4-bit table. */
+    if (blocks != 0)
+        ossl_ghash_blocks_c906(acc, kp, in + groups * 80, blocks);
     xi[0] = __builtin_bswap64(acc[1]);
     xi[1] = __builtin_bswap64(acc[0]);
     OPENSSL_cleanse(kp, sizeof(kp));
     OPENSSL_cleanse(powers, sizeof(powers));
     OPENSSL_cleanse(acc, sizeof(acc));
-    return groups * 80;
+    return groups * 80 + blocks * 16;
 }
 
 size_t ossl_gcm_c906_prefix(u64 xi[2], const u128 *table,

gcm_ghash_c906 needs no change: done == len now, and the done != len guard still covers any non-multiple of 16.

Test. The harness below runs the real gcm_c906.c and the generated assembly against a bit-serial SP800-38D GHASH:

  • 40 keys including all-ones and H=1;
  • lengths 3072, 3136, 3200, 3216, 4096, 6144, 16384, 16400;
  • input offsets 0–7.

Results:

Build Cases Failures
Patched glue 1980 0
Current glue, checked against the new done 1980 1320
Tail started one block late (negative control) 1980 1314

QEMU has no XTheadVector, so for this run the th.v* WORDs were mapped to RVV 1.0 (vsetvli e32,m1, vle32.v, vlse32.v) and run on -cpu max,vlen=128,xtheadbb=true. That checks the glue, the key layout and the scalar code, not the 0.7.1 encodings; those stay with the binutils check. This could become the missing CI step for request 2.

harness.c and stubs.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef uint64_t u64; typedef uint32_t u32; typedef uint8_t u8;
typedef struct { u64 hi, lo; } u128;
/* reference: SP800-38D bit-serial multiply on byte strings */
static void gfmul(u8 x[16], const u8 h[16]) {
  u8 z[16] = {0}, v[16]; memcpy(v, h, 16);
  for (int i = 0; i < 128; i++) {
    if (x[i/8] & (0x80 >> (i%8))) for (int k=0;k<16;k++) z[k]^=v[k];
    int lsb = v[15] & 1;
    for (int k=15;k>0;k--) v[k] = (v[k]>>1) | (v[k-1]<<7);
    v[0] >>= 1; if (lsb) v[0] ^= 0xe1;
  }
  memcpy(x, z, 16);
}
static void ref_ghash(u8 xi[16], const u8 h[16], const u8 *in, size_t len) {
  for (size_t i=0;i+16<=len;i+=16){ for(int k=0;k<16;k++) xi[k]^=in[i+k]; gfmul(xi,h); }
}
static u64 be64(const u8 *p){u64 v=0;for(int i=0;i<8;i++)v=v<<8|p[i];return v;}
size_t ossl_gcm_c906_prefix(u64 xi[2], const u128 *table, const u8 *in, size_t len);
int ossl_riscv_has_xtheadvector = 1;
static uint64_t rs=88172645463325252ULL; static u64 rnd(void){rs^=rs<<13;rs^=rs>>7;rs^=rs<<17;return rs;}
int main(void){
  static u8 buf[20000+64]; int fails=0, n=0;
  size_t lens[]={3072,3200,3216,4096,16384,16400,3136, 80*40, 6144};
  for (int key=0; key<40; key++){
    u8 h[16]; for(int k=0;k<16;k++) h[k]=rnd();
    if (key==0) memset(h,0xff,16);
    if (key==1) { memset(h,0,16); h[15]=1; }
    if (key==2) { memset(h,0,16); h[0]=0x80; } /* H = 1 */
    u128 table[16]; memset(table,0,sizeof table); table[8].hi=be64(h); table[8].lo=be64(h+8);
    for (size_t li=0; li<sizeof lens/sizeof*lens; li++) for (int off=0; off<8; off+= (key&1)?3:1) {
      size_t len=lens[li]; u8 *in=buf+off; for(size_t i=0;i<len;i++) in[i]=rnd();
      if (key==3) memset(in,0xff,len);
      u8 x0[16]; for(int k=0;k<16;k++) x0[k]=rnd(); if(key==4) memset(x0,0,16);
      u64 xi[2]; memcpy(xi,x0,16);
      size_t done = ossl_gcm_c906_prefix(xi,table,in,len);
      u8 ref[16]; memcpy(ref,x0,16); ref_ghash(ref,h,in,done);
      n++;
      if (done != len/16*16 || memcmp(ref,xi,16)) { fails++; if (fails<5) printf("FAIL key=%d len=%zu off=%d done=%zu\n",key,len,off,done); }
    }
  }
  printf("%d cases, %d failures\n", n, fails); return fails!=0;
}
#include <stdarg.h>
char *secure_getenv(const char *n){ (void)n; return "auto"; }
int prctl(int op, ...){ (void)op; return 2; }
python3 to_rvv.py ghash-c906.S ghash-rvv.S   # .4byte th.v* -> RVV 1.0 mnemonics, +v in .option arch
CC="riscv64-linux-gnu-gcc -O2 -static -march=rv64gc_xtheadbb -Iinc"   # inc: crypto/modes.h, crypto/riscv_arch.h, openssl/crypto.h
$CC -c gcm_c906.c && $CC -c ghash-rvv.S && $CC harness.c gcm_c906.o ghash-rvv.o stubs.c -o harness
qemu-riscv64 -cpu max,vlen=128,xtheadbb=true ./harness

@dormancygrace dormancygrace left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Restacked head cf3cbbddaa6c98222c3a1acd7655375b30360ce3 onto the cleaned AES #511 (9e3de102), directly above SHA-256 #508. SHA-1 offload commits and CI are absent; AES verification and DMA helper cleanup come from the parent.

All 15 final-head checks pass. The clean C906/O3 build applies every patch with zero fuzz, retains all 5808 public exports, and has no SHA-1 offload or legacy-provider DMA objects. GHASH generator reference/encoding checks, dispatcher sanitizer/negative controls and downloader tests pass. scope-cleanup-validation.json pins this build.

0010-ghash-c906.patch is byte-identical to the previously qualified patch. Native timing and compatibility results remain attributed to their recorded pre-cleanup builds; no native rerun is claimed here. The scalar-remainder proposal in comment 6439303613 remains a separate pending change and is not included in this restack.

@dormancygrace dormancygrace left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed the final head 0d12f28d1c45f1c309eb7b36d272a35e02d9d75c. The scalar-remainder proposal in comment 6439303613 is implemented, and the four requests in review 5665138577 are addressed.

  • Eligible calls process their remaining 0–4 blocks with the existing scalar C906 key before cleansing. No persistent context growth, threshold change or broader opt-in is introduced. Short calls, gmult/tag work, disabled vectors and processes without opt-in still use the data-indexed 4-bit implementation; the documentation no longer implies whole-library constant-time GHASH. The downloader reaches this opt-in on its second, generic attempt after ChaCha.
  • Fast CI now applies the complete patches with zero fuzz to SHA-256-verified OpenSSL source and runs the real glue/generated assembly against an independent bit-serial SP800-38D reference anchored by a known-answer test. All 3520 cases pass: 40 keys, 11 lengths, eight offsets, every scalar-tail count, initial Xi, key layout and input/context/table guards. All five deliberate arithmetic/glue corruptions are detected. QEMU maps the draft vector words to RVV 1.0 for arithmetic coverage; the separate encoding check and a physical C906 run of all 3520 cases with the original XTheadVector instructions also pass.
  • The 1400 B results and their trial spread are documented, without a new small-record or HTTPS gain claim. Earlier throughput figures are explicitly historical; the scalar-tail change was qualified for correctness, not newly benchmarked.
  • Per-call key construction (four scalar multiplies, a 576-byte vector table and prctl, repeated per 3 KiB GHASH chunk) and unchanged context sizes are documented.

A clean C906/O3 build retains exactly 5808 public definitions with no test exports. Native generic/vector/vector+DMA profiles pass the AES, AEAD, GCM, CTR, tail/IV and SHA suites; 28 additional independent EVP SHA tail answers pass. Final production patch hashes match this tested build. scalar-tail-validation.json records identities and results. No remaining correctness finding.

All 19 final-head CI checks pass, including the firmware build.

Base automatically changed from codex/openssl-aes to main October 27, 2026 14:09
@dormancygrace
dormancygrace merged commit 5d373b2 into main Oct 27, 2026
19 checks passed
@dormancygrace
dormancygrace deleted the codex/openssl-ghash branch October 27, 2026 14:09
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.

1 participant