ppad measure
constant-time analysis

Constant-Time Analysis: ppad-chacha

target
ppad-chacha 0.2.2
runtime
haskell (GHC 9.10.3 · LLVM 19.1.7), generic (clang 21.1.2)
architectures
aarch64, x86_64, riscv64
meters
wall, instructions, cycles
updated
2026-08-09
auditor
Jared Tobin (jared@ppad.tech)

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)
SC SC SC·
block
ChaCha20 keystream block function (RFC 8439)
SC SC SC·
chacha20_cipher_arm
ARM NEON kernel backing cipher on NEON-capable ARM hosts
SC S·C· S·C·
chacha20_block_arm
ARM NEON kernel backing block on NEON-capable ARM hosts
SC S·C· S·C·

no leak detected   leak   qualified   ? inconclusive   · not run    S = substratum   C = censor

method
adversary model

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.

security property

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.

static analysis

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).
dynamic analysis

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.

results

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
substratumllvm
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
aarch64
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
x86_64
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
riscv64
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
censor50000 pairs · α=1.0e-6 · batch 1
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
cipher (key fix-vs-random, 245 B)aarch64-darwin
wall
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.120
mean per-pair effect [-8.93, 27] ns · clip scale c=168 ns · clipped 20/50000 at 16c · batch 1 · baseline med 792 iqr 42 (5.3% jitter)
cipher (key fix-vs-random, 245 B)x86_64-linux
wall
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.111
mean per-pair effect [-63k, 21k] ns · clip scale c=392k ns · clipped 0/50000 at 16c · batch 1 · baseline med 27.22k iqr 7880 (29% jitter)
2 further meter panels
cipher (key fix-vs-random, 245 B)x86_64-linux
instructions
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.253
mean per-pair effect [-2416, 805] instr · clip scale c=15.15k instr · clipped 4/50000 at 16c · batch 1 · baseline med 16.55k iqr 116 (0.7% jitter)
cipher (key fix-vs-random, 245 B)x86_64-linux
cycles
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.541
mean per-pair effect [-6593, 2198] cyc · clip scale c=41.34k cyc · clipped 7/50000 at 16c · batch 1 · baseline med 18.62k iqr 450 (2.4% jitter)

block

Crypto.Cipher.ChaCha20.blocksensitive: key
substratumllvm
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
aarch64
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
x86_64
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
riscv64
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
censor50000 pairs · α=1.0e-6 · batch 1
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
block (key fix-vs-random)aarch64-darwin
wall
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.103
mean per-pair effect [-40, 13] ns · clip scale c=250 ns · clipped 10/50000 at 16c · batch 1 · baseline med 208 iqr 1.00 (0.5% jitter)
block (key fix-vs-random)x86_64-linux
wall
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.618
mean per-pair effect [-1327, 3981] ns · clip scale c=24.96k ns · clipped 1/50000 at 16c · batch 1 · baseline med 430 iqr 200 (47% jitter)
2 further meter panels
block (key fix-vs-random)x86_64-linux
instructions
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.916
mean per-pair effect [-55, 18] instr · clip scale c=342 instr · clipped 114/50000 at 16c · batch 1 · baseline med 3357 iqr 95 (2.8% jitter)
block (key fix-vs-random)x86_64-linux
cycles
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.060
mean per-pair effect [-2961, 987] cyc · clip scale c=18.57k cyc · clipped 130/50000 at 16c · batch 1 · baseline med 6524 iqr 801 (12% jitter)

chacha20_cipher_arm

chacha20_cipher_arm (cbits/chacha20_arm.c)sensitive: key
substratumcc
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
aarch64
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
censor50000 pairs · α=1.0e-6 · batch 1
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
cipher (key fix-vs-random, 320 B)aarch64-darwin
wall
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.438
mean per-pair effect [-8.82, 26] ns · clip scale c=166 ns · clipped 22/50000 at 16c · batch 1 · baseline med 542 iqr 1.00 (0.2% jitter)

chacha20_block_arm

chacha20_block_arm (cbits/chacha20_arm.c)sensitive: key
substratumcc
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
aarch64
b.ne LBB0_1
cond-branch
control-flow
_chacha20_block_arm:64 benign
censor50000 pairs · α=1.0e-6 · batch 1
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
block (key fix-vs-random, ARM kernel)aarch64-darwin
wall
log(1/α) 0 0
no leak detected · 50000 pairs · α=1.0e-6 · p=0.103
mean per-pair effect [-40, 13] ns · clip scale c=250 ns · clipped 10/50000 at 16c · batch 1 · baseline med 208 iqr 1.00 (0.5% jitter)
analysis bounds
aarch64
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
riscv64
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)
x86_64
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
run environments
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
run totals
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