β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3563

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3563 Β· 5 files
FileTypeDescriptionSize
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
README.md file
↓ download 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) calls chacha_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.c transport path; wg_cookie.c cookie path). On the encrypt direction, tmp retains the outbound packet's plaintext tail; on decrypt, hostile-chosen ciphertext (not secret).
  • sys/kern/subr_csprng.c:155 (KEYSTREAM_ONLY embed build): the KEYSTREAM_ONLY configuration never copies message data into tmp (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: RMD160Final memsets the whole context (rmd160.c:156, proven live); poly1305_finish zeroes 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/).