chacha_encrypt_bytes() leaves final partial-block plaintext in tmp[64] on the kernel stack (wg outbound packet tails via chachapoly)
Summary
The sub-64-byte tail path of chacha_encrypt_bytes() copies caller data into the 64-byte stack buffer tmp, encrypts, copies 'bytes' back to ctarget, and returns without clearing tmp - up to 63 bytes of plaintext remain resident in the kernel stack frame. Reachable on every wg message whose length is not a multiple of 64 (chachapoly.c:170/174 -> chacha.c partial-block path); on the encrypt direction this is the outbound packet's plaintext tail. The csprng (KEYSTREAM_ONLY) configuration only ever holds keystream there, not message data. Not independently exploitable - requires a separate kernel-stack-disclosure primitive; defense-in-depth. Inherited from upstream Bernstein chacha-merged 20080118 / OpenBSD lineage (no DFly-local divergence). Primitive correctness proven live in the same run: RFC 8439 2.3.2/2.4.2, xchacha 2.2.1, 35 random vectors vs an independent Python reference incl. 64-bit counter wrap, BOTH kernel macro-configurations (plain wg build + csprng CHACHA_EMBED build) tested; 917 checks 0 failures across the 6 crypto suites this batch.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3563 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 2.5 KB | β raw | |
| build.log | file | 2.0 KB | view raw | |
| fix.diff | file | 374 B | view raw | |
| run.log | file | 2.8 KB | view raw | |
| verdict.json | file | 577 B | view raw |
DF-3563 β chacha_encrypt_bytes leaves partial-block plaintext in tmp[64] on the kernel stack
File: sys/crypto/chacha20/chacha.c (pass 2, GLM 5.3)
Severity: Info (hardening / defense-in-depth) β CWE-226
Root cause
chacha_encrypt_bytes() (sys/crypto/chacha20/chacha.c:110-261) handles a
final partial block (bytes < 64, chacha.c:139-148) by copying the
message tail into a 64-byte stack buffer tmp (chacha.c:116), encrypting
into tmp, copying bytes out to ctarget (chacha.c:243-246), and
returning without clearing tmp. Up to 63 bytes of the caller's
buffer contents β plaintext in the caller's frame β remain in the current
kernel stack frame after return, available to any later (separate,
independent) kernel-stack-disclosure primitive on the same CPU/thread.
This matches upstream (Bernstein chacha-merged 20080118 / OpenBSD chacha.c r1.1 lineage) β it is an inherited hardening gap, not a DFly regression.
Reachability (live-confirmed consumers)
sys/crypto/chachapoly.c:170/174(_chacha20poly1305_final) callschacha_encrypt_bytes(..., in_len % 64)for every wg message whose data length is not a multiple of 64 β i.e. nearly all real traffic (sys/net/wg/wg_noise.ctransport path;wg_cookie.ccookie path). On the encrypt direction,tmpretains the outbound packet's plaintext tail; on decrypt, hostile-chosen ciphertext (not secret).sys/kern/subr_csprng.c:155(KEYSTREAM_ONLY embed build): theKEYSTREAM_ONLYconfiguration never copies message data intotmp(chacha.c:143-145) β only keystream (the RNG output itself) passes through; the RNG tail buffer retains delivered-by-construction output.- rmd160.c / poly1305.c equivalents wipe properly:
RMD160Finalmemsets the whole context (rmd160.c:156, proven live);poly1305_finishzeroes the key-bearing h/r/pad state (poly1305.c:287-300, proven live).
No exploit on its own: requires a separate stack-content disclosure bug
to matter. Not memory-unsafe, not reachable as UAF/OOB; the primitive
itself was KAT-verified clean (917 checks, see
findings/poc/neg-evidence-crypto-pass2/).
Fix
explicit_bzero(tmp, sizeof(tmp)) before the return at chacha.c:253
(see fix.diff).
Verification
Info hardening finding β Phase V PoC skipped per audit contract (no
runtime manifestation to reproduce; correctness of the primitive
verified in the same run via the KAT harness in
neg-evidence-crypto-pass2/). Fix validation not run (fix_status:
not_tested).
Confirmed kernel references
β
Detail
Evidence (decisive lines)
findings/poc/neg-evidence-crypto-pass2/run.log
Verdict
Info hardening - Phase V skipped per contract (precedent DF-3560). Reachability established by code path (wg transport encrypt partial-block tail). Primitive correctness proven live in the same run (917 checks 0 failures, findings/poc/neg-evidence-crypto-pass2/).
No comments yet.