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

seq_printf forwards __va_list into ksnprintf variadic parameter instead of using kvsnprintf

Summary

seq_printf() builds __va_list ap from variadic args then calls ksnprintf(m->buf m->size f ap) (linux_printf.c:37). ksnprintf is declared __printflike(3 4) variadic function. Passing __va_list to ... parameter does NOT forward original arguments: on x86-64 SysV __va_list is array of one struct decays to pointer-to-struct consumed by ksnprintf as FIRST variadic argument. Correct API is kvsnprintf(char* size_t const char* __va_list). ksnprintf own __va_start sees pointer-to-va_list struct as variadic slot 0 (lands in RCX after buf/size/fmt consume RDI/RSI/RDX). __va_arg calls read: 1st conversion pointer-to-va_list struct reinterpreted as T (char*/void*/int/long) 2nd+ conversions stale register/stack contents from seq_printf frame. None of caller actual variadic arguments ever reach ksnprintf. Fix is one token: kvsnprintf. Bug currently LATENT zero runtime reachability: CONFIG_DEBUG_FS undefined so all debugfs show callbacks compiled out under #ifdef CONFIG_DEBUG_FS. Only compiled-in callers gen8/gen6_dump_ppgtt (debug_dump function pointer never invoked) and drm_dp_downstream_debug (EXPORT_SYMBOL zero in-tree callers). If enabled: %p prints kernel-stack address (KASLR-defeating) %s walks bytes at &ap. No in-tree format literal attacker-controlled. Also seq_printf always writes from buf[0] discards ksnprintf return no append/cursor tracking (functional bug not memory-safety). [VERIFIED pass2: reproduced - va_list forwarded into variadic param garbles every conversion per ABI prediction, percent-p prints a kernel stack address 0xfffff8011800b650 as the leak flavor, canaries intact so bound-respecting; LATENT in stock - CONFIG_DEBUG_FS undefined kills all callers; kvsnprintf fix-validated exact strings]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2224 Β· 14 files
FileTypeDescriptionSize
rig_head.c rig-source shared batch rig head 2.0 KB view raw
rig_mid.c rig-source verbatim production includes + fixed prototypes 1.1 KB view raw
rig_tail.c rig-source meter + canary-bracketed seq_printf cases, baseline vs fixed with identical variadic args 18.1 KB view raw
build.sh build-script provenance chain + fixed-variant extraction + kernel-compile 121 B view raw
run.sh run-script sysctl trigger + dmesg extraction 172 B view raw
fix.diff suggested-fix one token: ksnprintf -> kvsnprintf (correct va_list forwarding) 217 B view raw
build.log build-log FULL untrimmed nativekernel DFV22X build 5.5 MB ↓ download
run.log run-log FULL decisive rig output 7.8 KB view raw
run_section.log run-log DF-2224 lines only 1.3 KB view raw
env.txt environment guest env + hashes 2.1 KB view raw
VERDICT.md verdict full narrative incl. ABI derivation, 3-run leak stability, latency analysis 4.9 KB ↓ raw
README.md readme build/run/expected 4.0 KB ↓ raw
manifest.json manifest this file 2.5 KB view raw
verdict.json verdict machine verdict for persist_poc.py 3.6 KB view raw
README.md readme build/run/expected
↓ download raw

DF-2224 β€” seq_printf() forwards __va_list into ksnprintf instead of kvsnprintf

Status: REPRODUCED (verbatim-code, kernel-compiled rig) β€” demonstrated primitive: every conversion garbled + %p prints a kernel stack address (KASLR-defeating leak flavor) into the seq buffer. LATENT in the stock tree (CONFIG_DEBUG_FS undefined β†’ i915 show callbacks compiled out; drm_dp_downstream_debug has zero in-tree callers). Fix: A/B-validated in the same harness kernel (fix.diff, one token: ksnprintf β†’ kvsnprintf).

Part of the DF-22X batch (DF-2216 / DF-2220 / DF-2223 / DF-2224) verified in ONE harness kernel; this pack documents the DF-2224 section.

What this pack contains

Same layout as the DF-2216 pack; finding-specific: fix.diff (against sys/dev/drm/linux_printf.c), run_section.log (DF-2224 lines).

How to reproduce

Identical to the DF-2216 pack (same batch build + run).

Provenance

  1. guest tree linux_printf.c sha256 a0570e25... == audited tree; seq_printf compiled into the kernel verbatim.
  2. seq_printf_fixed = same bytes + fix.diff, def renamed (fixed_printf.c sha256 553ca5b7...).
  3. Each case calls baseline and fixed back-to-back with identical variadic arguments into separate 64-byte buffers bracketed by 32-byte 0xCC canary zones β€” exactly how production callers invoke the function (direct variadic calls; a va_list trampoline would have changed the register setup).

Expected output (baseline = unmodified production code)

DF2224 BASELINE [int fmt ...] buf=[402699856/48879/5f5e100] canaries=INTACT ...
DF2224 FIXED    [int fmt ...] buf=[111/222222/beef] canaries=INTACT ...
DF2224 BASELINE [%p of 0xdeadbeef ...] buf=[ptr=0xfffff8011800b650] canaries=INTACT ...
DF2224 FIXED    [%p of 0xdeadbeef] buf=[ptr=0xdeadbeef] canaries=INTACT ...
DF2224 witness: rig kernel stack address for comparison: 0xfffff8011800b6ec ...
DF2224 BASELINE [%s of hi] buf=[s=[^P]] canaries=INTACT ...
DF2224 FIXED    [%s of hi] buf=[s=[hi]] canaries=INTACT ...

What the baseline output proves (x86-64 SysV ABI)

__va_list is an array of one struct that decays to a pointer; passing it as ksnprintf's 4th C argument puts the pointer itself into ksnprintf's first variadic slot (rcx):

  • %d (first conversion) prints the low 32 bits of &ap β€” a kernel stack address (402699856 = 0x17FFB7F0-ish; run-dependent with the stack address).
  • Subsequent conversions read stale registers: 48879 (0xbeef) is the caller's third argument arriving in the second slot via leftover r8; 5f5e100 is stale r9 junk. The intended 111/222222 never print.
  • %p prints the actual &ap kernel stack address β€” 0xfffff8011800b650, just 0x9c bytes from the rig's witness frame (0xfffff8011800b6ec). Across 3 runs the printed value is always a canonical kernel stack address in the same thread stack (0xfffff801166db650 / 0xfffff8011800b650 / 0xfffff80118005650) β€” the leak-flavor primitive, stable in shape.
  • %s consumes &ap as a char *; the va_list struct begins with gp_offset = 16 (two named GP args before the varargs), so it prints the single byte 0x10 (^P) β€” a deterministic ABI artifact, not hi.
  • Canary zones INTACT in every case: both ksnprintf and kvsnprintf bound-respect; the bug is a forwarding/garbage bug, not an overflow.

Attacker input / reachability (honest: LATENT)

seq_printf callers compiled in the stock tree: the i915 debugfs show callbacks (i915_gem_gtt.c:1540+, gen8/gen6 dump) are all inside #if defined(CONFIG_DEBUG_FS) and kconfig.h does not define it; drm_dp_downstream_debug (drm_dp_helper.c:586-662, EXPORT_SYMBOL) has zero in-tree callers. So today there is no runtime trigger on a stock system β€” the defect, its garbage output, and its kernel-stack-address disclosure are proven through the verbatim function; any future port enabling debugfs-style callers would light it up. No write primitive β†’ no privesc route; severity stays Low.

VERDICT.md verdict full narrative incl. ABI derivation, 3-run leak stability, latency analysis
↓ download raw

DF-2224 β€” VERDICT

REPRODUCED β€” verbatim-code, kernel-compiled rig; the misforwarding is proven byte-for-byte, including the kernel-stack-address leak flavor of %p; fix A/B-validated in the same kernel (one token). LATENT in the stock tree: no compiled-in caller exists today. Impact: if ever called, garbled output plus a kernel stack address disclosure (KASLR-defeating); currently zero stock-system reachability. No write primitive, no privesc route (see below).

Rig class and why

Same batch harness kernel as DF-2216 (DFV22X); the production sys/dev/drm/linux_printf.c is #included verbatim (sha-gated a0570e25...). Baseline and fixed are called back-to-back with identical variadic arguments into separate canary-bracketed buffers.

Line-accurate confirmation

linux_printf.c:37: ksnprintf(m->buf, m->size, f, ap); β€” ksnprintf is __printflike(3,4) variadic; passing an __va_list into a ... parameter does NOT forward the original arguments. The correct API (used by the verbatim kvasprintf two files over) is kvsnprintf(char *, size_t, const char *, __va_list). The fix is exactly one token.

Mechanical reproduction (decisive run, run.log) β€” with ABI derivation

On x86-64 SysV, seq_printf(m, f, args...) receives args in rdi/rsi/rdx/rcx/r8/r9; its ap points into its register-save-area. The call ksnprintf(buf, size, f, ap) puts the ap pointer value in rcx β€” ksnprintf's first variadic slot. Every conversion therefore reads the pointer / stale registers, never the caller's arguments:

case BASELINE (verbatim) FIXED (one token)
"%d/%lu/%x", 111, 222222UL, 48879 402699856/48879/5f5e100 111/222222/beef
"ptr=%p", 0xdeadbeef ptr=0xfffff8011800b650 ptr=0xdeadbeef
"s=[%s]", "hi" s=[^P] (single byte 0x10) s=[hi]
  • First %d = low 32 bits of &ap (kernel stack address; run-dependent).
  • %p prints the full &ap β€” a kernel stack address, observed 0xfffff8011800b650 vs the rig witness frame 0xfffff8011800b6ec (0x9c bytes apart, same thread stack). Stable in shape across 3 runs: 0xfffff801166db650, 0xfffff8011800b650, 0xfffff80118005650 β€” always a canonical kernel-stack address in seq_printf's own frame region. That is the KASLR-defeating disclosure primitive the finding predicted for %p.
  • %s prints exactly one byte, 0x10 = the va_list struct's gp_offset (16, because m and f occupy rdi/rsi) β€” a fully deterministic ABI artifact proving the formatter is walking the va_list struct bytes, not the arguments.
  • The second/third conversions show the stale-register shuffle: 48879 (0xbeef, the caller's third argument, leftover in r8) arrives in the second slot while the intended second argument 222222 is lost β€” nothing the caller passed arrives where it should.
  • Canary zones (32 B of 0xCC on both sides of the 64 B seq buffer) are INTACT in every case: both formatters bound-respect β€” this is a forwarding bug, not an overflow.

Fix validation (A/B, same kernel)

fix.diff (one token, ksnprintf β†’ kvsnprintf) extracted-from-patched-bytes as seq_printf_fixed in the same kernel: all three cases produce the exact intended strings, canaries intact, identical call pattern. fix_status: fixed. (Note the adjacent functional bug β€” seq_printf always writes from buf[0] and discards the length return, i.e. no append/cursor semantics β€” is unchanged by this fix; it is a functional divergence, not memory-unsafe, and is noted for upstream.)

Reachability (honest: LATENT)

Compiled-in callers in the stock tree: the i915 debugfs show callbacks (i915_gem_gtt.c:1540+, :1592+, gen8/gen6 dump paths) all sit inside #if defined(CONFIG_DEBUG_FS); sys/dev/drm/kconfig.h does not define it. drm_dp_downstream_debug (drm_dp_helper.c:586-662) is compiled and EXPORT_SYMBOLed but has zero in-tree callers. Therefore no stock runtime trigger exists today; the demonstrated primitive requires a future/ported caller (any debugfs-enable or new consumer of the shim). Defect, garbage output, and stack-address disclosure are all proven through the verbatim function. Filed Low severity stands.

Privesc assessment (standing directive)

None. The bug mis-reads varargs (garbage in, bounded write out). The write is always confined to m->buf[0..m->size) by ksnprintf's bounds; the only security-relevant consequence is the demonstrated kernel-stack-address disclosure (and output corruption). Even with the leak, this guest and stock systems have no caller to invoke it, and a stack-address leak alone is not an escalation primitive.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
↓ fix.diffper-fix-DF-2224

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log lines DF2224 BASELINE/FIXED: baseline buf=[402699856/48879/5f5e100] vs fixed buf=[111/222222/beef]; %p baseline buf=[ptr=0xfffff8011800b650] vs rig witness 0xfffff8011800b6ec (same stack, 0x9c apart) vs fixed ptr=0xdeadbeef; %s baseline s=[0x10] vs fixed s=[hi]; canaries=INTACT everywhere; third run (notes) shows the same stack-shaped %p value

PoC changes

Rebuilt as verbatim-code kernel-compiled rig; PoC sketch rewritten to call the variadic baseline and fixed functions directly with identical arguments (a va_list trampoline would have changed the register setup); 64-byte canary-bracketed seq buffer; witness stack address printed for the %p leak comparison

Verified recommended fix

Forward the va_list with kvsnprintf instead of ksnprintf (one token, see fix.diff)

Verdict

Reproduced mechanically with the verbatim production seq_printf(): on x86-64 SysV the __va_list decays to a pointer that lands in ksnprintfs first variadic slot, so no caller argument ever reaches the formatter. With identical variadic arguments the baseline produces 402699856/48879/5f5e100 where the fixed copy (kvsnprintf, one token from fix.diff in the same kernel) produces 111/222222/beef; the first baseline conversion is the low 32 bits of the va_list address (a kernel stack address). The leak flavor is directly demonstrated: seq_printf(m, ptr=%p, 0xdeadbeef) prints a kernel stack address (0xfffff8011800b650) instead of the argument, landing 0x9c bytes from the rig witness stack address in the same thread stack, stable in canonical kernel-stack shape across 3 runs; %s prints the single byte 0x10 (the va_list struct gp_offset, deterministic ABI artifact) instead of hi. Canary zones on both sides of the 64-byte seq buffer are intact in every case (ksnprintf/kvsnprintf bound-respect, no overflow). LATENT in the stock tree: CONFIG_DEBUG_FS is undefined so every i915 debugfs show callback is compiled out and drm_dp_downstream_debug has zero in-tree callers - the demonstrated primitive requires a future/ported caller. No write primitive, no privesc chain; ceiling is a kernel-stack-address disclosure (KASLR-defeating) plus garbage output if any caller is ever enabled. Fix validated A/B in the same kernel.