cryptobench: per-SoC AEAD seal rate, in software and on the cipher engine - #187
Conversation
`char` is signed on x86 and ARM alike, so `*c < 32` in print_string() is true for every byte of a UTF-8 sequence, not just for control characters. Those bytes then fell into the \u expansion below, whose scratch buffer holds ten bytes: 0xe2 read as a negative int expands to eleven bytes with its terminator, so the TRY() fails and the failure propagates all the way out of cYAML_Print(), which returns NULL. The caller then prints nothing at all. Not one mangled string in an otherwise fine document -- no document. `ipctool` in its default output mode exits 0 having said nothing, which reads as "this tool found nothing to report" rather than as a failure, and JSON mode is unaffected, so the two modes disagree about whether the machine has any hardware. This is reachable from ordinary output: sensor names, U-Boot environments and vendor strings are read off the device and passed through verbatim. Compare as `unsigned char`. Bytes >= 128 then take the pass-through path, which is what we want -- YAML is UTF-8 and they need no escaping -- and control characters keep being expanded exactly as before. Two test cases cover both halves. The harness was also computing a per-case verdict and then unconditionally returning 0, so no regression in it could ever fail a build; it now exits non-zero when a case fails.
Closes #186, which asks for a per-SoC probe of AES-128-GCM, AES-256-GCM and ChaCha20-Poly1305 on packet-sized buffers after ChaCha20 measured 2.3x AES-128-GCM on a GK7202V300. Adds `ipctool cryptobench`, beside cpubench and membw, which are the same kind of per-SoC probe. ipctool cryptobench [--json] [--bytes N] [--iters N] [--no-hw] Measured on a lab gk7205v200 (Cortex-A7, 1100-byte packets, majestic stopped), microseconds per packet: | | wall | cpu | MB/s | |---|---|---|---| | AES-128-GCM | 141.4 | 141.4 | 7.4 | | AES-256-GCM | 167.6 | 167.6 | 6.3 | | ChaCha20-Poly1305 | 45.1 | 45.1 | 23.3 | | engine AES-CTR, one packet per ioctl | 83.9 | 44.1 | 12.5 | | engine AES-CTR, 15 packets per ioctl | 49.7 | 12.3 | 21.1 | | engine, 16 bytes (the per-ioctl floor) | 37.4 | 34.1 | - | ChaCha20 wins by 3.14x here, so the reported ordering holds and is wider on this part. Written in C, in-tree. ipctool ships as a static musl binary (CMakeLists.txt:9-12, and release.yml builds with the default), musl's dlopen is a no-op when static, and all three CI toolchains carry only libmbedcrypto.so with no .a -- so the platform mbedTLS is not reachable from the shipped binary and linking it would trade the single static binary for a benchmark. The AES uses four runtime-generated T-tables and GHASH uses 4-bit tables, which is what mbedTLS itself does by default; a compact implementation would be several times slower and would inflate ChaCha20's lead into a number about this file rather than about the silicon. THE HARDWARE HALF REFRAMES THE PROPOSAL. The Cipher engine on HiSilicon and Goke parts cannot seal at all: gen-4 silicon has no GCM or CCM mode, and cryptobench establishes that by configuring a channel for AES-GCM and reporting what the driver answers, rather than by asserting it, so a part that does carry the mode reports itself. What the engine can do is AES-CTR, the confidentiality half; the authentication half would still be GHASH on the CPU. So ChaCha20's advantage on these cameras is not a software artifact waiting to be engineered away -- it is the answer. The engine rows also carry their own lesson, which is why the 16-byte row is there. Nearly half the cost of a 1100-byte packet is the trip, not the cipher, and a burst of 15 under one ioctl (openhisilicon#217) cuts CPU per packet from 44 us to 12 while wall clock barely moves. Three things the numbers depend on, all learned the expensive way: - Both clocks, every row. The hardware rows differ by ~2x between them because the driver sleeps on its completion interrupt and hands the core back; quoting either alone hides the point. cpu_wall near 1.00 on a hardware row means the run was contending for CPU. - Nothing is printed between timed blocks. A single printf to a serial console or an ssh pipe between two identical runs made the second one read twice as slow. - Every primitive is checked against a published vector before it is timed -- GCM test cases 2 and 14, RFC 8439 2.8.2, and for the engine NIST SP 800-38A F.5.1 plus a vector spanning a 32-bit counter wrap. A row that fails is reported unverified with no numbers rather than as a fast wrong answer. The wrap vector is not optional: an engine carrying only the low 32 bits of the counter agrees with software for every IV that is not near a wrap, so it would pass F.5.1, ship, and then disagree on one packet in millions. The /dev/cipher ABI is transcribed rather than linked, as majestic does it: libhi_cipher.so is a thin ioctl wrapper and most images do not ship it, so a DT_NEEDED on it would stop ipctool starting on the very cameras it is meant to inspect. Commands are derived from struct sizes with the vendor's own macro, so a struct that does not match the driver's yields a command it does not recognise rather than one it misreads. Degradation was tested on the board, not just written: no /dev/cipher reports `hardware: absent` and the software half still runs; a driver that rejects the batched command reports `batched: unsupported` and still measures the single-packet and floor rows. Cross-builds clean and still statically linked on all three CI targets, for 17-24 KB of .text (arm32 +17636, arm64 +20328, mips32 +24432).
PR Summary by QodoAdd per-SoC AEAD and cipher-engine cryptobench
AI Description
Diagram
High-Level Assessment
Files changed (14)
|
Code Review by Qodo
1. --bytes permits non-packet AEAD
|
| bytes = (size_t)strtoul(optarg, NULL, 10); | ||
| if (bytes < 16 || bytes > MAX_PACKET) { |
There was a problem hiding this comment.
1. --bytes permits non-packet aead 📎 Requirement gap ≡ Correctness
The new option accepts payloads as small as 16 bytes and reports only that selected size, so a run can omit the required approximately 1200-byte AEAD measurements. Although packet_bytes discloses the size, materially different measurements are not accompanied by a packet-sized baseline.
Agent Prompt
## Issue description
Custom `--bytes` values can replace the required approximately 1200-byte AEAD measurements entirely.
## Issue Context
PR Compliance ID 2 permits additional sizes only when packet-sized results remain available and disclosed.
## Fix Focus Areas
- src/cryptobench.c[408-430]
- src/cryptobench.c[460-510]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| return j_inner; | ||
| } | ||
|
|
||
| const bool aead = hisi_cipher_supports_gcm(&hw); |
There was a problem hiding this comment.
3. Supported gcm remains unmeasured 📎 Requirement gap ≡ Correctness
When the driver accepts AES-GCM, the code reports aead: supported but still benchmarks and emits only AES-128-CTR hardware rows. Such a supported engine therefore receives no packet-sized AEAD seal measurement comparable with the software AES-GCM workload.
Agent Prompt
## Issue description
The hardware capability probe can detect AES-GCM support, but the supported path does not execute an AES-GCM sealing benchmark.
## Issue Context
Unsupported engines may continue reporting the explanatory AES-CTR measurements, but engines that accept GCM must run the same packet-sized AES-GCM workload and report it on the same per-packet basis as software.
## Fix Focus Areas
- src/crypto/hisi_cipher.c[179-200]
- src/cryptobench.c[339-390]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
The batch probe was the real defect. Its own comment claimed it used "two packets rather than one, because one would also succeed on a driver that ignored the per-package IV entirely" -- and then submitted two packets with the SAME zero IV and never looked at the output. It checked that the ioctl existed and nothing else, while the row it enabled inherited `verified` from the single-packet vector, so a driver that accepted the burst and applied the channel IV to all fifteen packages would have been reported as a verified 21 MB/s. There is now a known-answer test for the batched command specifically: fifteen packages, each with a distinct IV and distinct plaintext, every one compared against the software AES-CTR in this same binary, which F.5.1 has already pinned to the standard. `batched.verified` stands on that vector rather than inheriting, and the row is not measured unless it passes. On a gk7205v200 all fifteen match, so openhisilicon#217 does honour per-package IVs -- previously assumed, now checked. That test also settles a question raised in review about the batch path handing unrounded lengths to the driver while the single-packet path pads: it runs at the benchmark's own length, which is 1100 bytes by default on that board and not a multiple of the block. It passes, so the driver rounds the descriptor itself and the two paths agree. Padding in the batch path would need a bounce buffer per package and would make the benchmark measure that copy instead of the engine; the asymmetry is now documented as deliberate rather than left to look like an oversight. Also: - Tag the output with `chip:` from getchipname(), as membw does. #186 asks for a table with a row per SoC, and a row of numbers that does not say which SoC is not a row. - `--bytes`/`--iters` went through strtoul() with no endptr and no errno check, then narrowed to unsigned before a minimum-only bound. `--iters -1` became UINT_MAX and `--iters 100junk` silently became 100. Require the whole argument to parse and range-check before narrowing. - A part that does accept AES-GCM reported `aead: supported` and then measured only AES-CTR. No such part was available to test against -- CHIP_AES_CCM_GCM_SUPPORT is hi3569v100 alone -- so rather than ship a sealing path that has never run, it now reports "supported, not measured" and asks for an issue. An unverifiable number is worse than a missing one. - cYAML: validate UTF-8 rather than assuming every byte >= 128 is part of a well-formed sequence. U-Boot environments reach the printer as raw flash bytes, so latin-1 or truncated input could produce a stream that no parser accepts. Valid sequences still pass through unescaped; invalid bytes, overlong forms, surrogates and out-of-range code points become \u00XX, which round-trips losslessly. One more test. Not changed: review also asked that `--bytes` be constrained so a run cannot report a size other than ~1200. Choosing the size is the feature -- the default is 1200, every run states its `packet_bytes`, and the 16-byte row exists precisely to show how much of the cost is per-call. A benchmark that refuses to measure what it is asked to measure would answer a narrower question than #186 poses.
|
Thanks — finding 4 was a real defect and a well-aimed one. Dispositions below; fixes in ecca9ce. 4. Batch IV handling unverified — fixed, and it was worse than described. The probe's own comment claimed it used "two packets rather than one, because one would also succeed on a driver that ignored the per-package IV entirely", and then submitted two packets with the same zero IV and never looked at the output. So the comment described a check that did not exist, and the row it enabled inherited There is now a known-answer test for the batched command specifically: fifteen packages, each with a distinct IV and distinct plaintext, every one compared against the software AES-CTR in this same binary (which F.5.1 has already pinned to the standard). 6. Partial batch packets — answered by that same test, no padding added. The new vector runs at the benchmark's own length, which on that board is 1100 bytes by default and is not a multiple of the block. It passes, so the driver rounds the descriptor itself and the two paths agree. Padding in the batch path would need a bounce buffer per package and would make the benchmark measure that copy rather than the engine, so the asymmetry stays — now documented as deliberate and verified, rather than left looking like an oversight. 2. SoC identity — fixed. Correct, and it undercut the point of the PR: #186 wants a table with a row per SoC, and a row that does not say which SoC is not a row. Now tagged with 7. Iterations can wrap — fixed. 3. Supported GCM remains unmeasured — reported honestly rather than implemented. No part that accepts AES-GCM exists to test against: 5. Invalid UTF-8 emitted raw — fixed properly. Fair: my change traded a total-output failure for a possibly-unparsable one, which is better but not right. cYAML now validates sequences instead of assuming every byte ≥ 128 is well-formed. Valid sequences pass through unescaped; invalid bytes, overlong forms, surrogates and out-of-range code points become 1. |
Closes #186.
Adds
ipctool cryptobench, besidecpubenchandmembw, which are the same kind of per-SoC probe:The finding that reframes the proposal
The proposal offers the hardware engine as the thing that might rescue AES-GCM on these parts. It cannot, and not because it is slow — gen-4 HiSilicon and Goke silicon has no AEAD mode at all.
CHIP_AES_CCM_GCM_SUPPORTis defined forhi3569v100alone; the block does ECB/CBC/CTR/CFB/OFB and nothing else. There is no hardware sealing to enable, on any part OpenIPC currently ships.cryptobenchestablishes that by asking — it configures a channel for AES-GCM and reports what the driver answers — rather than by asserting it, so a part that does carry the mode reports itself instead of inheriting a hardcoded "unsupported".What the engine can do is AES-CTR, the confidentiality half of AES-GCM. The authentication half would still be GHASH on the CPU, which is the expensive half. So ChaCha20's advantage on these cameras is not a software artifact waiting to be engineered away. It is the answer.
Numbers
Lab gk7205v200 (Cortex-A7), 1100-byte packets, majestic stopped, µs per packet:
ChaCha20 wins by 3.14×, so your reported ordering holds and is wider on this part. Different chip and a different implementation from
ring, so I would not read much into 3.14 vs 2.3 beyond the direction — which is the argument for having the probe in-tree and reproducible rather than comparing two people's one-off harnesses.The engine rows carry their own lesson, which is why the 16-byte row is there: nearly half the cost of a 1100-byte packet is the trip, not the cipher. A burst of 15 packets under one ioctl (OpenIPC/openhisilicon#217) cuts CPU per packet from 44 µs to 12 while wall clock barely moves — the engine is a poor bargain per packet and a good one per burst.
Your three questions
1. Language and deps — C, in-tree. Not a style preference:
ipctoolships as a static musl binary (CMakeLists.txt:9-12, andrelease.ymlbuilds with the default), musl'sdlopenis a no-op when static, and all three CI toolchains carry onlylibmbedcrypto.sowith no.a. The platform mbedTLS is genuinely unreachable from the shipped binary, so linking it would trade the single static binary for a benchmark.That does cost the thing you correctly flagged — measuring the library our stacks use — so the implementation is built to keep the comparison honest rather than to be small: four runtime-generated AES T-tables and 4-bit GHASH tables, which is what mbedTLS itself does by default (
MBEDTLS_AES_ROM_TABLESis off). A compact S-box implementation would be several times slower and would inflate ChaCha20's lead into a number about this file rather than about the silicon.2. Where the numbers go — the tool generates the table. Output is YAML by default and
--jsonon request, so the wiki holds pasted tool output rather than hand-typed numbers, and a row can be regenerated by whoever doubts it.3. Scope — yes, it belongs here. It is a capability probe of the running chip, which is exactly
cpubenchandmembw's remit, and it reads/dev/cipher, so it needs to run on the camera anyway. Cost is 19–26 KB of.text(arm32 +19252, arm64 +22248, mips32 +26128); all three CI targets cross-build clean and stay statically linked.And for what it is worth: "interesting, wrong repo" was not the right answer. The proposal was right that this is per-chip and right that packet size is the design point not to compromise on —
--bytesdefaults to 1200 for that reason.Correctness before numbers
A benchmark of a wrong implementation is worth nothing, and a wrong one is invisible from the outside because ciphertext is supposed to look like noise. Every primitive is checked against a published vector before it is timed, and a row that fails is reported
verified: falsewith no numbers rather than as a fast wrong answer:The batch vector is separate from the single-packet one on purpose, and it earns its keep: a driver that accepted the burst but applied the channel's one IV to all fifteen packages would return correct ciphertext for package 0 and silent nonsense for the rest, which is precisely the shape of the benchmark's real jobs. It passes on a gk7205v200, so OpenIPC/openhisilicon#217 does honour per-package IVs — previously assumed, now checked. It also runs at a length that is not a multiple of the block, which establishes that the driver rounds the descriptor itself.
The counter-wrap vector is not optional either. An engine carrying only the low 32 bits of the counter agrees with software for every IV that is not near a wrap, which is nearly all of them — it would pass F.5.1, ship, and then disagree on one packet in millions with nothing to say so.
The §2.8.2 vector earned its keep during development: the first cut of this used Poly1305's own short-final-block padding instead of RFC 8439's
pad16()for the MAC input. That is correct for any message which is a multiple of 16 bytes and wrong for every other one, and nothing but the vector would have caught it.Measurement discipline
Three things the numbers depend on:
cpu_wallnear 1.00 on a hardware row means the run was contending for CPU;--helpsays to stop majestic.printfto a serial console or an ssh pipe between two identical runs has been measured making the second one read twice as slow.Degradation, tested on the board rather than written
/dev/cipher(rmmod open_cipher) →hardware: absent, software half still runsbatched: unsupported, single-packet and floor rows still measuredThe
/dev/cipherABI is transcribed rather than linked, the way majestic does it:libhi_cipher.sois a thin ioctl wrapper and most images do not ship it, so aDT_NEEDEDon it would stopipctoolstarting on the very cameras it is meant to inspect. Commands are derived from struct sizes with the vendor's own macro, so a struct that does not match the driver's yields a command it does not recognise rather than one it misreads.Second commit, unrelated but found by this work
cYAML_Print()returnedNULL— meaningipctoolprinted nothing at all and exited 0 — if any string anywhere in the document contained a non-ASCII byte.charis signed, so every UTF-8 byte tested as a control character, fell into the\uexpansion and overran its scratch buffer. JSON mode was unaffected, so the two output modes disagreed about whether the machine had any hardware. Sensor names and U-Boot environments are passed through verbatim, so this was reachable from ordinary output; mynote:strings are just what tripped it. Fixed by comparing asunsigned char, with two test cases. The test harness was also computing a per-case verdict and then unconditionally returning 0, so it now exits non-zero when a case fails.