Constant-Time Analysis: ppad-chacha
ppad-chacha v0.2.2 is a Haskell implementation of the ChaCha20 stream cipher specified by RFC 8439. It avails of NEON hardware acceleration on supporting ARM hosts, making use of C code via GHC's FFI.
No exploitable secret-dependent timing was found on the analysed surface, viz.
- the 'cipher' encryption/decryption function,
- the 'block' keystream block function, and
- the 'chacha20_cipher_arm' and 'chacha20_block_arm' NEON kernel functions that dispatch only on NEON-capable ARM hosts.
No exploitable secret-dependent timing was identified in the statically resolved reach of the aarch64, x86_64, and riscv64 builds. The aarch64 and x86_64 assembly corresponding to the Haskell source was produced by GHC's LLVM backend at LLVM 19.1.7; for riscv64 it was procured by lowering the x86_64 LLVM IR. The aarch64 assembly corresponding to the C source (which is only dispatched on aarch64) was procured by clang 21.1.2.
Dynamically, no constant-time hypothesis for the analysed surface was rejected on any available meter or host.
summary
| function | aarch64 | x86_64 | riscv64 |
|---|---|---|---|
|
cipher
ChaCha20 encryption/decryption (RFC 8439)
|
S✓C✓ | S✓C✓ | S✓C· |
|
block
ChaCha20 keystream block function (RFC 8439)
|
S✓C✓ | S✓C✓ | S✓C· |
|
chacha20_cipher_arm
ARM NEON kernel backing cipher on NEON-capable ARM hosts
|
S✓C✓ | S·C· | S·C· |
|
chacha20_block_arm
ARM NEON kernel backing block on NEON-capable ARM hosts
|
S✓C✓ | S·C· | S·C· |
✓ no leak detected ✗ leak – qualified ? inconclusive · not run S = substratum C = censor
method
This analysis assumes an adversary capable of:
- invoking the analysed functions with chosen inputs an arbitrary number of times,
- measuring execution time with nanosecond precision,
- measuring retired instruction counts or cycles, where a PMU exists,
- observing cache state, and
- observing memory allocation counts and sizes.
A trusted execution environment at the physical layer is assumed; considered out-of-scope are adversaries who can perform attacks over power or electromagnetic emanation-related side channels, speculative execution attacks, and so on.
The security property under consideration is whether the analysed functions "run in constant time," i.e. whether the observable execution of any analysed function, when called with differing sensitive inputs, is indistinguishable to an attacker with the granted capabilities.
Consider the execution trace t(f, x) procured by evaluating an analysed function f on inputs x, where the trace consists of the sequence of instructions executed, the time they take to execute, the memory addresses accessed, and the size and count of allocations made. Then for any sensitive inputs s0 and s1, and any public input p, the property is, up to renaming of base pointers and stack addresses, and modulo any public comparison result, that:
t(f, (s0, p)) == t(f, (s1, p))
where equality is in terms of distribution.
This security property is scrutinized by way of two analyses.
substratum v0.3.7 statically scans compiled assembly for variable-time instructions, e.g. conditional and indirect branches, register-indexed memory accesses, division, and variable shifts. It deterministically ascribes facts to instructions, e.g. that they represent control flow, memory addressing, etc., as well as establishes structural roles for them, such as that an instruction functions as an early-return guard or loop-latch. It also traces each fact's timing-relevant operands back to where the values entered the function.
From each audited entry, substratum filters out known runtime patterns and resolves the transitive closure of direct calls and return-frame continuations, scanning the body of every symbol it expands. The walk stops where control goes dynamic, where it leaves the parsed unit, or where the audit prunes it at a callee audited in its own lane. Every stopping edge, pruned callees included, is enumerated under analysis bounds, so the reach is an explicit lower bound rather than an implied whole. Filtered runtime facts are counted as "suppressed" under analysis bounds.
elenchus v0.1.0 is an agentic distillation protocol used to classify and annotate substratum facts. Two agents independently carry out an appraise action, constructing two separate candidate classification sets. One of those agents then carries out a supplemental dissent action that identifies where, if anywhere, the agents disagreed. A third agent then carries out a falsify action that tries to disprove every classification made. Finally, a coordinating agent distills the results, performing its own independent analysis on anything left unresolved, and the auditor accepts or revises every resulting classification. The classification scheme is as follows:
- benign means the site's timing depends only on public data (lengths, allocation guards, constructor tags, loop counters over public bounds, etc.).
- input validation marks a branch that rejects malformed input on a public property before any sensitive input-dependent work begins, revealing only the rejection.
- negligible probability marks a path whose triggering event occurs with cryptographically negligible probability, typically that proportional to the inverse key size.
- leak is a site whose timing depends on sensitive content. It flags sensitive information an observer could infer (e.g. a scalar byte, a bitlength, Hamming weight).
censor v0.4.4 dynamically falsifies constant-time execution. A case fixes a hypothesis (in which one class pins the sensitive input, the other draws it fresh per pair, with public context shared) and streams paired timing differences through a hedged e-process: a mixture of anytime-valid tests for mean shifts, shape asymmetries, and rare-outlier leaks. The displayed p is the anytime-valid p-value associated with the e-process at the stopping time; a hypothesis is rejected when p < α.
A censor run is performed for every hypothesis using every available meter. Wall time is the portable, always-available meter; the 'instructions retired' and 'cycles' meters require a PMU that may be unavailable on some platforms. Raw differences are clipped via a per-meter scale set during warmup, so heavy-tailed measurement noise can neither bankrupt nor inflate the test. The effect interval under each panel is the anytime-valid confidence interval for that (clipped) mean; its endpoints resolve on a grid of roughly 0.1·c. The jitter is the ratio of a measured baseline interquartile range to its median.
measure v0.3.1 synthesizes and presents the analyses. substratum and censor results, elenchus annotations, provenance information, and the auditor's written summary are deterministically aggregated, verified, and rendered into this report, which is then signed with the auditor's GPG signature.
findings by function
cipher
Crypto.Cipher.ChaCha20.ciphersensitive: key| isa | result | symbols walked | instructions flagged |
|---|---|---|---|
| aarch64 | ✓ | 59 | 35 |
| x86_64 | ✓ | 46 | 28 |
| riscv64 | ✓ | 47 | 34 |
| class | assessment | note | aarch64 | x86_64 | riscv64 |
|---|---|---|---|---|---|
|
cond-branch
control-flow
|
input validation |
public 32-byte key- and 12-byte nonce-length rejection
|
×2
early-return-guard
|
— |
×2
|
|
cond-branch
control-flow
|
benign |
constructor-tag dispatch on the static chacha20_arm_available capability CAF
|
×1
← entry (R1)
|
×1
← counter
|
×1
← entry (R1) + immediate
|
|
cond-branch
control-flow
|
benign |
GHC heap- and stack-limit bookkeeping checks on runtime allocation and stack pointers
|
×2
early-return-guard
|
×1
← entry (Base) + counter
|
×2
|
|
cond-branch
control-flow
|
benign |
GHC closure pointer-tag re-entry tests (tst #0x7) on loop-state closures, deciding evaluation status rather than data
|
×2
← clobbered by call (approximate)
|
— | — |
|
cond-branch
control-flow
|
benign |
fixed-width key and nonce parser exhaustion and four-byte chunk control after length validation
|
×1
← counter
|
×1
← counter
|
×1
← counter
|
|
cond-branch
control-flow
|
benign |
fixed-count loop latches on compile-time bounds: the ten-round latch, the sixteen-word state loops, and fixed parser step bounds
|
×2
|
×1
← load (Sp frame slot)
|
×2
|
|
cond-branch
control-flow
|
benign |
chunk and builder control on the public plaintext and serialized-output lengths: chunk-cursor and remainder tests, 64-byte builder capacity guards, and output-length sign and overflow guards
|
×14
|
×10
|
×13
|
|
cond-branch
control-flow
|
benign |
constructor-tag dispatch on public lazy ByteString builder chunks
|
×4
← entry (R1)
|
×4
← counter
|
×4
← entry (R1) + immediate
|
|
reg-index
memory-address
|
benign |
ChaCha state-buffer addressing at fixed positions or public loop indices
|
×3
loop-exit, early-return-guard
address-base ← load
address-index ← load
|
×2
loop-exit
address-base ← load
address-index ← load
|
×3
loop-exit
address-base ← load
address-index ← load
|
|
reg-index
memory-address
|
benign |
plaintext and keystream XOR addressing on the public sequential byte cursor
|
×3
loop-latch, loop-exit, early-return-guard, chained-guard
address-base ← load
address-index ← load
|
×3
loop-latch, loop-exit
address-base ← load
address-index ← load
|
×3
loop-latch, loop-exit, chained-guard
address-base ← load
address-index ← load
|
|
length-call
work-count
|
benign |
copy calls whose work count is fixed at 64 bytes or bounded by the public plaintext chunk length
|
×1
← load
|
×1
← load
|
×1
← load
|
|
indirect-branch
control-flow
|
benign |
GHC continuation dispatch through info-table pointers in the fixed-width key parser
|
— |
×2
← load
|
— |
|
cond-branch
control-flow
|
benign |
fixed-size state-copy path select on allocation-pointer equality
|
— |
×2
local-diamond, select-diamond
← load
|
×2
local-diamond, select-diamond
← load
|
complete fact list — aarch64 ×35 · x86_64 ×28 · riscv64 ×34
| b.ge LBB256_4 |
cond-branch
control-flow
|
$w_parse_key:13578 | benign |
| b.ne LBB309_6 |
cond-branch
control-flow
|
$wcipher:16398 | input validation |
| b.ne LBB309_7 |
cond-branch
control-flow
|
$wcipher:16403 | input validation |
| b.ge LBB294_3 |
cond-branch
control-flow
|
blk_cd2H:15668 | benign |
| cbz x8, LBB294_4 |
cond-branch
control-flow
|
blk_cd2H:15682 | benign |
| cbnz x8, LBB294_2 |
cond-branch
control-flow
|
blk_cd2H:15708 | benign |
| cbz x9, LBB294_6 |
cond-branch
control-flow
|
blk_cd2H:15712 | benign |
| b.ge LBB105_4 |
cond-branch
control-flow
|
c8.:4796 | benign |
| b.ne LBB107_3 |
cond-branch
control-flow
|
c8D4:5034 | benign |
| b.ne LBB108_3 |
cond-branch
control-flow
|
c8Dk:5090 | benign |
| b.pl LBB108_5 |
cond-branch
control-flow
|
c8Dk:5097 | benign |
| b.ne LBB310_3 |
cond-branch
control-flow
|
ccYR:16478 | benign |
| tbnz w8, #0, LBB310_4 |
cond-branch
control-flow
|
ccYR:16480 | benign |
| tbz w8, #0, LBB310_5 |
cond-branch
control-flow
|
ccYR:16499 | benign |
| b.ge LBB284_4 |
cond-branch
control-flow
|
cd3F:15025 | benign |
| ldrb w8, [x8, x9] |
reg-index
memory-address
|
cd3F:15031 | benign |
| ldrb w10, [x10, x9] |
reg-index
memory-address
|
cd3F:15032 | benign |
| strb w8, [x11, x9] |
reg-index
memory-address
|
cd3F:15038 | benign |
| b.lt LBB284_3 |
cond-branch
control-flow
|
cd3F:15041 | benign |
| b.gt LBB283_3 |
cond-branch
control-flow
|
cd3s:14948 | benign |
| tbnz x8, #63, LBB283_4 |
cond-branch
control-flow
|
cd3s:14952 | benign |
| tbz x8, #63, LBB283_2 |
cond-branch
control-flow
|
cd3s:14972 | benign |
| b.ge LBB288_5 |
cond-branch
control-flow
|
cd4Y:15206 | benign |
| b.gt LBB288_6 |
cond-branch
control-flow
|
cd4Y:15217 | benign |
| bl _memcpy |
length-call
work-count
|
cd4Y:15221 | benign |
| b.ne LBB296_3 |
cond-branch
control-flow
|
cd6J:15810 | benign |
| b.pl LBB296_5 |
cond-branch
control-flow
|
cd6J:15817 | benign |
| b.ne LBB302_4 |
cond-branch
control-flow
|
cd6h:16073 | benign |
| ldr w10, [x10, #16] |
reg-index
memory-address
|
cd6h:16088 | benign |
| ldr w11, [x9, #16] |
reg-index
memory-address
|
cd6h:16089 | benign |
| str w10, [x9, #16] |
reg-index
memory-address
|
cd6h:16091 | benign |
| b.eq LBB302_5 |
cond-branch
control-flow
|
cd6h:16092 | benign |
| b.ne LBB295_3 |
cond-branch
control-flow
|
cd6z:15754 | benign |
| b.eq LBB102_4 |
cond-branch
control-flow
|
s6Ch:4542 | benign |
| b.eq LBB104_4 |
cond-branch
control-flow
|
s6Cj:4724 | benign |
| jg .LBB256_4 |
cond-branch
control-flow
|
$w_parse_key:13908 | benign |
| jmp *%rax |
indirect-branch
control-flow
|
$w_parse_key:14019 | benign |
| jmp *%rax |
indirect-branch
control-flow
|
$w_parse_key:14026 | benign |
| jge .LBB294_1 |
cond-branch
control-flow
|
blk_cd4q:16264 | benign |
| jne .LBB294_3 |
cond-branch
control-flow
|
blk_cd4q:16314 | benign |
| je .LBB294_7 |
cond-branch
control-flow
|
blk_cd4q:16317 | benign |
| jne .LBB108_1 |
cond-branch
control-flow
|
c8DM:5172 | benign |
| jns .LBB108_4 |
cond-branch
control-flow
|
c8DM:5178 | benign |
| jne .LBB107_3 |
cond-branch
control-flow
|
c8Dw:5110 | benign |
| jne .LBB310_1 |
cond-branch
control-flow
|
cd0o:17201 | benign |
| ja .LBB310_4 |
cond-branch
control-flow
|
cd0o:17209 | benign |
| jne .LBB300_2 |
cond-branch
control-flow
|
cd4B:16633 | benign |
| jne .LBB300_4 |
cond-branch
control-flow
|
cd4B:16690 | benign |
| jg .LBB283_1 |
cond-branch
control-flow
|
cd5b:15465 | benign |
| jge .LBB284_4 |
cond-branch
control-flow
|
cd5o:15550 | benign |
| movzbl (%rax,%rcx,1), %eax |
reg-index
memory-address
|
cd5o:15556 | benign |
| movzbl (%rdx,%rcx,1), %edx |
reg-index
memory-address
|
cd5o:15559 | benign |
| movb %dl, (%rsi,%rcx,1) |
reg-index
memory-address
|
cd5o:15563 | benign |
| jl .LBB284_3 |
cond-branch
control-flow
|
cd5o:15567 | benign |
| jge .LBB288_5 |
cond-branch
control-flow
|
cd6K:15749 | benign |
| jg .LBB288_6 |
cond-branch
control-flow
|
cd6K:15761 | benign |
| call memcpy@PLT |
length-call
work-count
|
cd6K:15766 | benign |
| jne .LBB302_5 |
cond-branch
control-flow
|
cd87:16770 | benign |
| movl 16(%rdx,%rcx,4), %edx |
reg-index
memory-address
|
cd87:16784 | benign |
| addl %edx, 16(%rax,%rcx,4) |
reg-index
memory-address
|
cd87:16785 | benign |
| jne .LBB295_3 |
cond-branch
control-flow
|
cd8p:16363 | benign |
| jne .LBB296_1 |
cond-branch
control-flow
|
cd8z:16425 | benign |
| jns .LBB296_4 |
cond-branch
control-flow
|
cd8z:16431 | benign |
| blt zero, a3, .LBB256_4 |
cond-branch
control-flow
|
$w_parse_key:13819 | benign |
| bne s7, a0, .LBB309_6 |
cond-branch
control-flow
|
$wcipher:16931 | input validation |
| bne a0, a1, .LBB309_7 |
cond-branch
control-flow
|
$wcipher:16936 | input validation |
| bge a2, a0, .LBB294_2 |
cond-branch
control-flow
|
blk_cd4q:16110 | benign |
| beq a0, zero, .LBB294_5 |
cond-branch
control-flow
|
blk_cd4q:16136 | benign |
| beq a0, zero, .LBB294_7 |
cond-branch
control-flow
|
blk_cd4q:16157 | benign |
| bne a1, a2, .LBB108_3 |
cond-branch
control-flow
|
c8DM:5306 | benign |
| bge a0, zero, .LBB108_5 |
cond-branch
control-flow
|
c8DM:5313 | benign |
| bne a0, a1, .LBB107_3 |
cond-branch
control-flow
|
c8Dw:5247 | benign |
| bge a2, a0, .LBB105_4 |
cond-branch
control-flow
|
c8zK:4874 | benign |
| bne a0, a2, .LBB310_3 |
cond-branch
control-flow
|
cd0o:17007 | benign |
| bne a1, a2, .LBB300_4 |
cond-branch
control-flow
|
cd4B:16459 | benign |
| bne a0, a1, .LBB300_7 |
cond-branch
control-flow
|
cd4B:16515 | benign |
| blt a3, a1, .LBB283_3 |
cond-branch
control-flow
|
cd5b:15329 | benign |
| blt a0, zero, .LBB283_4 |
cond-branch
control-flow
|
cd5b:15335 | benign |
| bge a0, zero, .LBB283_2 |
cond-branch
control-flow
|
cd5b:15358 | benign |
| bge a0, a1, .LBB284_4 |
cond-branch
control-flow
|
cd5o:15411 | benign |
| lbu a0, 0(a0) |
reg-index
memory-address
|
cd5o:15418 | benign |
| lbu a2, 0(a2) |
reg-index
memory-address
|
cd5o:15420 | benign |
| sb a0, 0(a3) |
reg-index
memory-address
|
cd5o:15426 | benign |
| blt a0, a1, .LBB284_3 |
cond-branch
control-flow
|
cd5o:15431 | benign |
| bge a0, a2, .LBB288_5 |
cond-branch
control-flow
|
cd6K:15602 | benign |
| blt a0, a4, .LBB288_6 |
cond-branch
control-flow
|
cd6K:15615 | benign |
| call memcpy |
length-call
work-count
|
cd6K:15620 | benign |
| bne a0, a1, .LBB302_4 |
cond-branch
control-flow
|
cd87:16596 | benign |
| lw a3, 16(a3) |
reg-index
memory-address
|
cd87:16613 | benign |
| lw a4, 16(a2) |
reg-index
memory-address
|
cd87:16614 | benign |
| sw a3, 16(a2) |
reg-index
memory-address
|
cd87:16616 | benign |
| beq a1, a0, .LBB302_5 |
cond-branch
control-flow
|
cd87:16617 | benign |
| bne a0, a1, .LBB295_3 |
cond-branch
control-flow
|
cd8p:16200 | benign |
| bne a1, a2, .LBB296_3 |
cond-branch
control-flow
|
cd8z:16259 | benign |
| bge a0, zero, .LBB296_5 |
cond-branch
control-flow
|
cd8z:16266 | benign |
| bltu a0, s10, .LBB106_3 |
cond-branch
control-flow
|
s6D4:5103 | benign |
| bltu a0, s10, .LBB290_3 |
cond-branch
control-flow
|
s70V:15740 | benign |
test cases
| cipher (key fix-vs-random, 245 B) | class A encrypts a fixed 245-byte plaintext with a fixed 32-byte key, class B with a fresh random key per draw; the 96-bit nonce, the block counter and the plaintext stay fixed in both classes, a per-draw key copy keeps buffer identity and cache history symmetric, and only key content differs (245 bytes is three full keystream blocks plus a 53-byte partial tail, exercising the per-block loop and the byte-wise tail on both the Haskell and ARM-kernel paths) |
| host | meter | result | p | effect (mean/pair) | jitter | clip scale | clipped |
|---|---|---|---|---|---|---|---|
| cipher (key fix-vs-random, 245 B) | |||||||
| aarch64-darwin | wall | ✓ | 0.120 | [-8.93, 27] ns | 5.3% | 168 ns | 20/50000 at 16c |
| x86_64-linux | wall | ✓ | 0.111 | [-63k, 21k] ns | 29% | 392k ns | 0/50000 at 16c |
| x86_64-linux | instructions | ✓ | 0.253 | [-2416, 805] instr | 0.7% | 15.15k instr | 4/50000 at 16c |
| x86_64-linux | cycles | ✓ | 0.541 | [-6593, 2198] cyc | 2.4% | 41.34k cyc | 7/50000 at 16c |
wall
wall
2 further meter panels
instructions
cycles
block
Crypto.Cipher.ChaCha20.blocksensitive: key| isa | result | symbols walked | instructions flagged |
|---|---|---|---|
| aarch64 | ✓ | 37 | 17 |
| x86_64 | ✓ | 28 | 12 |
| riscv64 | ✓ | 29 | 16 |
| class | assessment | note | aarch64 | x86_64 | riscv64 |
|---|---|---|---|---|---|
|
cond-branch
control-flow
|
input validation |
public 32-byte key- and 12-byte nonce-length rejection
|
×2
early-return-guard
|
— |
×2
|
|
cond-branch
control-flow
|
benign |
constructor-tag dispatch on the static chacha20_arm_available capability CAF
|
×1
← entry (R1)
|
×1
← counter
|
×1
← entry (R1) + immediate
|
|
cond-branch
control-flow
|
benign |
GHC heap- and stack-limit bookkeeping checks on runtime allocation and stack pointers
|
×2
early-return-guard
|
×1
← entry (Base) + counter
|
×2
|
|
cond-branch
control-flow
|
benign |
GHC closure pointer-tag re-entry tests (tst #0x7) on loop-state closures, deciding evaluation status rather than data
|
×2
← clobbered by call (approximate)
|
— | — |
|
cond-branch
control-flow
|
benign |
fixed-width key and nonce parser exhaustion and four-byte chunk control after length validation
|
×1
← counter
|
×1
← counter
|
×1
← counter
|
|
cond-branch
control-flow
|
benign |
fixed-count loop latches on compile-time bounds: the ten-round latch, the sixteen-word state loops, and fixed parser step bounds
|
×2
|
×1
← load (Sp frame slot)
|
×2
|
|
cond-branch
control-flow
|
benign |
chunk and builder control on the public plaintext and serialized-output lengths: chunk-cursor and remainder tests, 64-byte builder capacity guards, and output-length sign and overflow guards
|
×2
|
×1
← accumulator over loads
|
×2
|
|
cond-branch
control-flow
|
benign |
constructor-tag dispatch on public lazy ByteString builder chunks
|
×2
← entry (R1)
|
×2
← counter
|
×2
← entry (R1) + immediate
|
|
reg-index
memory-address
|
benign |
ChaCha state-buffer addressing at fixed positions or public loop indices
|
×3
loop-exit, early-return-guard
address-base ← load
address-index ← load
|
×2
loop-exit
address-base ← load
address-index ← load
|
×3
loop-exit
address-base ← load
address-index ← load
|
|
indirect-branch
control-flow
|
benign |
GHC continuation dispatch through info-table pointers in the fixed-width key parser
|
— |
×2
← load
|
— |
|
cond-branch
control-flow
|
benign |
fixed-size state-copy path select on allocation-pointer equality
|
— |
×1
local-diamond, select-diamond
← load
|
×1
local-diamond, select-diamond
← load
|
complete fact list — aarch64 ×17 · x86_64 ×12 · riscv64 ×16
| b.ge LBB256_4 |
cond-branch
control-flow
|
$w_parse_key:13578 | benign |
| b.ne LBB276_6 |
cond-branch
control-flow
|
$wblock:14629 | input validation |
| b.ne LBB276_7 |
cond-branch
control-flow
|
$wblock:14634 | input validation |
| b.ge LBB105_4 |
cond-branch
control-flow
|
c8.:4796 | benign |
| b.ne LBB107_3 |
cond-branch
control-flow
|
c8D4:5034 | benign |
| b.ne LBB108_3 |
cond-branch
control-flow
|
c8Dk:5090 | benign |
| b.pl LBB108_5 |
cond-branch
control-flow
|
c8Dk:5097 | benign |
| b.ne LBB277_3 |
cond-branch
control-flow
|
ccAx:14708 | benign |
| tbnz w8, #0, LBB277_4 |
cond-branch
control-flow
|
ccAx:14710 | benign |
| tbz w8, #0, LBB277_5 |
cond-branch
control-flow
|
ccAx:14725 | benign |
| b.ne LBB270_4 |
cond-branch
control-flow
|
ccF5:14392 | benign |
| ldr w10, [x10, #16] |
reg-index
memory-address
|
ccF5:14406 | benign |
| ldr w11, [x9, #16] |
reg-index
memory-address
|
ccF5:14407 | benign |
| str w10, [x9, #16] |
reg-index
memory-address
|
ccF5:14409 | benign |
| b.eq LBB270_5 |
cond-branch
control-flow
|
ccF5:14410 | benign |
| b.eq LBB102_4 |
cond-branch
control-flow
|
s6Ch:4542 | benign |
| b.eq LBB104_4 |
cond-branch
control-flow
|
s6Cj:4724 | benign |
| jg .LBB256_4 |
cond-branch
control-flow
|
$w_parse_key:13908 | benign |
| jmp *%rax |
indirect-branch
control-flow
|
$w_parse_key:14019 | benign |
| jmp *%rax |
indirect-branch
control-flow
|
$w_parse_key:14026 | benign |
| jne .LBB108_1 |
cond-branch
control-flow
|
c8DM:5172 | benign |
| jns .LBB108_4 |
cond-branch
control-flow
|
c8DM:5178 | benign |
| jne .LBB107_3 |
cond-branch
control-flow
|
c8Dw:5110 | benign |
| jne .LBB277_1 |
cond-branch
control-flow
|
ccBH:15167 | benign |
| ja .LBB277_4 |
cond-branch
control-flow
|
ccBH:15175 | benign |
| jne .LBB268_2 |
cond-branch
control-flow
|
ccCE:14750 | benign |
| jne .LBB270_5 |
cond-branch
control-flow
|
ccGr:14830 | benign |
| movl 16(%rdx,%rcx,4), %edx |
reg-index
memory-address
|
ccGr:14844 | benign |
| addl %edx, 16(%rax,%rcx,4) |
reg-index
memory-address
|
ccGr:14845 | benign |
| blt zero, a3, .LBB256_4 |
cond-branch
control-flow
|
$w_parse_key:13819 | benign |
| bne s7, a0, .LBB276_6 |
cond-branch
control-flow
|
$wblock:14983 | input validation |
| bne a0, a1, .LBB276_7 |
cond-branch
control-flow
|
$wblock:14988 | input validation |
| bne a1, a2, .LBB108_3 |
cond-branch
control-flow
|
c8DM:5306 | benign |
| bge a0, zero, .LBB108_5 |
cond-branch
control-flow
|
c8DM:5313 | benign |
| bne a0, a1, .LBB107_3 |
cond-branch
control-flow
|
c8Dw:5247 | benign |
| bge a2, a0, .LBB105_4 |
cond-branch
control-flow
|
c8zK:4874 | benign |
| bne a0, a2, .LBB277_3 |
cond-branch
control-flow
|
ccBH:15057 | benign |
| bne a2, a0, .LBB268_4 |
cond-branch
control-flow
|
ccCE:14649 | benign |
| bne a0, a1, .LBB270_4 |
cond-branch
control-flow
|
ccGr:14730 | benign |
| lw a3, 16(a3) |
reg-index
memory-address
|
ccGr:14747 | benign |
| lw a4, 16(a2) |
reg-index
memory-address
|
ccGr:14748 | benign |
| sw a3, 16(a2) |
reg-index
memory-address
|
ccGr:14750 | benign |
| beq a1, a0, .LBB270_5 |
cond-branch
control-flow
|
ccGr:14751 | benign |
| bltu a0, s10, .LBB106_3 |
cond-branch
control-flow
|
s6D4:5103 | benign |
| bltu a0, s10, .LBB266_3 |
cond-branch
control-flow
|
s74o:14353 | benign |
test cases
| block (key fix-vs-random) | class A derives one 64-byte keystream block from a fixed 32-byte key, class B from a fresh random key per draw; the 96-bit nonce and the block counter stay fixed in both classes, a per-draw key copy keeps buffer identity and cache history symmetric, and only key content differs |
| host | meter | result | p | effect (mean/pair) | jitter | clip scale | clipped |
|---|---|---|---|---|---|---|---|
| block (key fix-vs-random) | |||||||
| aarch64-darwin | wall | ✓ | 0.103 | [-40, 13] ns | 0.5% | 250 ns | 10/50000 at 16c |
| x86_64-linux | wall | ✓ | 0.618 | [-1327, 3981] ns | 47% | 24.96k ns | 1/50000 at 16c |
| x86_64-linux | instructions | ✓ | 0.916 | [-55, 18] instr | 2.8% | 342 instr | 114/50000 at 16c |
| x86_64-linux | cycles | ✓ | 0.060 | [-2961, 987] cyc | 12% | 18.57k cyc | 130/50000 at 16c |
wall
wall
2 further meter panels
instructions
cycles
chacha20_cipher_arm
chacha20_cipher_arm (cbits/chacha20_arm.c)sensitive: key| isa | result | symbols walked | instructions flagged |
|---|---|---|---|
| aarch64 | ✓ | 2 | 20 |
| class | assessment | note | aarch64 |
|---|---|---|---|
|
cond-branch
control-flow
|
benign |
fixed ten-round latches, and full-block and remainder-copy control on the public input length and buffer geometry
|
×15
|
|
reg-index
memory-address
|
benign |
input and output buffer addressing at the public sequential block cursor
|
×4
loop-latch, loop-exit
address-base ← entry (approximate)
address-index ← immediate (approximate)
|
|
cond-branch
control-flow
|
benign |
stack-protector epilogue check on the per-process __stack_chk_guard canary
|
×1
early-return-guard
← load (approximate)
|
complete fact list — aarch64 ×20
| b.hs LBB1_2 |
cond-branch
control-flow
|
_chacha20_cipher_arm:119 | benign |
| b.ne LBB1_4 |
cond-branch
control-flow
|
_chacha20_cipher_arm:177 | benign |
| ldp q16, q17, [x12] |
reg-index
memory-address
|
_chacha20_cipher_arm:184 | benign |
| ldp q18, q19, [x12, #32] |
reg-index
memory-address
|
_chacha20_cipher_arm:185 | benign |
| stp q5, q6, [x11] |
reg-index
memory-address
|
_chacha20_cipher_arm:189 | benign |
| stp q5, q4, [x11, #32] |
reg-index
memory-address
|
_chacha20_cipher_arm:192 | benign |
| b.ls LBB1_3 |
cond-branch
control-flow
|
_chacha20_cipher_arm:197 | benign |
| b.ls LBB1_23 |
cond-branch
control-flow
|
_chacha20_cipher_arm:200 | benign |
| b.ne LBB1_8 |
cond-branch
control-flow
|
_chacha20_cipher_arm:248 | benign |
| b.eq LBB1_23 |
cond-branch
control-flow
|
_chacha20_cipher_arm:258 | benign |
| b.lo LBB1_21 |
cond-branch
control-flow
|
_chacha20_cipher_arm:262 | benign |
| b.ls LBB1_21 |
cond-branch
control-flow
|
_chacha20_cipher_arm:266 | benign |
| b.hs LBB1_14 |
cond-branch
control-flow
|
_chacha20_cipher_arm:269 | benign |
| b.ne LBB1_15 |
cond-branch
control-flow
|
_chacha20_cipher_arm:295 | benign |
| cbz x11, LBB1_23 |
cond-branch
control-flow
|
_chacha20_cipher_arm:297 | benign |
| b.lo LBB1_21 |
cond-branch
control-flow
|
_chacha20_cipher_arm:300 | benign |
| b.ne LBB1_19 |
cond-branch
control-flow
|
_chacha20_cipher_arm:318 | benign |
| cbz x11, LBB1_23 |
cond-branch
control-flow
|
_chacha20_cipher_arm:320 | benign |
| b.lo LBB1_22 |
cond-branch
control-flow
|
_chacha20_cipher_arm:334 | benign |
| b.ne LBB1_25 |
cond-branch
control-flow
|
_chacha20_cipher_arm:344 | benign |
test cases
| cipher (key fix-vs-random, 320 B) | class A encrypts a fixed 320-byte plaintext through the shipped ARM dispatch with a fixed 32-byte key, class B with a fresh random key per draw; the 96-bit nonce, the block counter and the plaintext stay fixed in both classes, a per-draw key copy keeps buffer identity and cache history symmetric, and only key content differs (320 bytes is five exact keystream blocks, engaging the kernel's block loop with no partial tail) |
| host | meter | result | p | effect (mean/pair) | jitter | clip scale | clipped |
|---|---|---|---|---|---|---|---|
| cipher (key fix-vs-random, 320 B) | |||||||
| aarch64-darwin | wall | ✓ | 0.438 | [-8.82, 26] ns | 0.2% | 166 ns | 22/50000 at 16c |
wall
chacha20_block_arm
chacha20_block_arm (cbits/chacha20_arm.c)sensitive: key| isa | result | symbols walked | instructions flagged |
|---|---|---|---|
| aarch64 | ✓ | 1 | 1 |
| class | assessment | note | aarch64 |
|---|---|---|---|
|
cond-branch
control-flow
|
benign |
fixed ten-iteration ChaCha double-round loop latch
|
×1
loop-latch, loop-exit, early-return-guard
← counter
|
complete fact list — aarch64 ×1
| b.ne LBB0_1 |
cond-branch
control-flow
|
_chacha20_block_arm:64 | benign |
test cases
| block (key fix-vs-random, ARM kernel) | the darwin lane of the block case, attributed to the kernel: on aarch64-darwin the shipped dispatch routes block to the NEON kernel, so the same run exercises chacha20_block_arm (class A derives one 64-byte keystream block from a fixed 32-byte key, class B from a fresh random key per draw; the 96-bit nonce and the block counter stay fixed in both classes, and only key content differs) |
| host | meter | result | p | effect (mean/pair) | jitter | clip scale | clipped |
|---|---|---|---|---|---|---|---|
| block (key fix-vs-random, ARM kernel) | |||||||
| aarch64-darwin | wall | ✓ | 0.103 | [-40, 13] ns | 0.5% | 250 ns | 10/50000 at 16c |
wall
analysis bounds
- lines parsed
- 18849 (11393 instruction lines)
- unstructured
- 17 instruction lines the decoder did not structure: ldurb ×5, sturb ×12
- memory unmodelled
- 17 memory-capable instructions with no modelled effect: ldurb ×5, sturb ×12
- control-flow graph
- 508 functions, 0 with a reachable unknown successor
- facts by disposition
- active ×63, suppressed ×686
- fact operands
- 749 of 749 facts with a modelled subject, 0 unmodelled, 0 with none by design; 763 subjects: address-base ×14, address-index ×14, branch-condition ×397, indirect-target ×337, work-count ×1
- operand chains
- exact ×669, approximate ×94 — an approximate chain bounds what the subject's provenance establishes, and is marked as such beside the finding
- walk boundary · cipher
- 93 edges — indirect-call ×49, external-callee ×29, unresolved-enter ×15; at: _stg_gc_unpt_r1 ×9, _stg_gc_noregs ×5, _stg_ap_pv_fast ×3, bytestring:Data.ByteString.Builder.Internal.toLazyByteString_info ×2, bytestring:Data.ByteString.Lazy.Internal.$wgoLen_info ×2, _stg_newPinnedByteArrayzh ×1, _stg_ap_0_fast ×1, _memcpy ×1
- walk boundary · block
- 57 edges — indirect-call ×34, external-callee ×14, unresolved-enter ×9; at: _stg_gc_unpt_r1 ×5, _stg_ap_pv_fast ×2, _stg_gc_noregs ×2, bytestring:Data.ByteString.Lazy.Internal.$wgoLen_info ×1, ppad-chacha:Crypto.Cipher.ChaCha20.Arm.$wblock_info ×1, _newCAF ×1, ghc-internal:GHC.Internal.Err.error_info ×1, bytestring:Data.ByteString.Builder.Internal.toLazyByteString_info ×1
- walk boundary · chacha20_cipher_arm
- 1 edge — external-callee ×1; at: ___stack_chk_fail ×1
- lines parsed
- 19964 (13831 instruction lines)
- unstructured
- 0 instruction lines the decoder did not structure
- memory unmodelled
- 0 memory-capable instructions with no modelled effect
- control-flow graph
- 994 functions, 241 with a reachable unknown successor
- facts by disposition
- active ×94, suppressed ×653
- fact operands
- 747 of 747 facts with a modelled subject, 0 unmodelled, 0 with none by design; 761 subjects: address-base ×14, address-index ×14, branch-condition ×399, indirect-target ×333, work-count ×1
- operand chains
- exact ×727, approximate ×34 — an approximate chain bounds what the subject's provenance establishes, and is marked as such beside the finding
- walk boundary · cipher
- 120 edges — indirect-tail ×47, external-callee ×27, unresolved-edge-target ×23, unresolved-enter ×23; at: stg_gc_unpt_r1 ×9, bytestring:Data.ByteString.Internal.Type.BS_con_info ×6, stg_gc_noregs ×5, ghc-internal:GHC.Internal.ForeignPtr.PlainPtr_con_info ×4, stg_upd_frame_info ×4, bytestring:Data.ByteString.Builder.Internal.BufferRange_con_info ×3, stg_ap_pv_fast ×3, stg_ARR_WORDS_info ×3
- no structural role · cipher
- 16 sites (incomplete-cfg ×16, incomplete-side-region ×14)
- walk boundary · block
- 72 edges — indirect-tail ×32, unresolved-edge-target ×14, unresolved-enter ×14, external-callee ×12; at: stg_gc_unpt_r1 ×5, bytestring:Data.ByteString.Internal.Type.BS_con_info ×3, stg_upd_frame_info ×3, bytestring:Data.ByteString.Builder.Internal.BufferRange_con_info ×2, stg_ap_pv_fast ×2, ghc-internal:GHC.Internal.ForeignPtr.PlainPtr_con_info ×2, stg_gc_noregs ×2, stg_ARR_WORDS_info ×2
- no structural role · block
- 10 sites (incomplete-cfg ×10, incomplete-side-region ×10)
- lines parsed
- 20236 (13574 instruction lines)
- unstructured
- 0 instruction lines the decoder did not structure
- memory unmodelled
- 0 memory-capable instructions with no modelled effect
- control-flow graph
- 497 functions, 38 with a reachable unknown successor
- facts by disposition
- active ×47, suppressed ×698
- fact operands
- 745 of 745 facts with a modelled subject, 0 unmodelled, 0 with none by design; 757 subjects: address-base ×12, address-index ×12, branch-condition ×400, indirect-target ×332, work-count ×1
- operand chains
- exact ×670, approximate ×87 — an approximate chain bounds what the subject's provenance establishes, and is marked as such beside the finding
- walk boundary · cipher
- 98 edges — indirect-tail ×47, external-callee ×27, unresolved-enter ×24; at: stg_gc_unpt_r1@PLT ×9, stg_gc_noregs@PLT ×5, stg_ap_pv_fast@PLT ×3, bytestring:Data.ByteString.Builder.Internal.toLazyByteString_info ×2, bytestring:Data.ByteString.Lazy.Internal.$wgoLen_info ×2, stg_newPinnedByteArrayzh@PLT ×1, stg_ap_0_fast@PLT ×1, memcpy@PLT ×1
- no structural role · cipher
- 14 sites (incomplete-cfg ×14, incomplete-side-region ×8)
- walk boundary · block
- 59 edges — indirect-tail ×32, unresolved-enter ×15, external-callee ×12; at: stg_gc_unpt_r1@PLT ×5, stg_ap_pv_fast@PLT ×2, stg_gc_noregs@PLT ×2, bytestring:Data.ByteString.Lazy.Internal.$wgoLen_info ×1, ppad-chacha:Crypto.Cipher.ChaCha20.Arm.$wblock_info ×1, bytestring:Data.ByteString.Builder.Internal.toLazyByteString_info ×1
- no structural role · block
- 8 sites (incomplete-cfg ×8, incomplete-side-region ×5)
provenance
- target
- ppad-chacha 0.2.2 (80cc745)
- substratum
- 0.3.7 (2d3b67b)
- censor
- 0.4.4 (9b550af)
- elenchus
-
(appraise) gpt-5.6-sol(dissent) gpt-5.6-sol(falsify) claude-fable-5(distill) claude-fable-5
- source
- git.ppad.tech/chacha
- seed
- 0x5eed5eed5eed5eed
- censor config
- alpha=1.000000e-6, budget=50000, warmup=500, batch=per-case
- aarch64-darwin · wall
- Apple M4 (10 cores) · darwin 24.6.0 · 2026-08-08T18:31:51Z
- x86_64-linux · cycles
- AMD EPYC 7713 64-Core Processor (6 cores) · linux 6.18.33 · 2026-08-08T18:38:27Z
- x86_64-linux · instructions
- AMD EPYC 7713 64-Core Processor (6 cores) · linux 6.18.33 · 2026-08-08T18:38:12Z
- x86_64-linux · wall
- AMD EPYC 7713 64-Core Processor (6 cores) · linux 6.18.33 · 2026-08-08T18:38:41Z
- aarch64-darwin · wall
- 3 cases · 150000 pairs · 0 reject · α=1.0e-6 · family α=3.0e-6
- x86_64-linux · cycles
- 2 cases · 100000 pairs · 0 reject · α=1.0e-6 · family α=2.0e-6
- x86_64-linux · instructions
- 2 cases · 100000 pairs · 0 reject · α=1.0e-6 · family α=2.0e-6
- x86_64-linux · wall
- 2 cases · 100000 pairs · 0 reject · α=1.0e-6 · family α=2.0e-6
# substratum 0.3.7 (2d3b67b) # toolchain: GHC 9.10.3, LLVM 19.1.7 (nix-pinned via flake) # ppad-chacha v0.2.2 (80cc745; lib source == v0.2.2 tag, # only flake/cabal metadata differs) substratum scan -i audit-chacha/aarch64/asm/ChaCha20.llvm.s --isa aarch64 --runtime haskell --format json --why --decisions --loops substratum scan -i audit-chacha/aarch64/asm/ChaCha20.llvm.s --isa aarch64 --runtime haskell --format json --why -s 'ppad-chacha:Crypto.Cipher.ChaCha20:cipher' substratum scan -i audit-chacha/aarch64/asm/ChaCha20.llvm.s --isa aarch64 --runtime haskell --format json --why -s 'ppad-chacha:Crypto.Cipher.ChaCha20:block' # cbits: the ARM NEON kernel cbits/chacha20_arm.c ships in the # same library but is compiled by the C toolchain, not GHC. The # dump, whole-file scan, and rooted walks below present it as # its own static lane (generic runtime, backend "cc"). # C compiler: clang 21.1.2 (nix clang-wrapper-21.1.2, the # devshell cc the shipped build uses), target # arm64-apple-darwin, apple-sdk 14.4. Codegen flags per the # cabal build (-optc-O2 -optc-march=armv8-a via GHC's driver): cc -O2 -march=armv8-a -S cbits/chacha20_arm.c -o audit-chacha/aarch64/asm/chacha20_arm.c.s substratum scan -i audit-chacha/aarch64/asm/chacha20_arm.c.s --isa aarch64 --runtime generic --format json --why --decisions --loops substratum scan -i audit-chacha/aarch64/asm/chacha20_arm.c.s --isa aarch64 --runtime generic --format json --why -s '_chacha20_cipher_arm' substratum scan -i audit-chacha/aarch64/asm/chacha20_arm.c.s --isa aarch64 --runtime generic --format json --why -s '_chacha20_block_arm'
# substratum 0.3.7 (2d3b67b) # toolchain: GHC 9.10.3, LLVM 19.1.7 (nix-pinned via flake) # ppad-chacha v0.2.2 (80cc745; lib source == v0.2.2 tag, # only flake/cabal metadata differs) substratum scan -i audit-chacha/rv64/asm/ChaCha20.llvm.s --isa riscv64 --runtime haskell --format json --why --decisions --loops substratum scan -i audit-chacha/rv64/asm/ChaCha20.llvm.s --isa riscv64 --runtime haskell --format json --why -s 'ppad-chacha:Crypto.Cipher.ChaCha20:cipher' substratum scan -i audit-chacha/rv64/asm/ChaCha20.llvm.s --isa riscv64 --runtime haskell --format json --why -s 'ppad-chacha:Crypto.Cipher.ChaCha20:block'
# substratum 0.3.7 (2d3b67b) # toolchain: GHC 9.10.3, LLVM 19.1.7 (nix-pinned via flake) # ppad-chacha v0.2.2 (80cc745; lib source == v0.2.2 tag, # only flake/cabal metadata differs) substratum scan -i audit-chacha/x86_64/asm/ChaCha20.llvm.s --isa x86_64 --runtime haskell --format json --why --decisions --loops substratum scan -i audit-chacha/x86_64/asm/ChaCha20.llvm.s --isa x86_64 --runtime haskell --format json --why -s 'ppad-chacha:Crypto.Cipher.ChaCha20:cipher' substratum scan -i audit-chacha/x86_64/asm/ChaCha20.llvm.s --isa x86_64 --runtime haskell --format json --why -s 'ppad-chacha:Crypto.Cipher.ChaCha20:block'
# censor 0.4.4 (9b550af) # toolchain: GHC 9.10.3, LLVM 19.1.7 (nix-pinned via flake) # # aarch64-darwin (Apple M4), niced; wall only -- no userspace PMU. nice -n 5 censor-chacha --format plain wall \ aarch64-darwin/wall/report.json # # x86_64-linux (AMD EPYC 7713), pinned and niced. taskset -c 0 nice -n 5 censor-chacha --format plain instructions \ x86_64-linux/instructions/report.json taskset -c 0 nice -n 5 censor-chacha --format plain cycles \ x86_64-linux/cycles/report.json taskset -c 0 nice -n 5 censor-chacha --format plain wall \ x86_64-linux/wall/report.json