Skip to content

Add ARM64 crypto as NEON C intrinsics for MSVC (WOLFSSL_ARMASM_INTRINSICS) - #11035

Draft
coneco-cy wants to merge 1 commit into
wolfSSL:masterfrom
coneco-cy:arm64-msvc-neon-intrinsics
Draft

Add ARM64 crypto as NEON C intrinsics for MSVC (WOLFSSL_ARMASM_INTRINSICS)#11035
coneco-cy wants to merge 1 commit into
wolfSSL:masterfrom
coneco-cy:arm64-msvc-neon-intrinsics

Conversation

@coneco-cy

Copy link
Copy Markdown

Description

wolfSSL's ARM64 crypto kernels ship in two forms today:

  • armv8-*-asm.S / armv8-*-asm_c.c — GNU/GAS assembly and GCC inline asm
  • armv8-*-asm.asm — Microsoft syntax, assembled by armasm64.exe

The armasm64 form already covers MSVC on Windows ARM64 and is wired up in
wolfssl.vcxproj (WolfSSLAarch64Asm=true). This PR does not replace it.

What it adds is a third form of the same kernels — portable NEON C
intrinsics
— because the armasm64 route requires a separate assembler
invocation, and in practice that is only wired up through MSBuild
CustomBuild rules. Projects that consume wolfSSL through CMake, or any
build system without an ARM64 assembler step, cannot use it as-is and silently
fall back to portable C for AES, AES-GCM (including the table-based GHASH),
SHA-256, Poly1305 and ChaCha20.

Intrinsics are ordinary C, so cl.exe compiles them directly: no assembler
step, no generated .asm, and no per-source target-feature flag (MSVC enables
NEON crypto unconditionally on ARM64, unlike clang which needs
-march=armv8-a+crypto).

This came out of profiling MariaDB — which bundles wolfSSL and builds it with
CMake — on a native Windows ARM64 machine, where GHASH and AES showed up as the
top crypto hot spots purely because the hardware path was unreachable from that
build system.

What is added

Four new sibling files under wolfcrypt/src/port/arm/, plus 44 lines of build
wiring. No existing crypto source is modified.

File Symbols
armv8-aes-intrinsics-msvc.c 19 AES / AES-CBC / AES-CTR / AES-ECB / AES-GCM (one-shot and streaming) *_AARCH64 kernels, plus the 7 base-layer symbols aes.c links unconditionally
armv8-sha-intrinsics-msvc.c Transform_Sha256_Len_crypto
armv8-poly1305-intrinsics-msvc.c poly1305_set_key, poly1305_arm64_block_16, poly1305_arm64_blocks, poly1305_final
armv8-chacha-intrinsics-msvc.c wc_chacha_setkey, wc_chacha_setiv, wc_chacha_use_over, wc_chacha_crypt_bytes — 4-block-parallel

These supply the same *_AARCH64 symbols as the assembly, so they are an
alternative to it, not an addition — hence an explicit opt-in rather than an
automatic fallback. Two independent layers keep existing builds untouched:

  1. A new WOLFSSL_ARMASM_INTRINSICS option, default off, which FATAL_ERRORs
    if requested on a non-MSVC toolchain.
  2. Each file self-guards on
    WOLFSSL_ARMASM && WOLFSSL_ARMASM_INTRINSICS && _MSC_VER && !__clang__ && (_M_ARM64 || _M_ARM64EC),
    so it compiles to nothing otherwise. (!__clang__ matters: clang-cl defines
    _MSC_VER but ships clang's arm_neon.h, where vmull_p64's low form takes a
    scalar poly64_t rather than MSVC's __n64.)

Why no changes to existing sources were needed

Worth noting explicitly, since an earlier version of this work did need them —
current master already solves both prerequisites:

  • settings.h:388
    maps _M_ARM64__aarch64__ under WOLFSSL_ARMASM, so the ~104
    __aarch64__ gates in aes.c select correctly under MSVC.
  • cpuid.c:473
    has a _WIN32 branch using IsProcessorFeaturePresent(), so
    Check_CPU_support_HwCrypto() works — no GCC-only mrs probe needed.

Because HAVE_CPUID_AARCH64 derives from __aarch64__ && WOLFSSL_ARMASM, that
bridge also enables the Windows cpuid path automatically.

Testing

All on a native Windows ARM64 machine (Snapdragon X2 Elite / Oryon),
MSVC 14.44.35207, SDK 10.0.26100.0.

Each kernel was validated against three independent references, not one:

  1. Byte-exact vs the original inline asm. The upstream armv8-*-asm_c.c
    bodies were extracted verbatim by script (never retyped) into a reference TU
    built with clang-cl (cl.exe cannot compile GCC inline asm), then compared
    over randomized inputs and edge cases — AES 100k random blocks × all three key
    sizes; CBC over block counts 1/2/3/7/16/255; ChaCha over a ragged multi-call
    sequence (5,1,10,64,3,100,63,65,200,7 bytes), which is the only shape that
    exercises the over/left leftover-keystream contract.
  2. Independent pure-C oracles written from the specs (AES-ECB, GF(2^128)
    GHASH, FIPS 180-4 SHA-256/512, RFC 8439 Poly1305/ChaCha20).
  3. Published KATs: FIPS-197 AES-128 and key schedule, SP800-38A AES-128-CBC
    (F.2.1) and AES-128-CTR (F.5.1, including a 20-byte partial tail), NIST GCM
    case 4 (ciphertext + tag + valid-decrypt + corrupt-tag reject), FIPS 180-4
    SHA-256/512, RFC 8439 §2.4.2 and §2.5.2.

Every test's failure path was proven before its pass was trusted. For each
kernel a negative control was injected and confirmed to turn the suite red
(exit 1): GHASH reduction constant 0x870x86, an extra aesmc in the final
AES round, removing the CBC chaining XOR, 128-bit→32-bit CTR increment, ChaCha
rotation 7→6, not stashing leftover keystream, advancing the ChaCha counter by 1
instead of 4, Poly1305's wrong *5 mod-2^130-5 fold multiplier.

Library-level validation through the public API (wc_AesCbcEncrypt/Decrypt,
wc_AesCtrEncrypt, wc_AesGcmEncrypt/Decrypt) against the real MSVC-ARM64
wolfssl.lib, including ragged chunk sizes exercising aes->left/aes->tmp
carryover. dumpbin -disasm confirms MSVC emits genuine hardware instructions
(pmull/pmull2, aese/aesd/aesmc/aesimc, sha256h*, rbit); all four
files compile with zero warnings under real cl.exe.

The CMake wiring was verified both ways in this tree: configures clean with the
option off (files absent from LIB_SOURCES) and on (all four present), and the
non-MSVC guard was negative-controlled to confirm it actually aborts.

Two bugs this harness caught — offered as evidence it has teeth

  • Transform_Sha256_Len_crypto: the asm's round 2 is
    sha256su0 v4,v5 / add v24,v5,v9 — the register updated is not the one fed
    to the add. Modelling it the obvious way yields a self-consistent but wrong
    digest. Only the published FIPS vector caught it; an asm-vs-intrinsics
    comparison would have agreed if both sides shared the misconception.
  • An MSVC-only defect invisible to clang-cl-built tests: the code used __rev(),
    a clang builtin that is not a declared MSVC ARM64 intrinsic — real cl.exe
    emitted warning C4013: '__rev' undefined; assuming extern returning int.
    Fixed to _byteswap_ulong. A separate pure-C KAT harness built by cl.exe
    itself and linked against the production objects now covers the shipping
    compiler's view.

Performance

Paired A/B, two builds differing only in this option; same source, flags,
data and iteration counts; warm-up pass; anti-hoist checksum printed and
verified identical between builds, so both provably did the same work.
256 MiB per case in 64 KiB chunks. Baseline is wolfSSL's real portable-C
fallback (4-bit GMULT tables), not a strawman.

Case portable C intrinsics speedup
AES-256-CBC encrypt 245 MB/s 1173 MB/s 4.8x
AES-256-CTR 266 MB/s 3234 MB/s 12.2x
AES-256-ECB encrypt 296 MB/s 3076 MB/s 10.4x
AES-256-GCM encrypt 74 MB/s 3087 MB/s 41.5x
AES-256-GCM decrypt 77 MB/s 2986 MB/s 38.9x

In-process profiles, symbol swap visible in-trace:

Kernel portable C intrinsics speedup
SHA-256 1166.3 ms 208.0 ms 5.6x
ChaCha20 244.8 ms 125.0 ms 2.0x
Poly1305 212.7 ms 87.0 ms 2.4x

For ChaCha20 a control makes it credible: an unrelated unported symbol in the
same trace moved only +6.7%, within the noise band, so the delta is not a global
shift.

To be explicit about what these are not: they are primitive/kernel-level
figures. End-to-end in the host application the effect was measurable (1.36x,
non-overlapping ranges) only for byte-bound encrypted workloads; for
latency-bound OLTP microqueries crypto was ~0.2% of runtime and the difference
was indistinguishable from run-to-run noise.

Not measured: intrinsics vs. the armasm64 assembly path. Both derive from
the same kernels so I would expect rough parity, but I have not run that
comparison and will not claim it. The case for this PR is build-system
reachability, not beating the assembly.

One counter-intuitive result worth reporting

The first ChaCha20 version did one block per iteration — correct on every test,
but measurably slower than portable C (299 ms vs 245 ms in-server). One
block is a ~16-op NEON dependency chain with no ILP, while MSVC /O2 schedules
the 8 QUARTERROUND expansions well. Rewriting to 4 blocks in parallel (each
state word broadcast across a vector, lane i = block i, which also removes
the vextq lane rotations the diagonal rounds otherwise need) gave 2.0x. A
faithful-but-narrow intrinsic port can lose to optimized scalar C, and it is
only visible if you profile against the real fallback.

Questions for maintainers

  1. Is this direction wanted at all? The armasm64 path already covers MSVC
    ARM64, so this is additive convenience for non-MSBuild consumers. If you would
    rather see CMake taught to invoke armasm64.exe instead, that is a reasonable
    alternative and I am happy to pursue it — I would just want your call before
    investing further either way.
  2. Naming / gating. Is WOLFSSL_ARMASM_INTRINSICS the right name and shape,
    or would you prefer it derived automatically when MSVC AND ARM64 AND NOT
    MSBuild, or folded into the existing WOLFSSL_ARMASM_INLINE selector as a
    third value?
  3. Scope. AES_XTS_* and the _EOR3 variants are absent (the configuration
    this was developed against enables neither, so aes.c never referenced them).
    Enabling either with this option will not link. Should they be completed before
    merge, or is the guarded partial set acceptable with the armasm64 path as the
    answer for those configurations?
  4. SHA-512. A verified kernel is included but left inert behind
    WOLFSSL_ARM64_SHA512_HW_MSVC (defined nowhere). When it was written there was
    no usable MSVC feature gate; master's cpuid.c now queries
    PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE under WOLFSSL_ARMASM_CRYPTO_SHA512, so
    wiring it up looks straightforward — but I have not done or tested that, and did
    not want to ship an untested path for an optional architectural extension.
    Would you like it wired up here or dropped from this PR?
  5. Test integration. These kernels are covered by external GoogleTest projects
    and a cl.exe KAT harness, not by wolfSSL's own suite — I could not link
    wolfcrypt_test() in the host project (its config omits ~51 ed25519/fe_*
    symbols test.c wants). Guidance on the canonical way to run testwolfcrypt
    for an MSVC ARM64 build would be welcome, and I will add coverage there.
  6. Poly1305 throughput. This uses the classic single-block 5×26-bit form
    rather than the asm's 4-way r^1..r^4 blocking. That is safe because the limb
    fields are private to these kernels (grep-verified: no other TU reads
    ctx->r64/r1..r4/r4321), so only the MAC is observable. It is
    correctness-first; parity with the asm's throughput would need another pass.

Checklist

  • added tests — external GoogleTest + KAT harnesses with proven negative
    controls; not yet integrated into wolfSSL's own suite, see question 5
  • updated/added doxygen
  • updated appropriate READMEs
  • Updated manual and documentation

…SICS)

wolfSSL's ARM64 crypto kernels ship in two forms: GNU/GAS assembly with GCC
inline asm (armv8-*-asm.S / -asm_c.c), and Microsoft-syntax assembly assembled
by armasm64.exe (armv8-*-asm.asm). The armasm64 route covers MSVC ARM64 and is
wired up in wolfssl.vcxproj via WolfSSLAarch64Asm.

That route needs a separate assembler invocation, which in practice is only
wired up through MSBuild CustomBuild rules. Projects consuming wolfSSL through
CMake - or any build without an ARM64 assembler step - cannot use it as-is, and
fall back to portable C for AES, AES-GCM (including table-based GHASH),
SHA-256, Poly1305 and ChaCha20.

This adds a third form: the same kernels as portable NEON C intrinsics, which
cl.exe compiles as ordinary C. No assembler step, no generated .asm, and no
per-source target-feature flag (MSVC enables NEON crypto unconditionally on
ARM64, unlike clang which needs -march=armv8-a+crypto).

The four new files supply the same *_AARCH64 symbols as the assembly, so they
are an alternative to it rather than an addition. They are opt-in behind
WOLFSSL_ARMASM_INTRINSICS and each is self-guarding on
_MSC_VER && !__clang__ && _M_ARM64, so every existing configuration and
toolchain is unaffected.

  armv8-aes-intrinsics-msvc.c       19 AES/AES-GCM/AES-CBC *_AARCH64 kernels
                                    plus 7 base-layer fallback symbols
  armv8-sha-intrinsics-msvc.c       Transform_Sha256_Len_crypto
  armv8-poly1305-intrinsics-msvc.c  4 Poly1305 kernels
  armv8-chacha-intrinsics-msvc.c    4 ChaCha20 kernels, 4-block-parallel

No existing crypto source is modified: settings.h already bridges
_M_ARM64 to __aarch64__ under WOLFSSL_ARMASM, and cpuid.c already has a
Windows ARM64 IsProcessorFeaturePresent() path, so the gates and the runtime
feature probe work as-is.

Each kernel was verified against three independent references: the original
inline asm extracted verbatim and built with clang-cl, independent pure-C
oracles, and published KATs (FIPS-197, FIPS 180-4, SP800-38A CBC/CTR, NIST
GCM, RFC 8439). Every test's failure path was proven first with an injected
negative control.

Not yet ported: AES_XTS_* (WOLFSSL_AES_XTS) and the _EOR3 variants
(WOLFSSL_ARMASM_CRYPTO_SHA3). A verified SHA-512 kernel is included but left
inert pending a wired-up feature gate.
@wolfSSL-Bot

Copy link
Copy Markdown

Can one of the admins verify this patch?

@dgarske

dgarske commented Aug 3, 2026

Copy link
Copy Markdown
Member

Hi @coneco-cy , thank you for submitting this work. I've asked @SparkiDev to review it and see if this is something we'd consider accepting. Can you tell us more about your project and interest in wolfSSL? If we are to accept this we will need to get a signed contributor agreement in place. Thanks, David Garske, wolfSSL

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.

4 participants