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)
PoC verification
Evidence pack
findings/poc/DF-2224 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- guest tree
linux_printf.csha256a0570e25...== audited tree;seq_printfcompiled into the kernel verbatim. seq_printf_fixed= same bytes +fix.diff, def renamed (fixed_printf.csha256553ca5b7...).- Each case calls baseline and fixed back-to-back with identical variadic
arguments into separate 64-byte buffers bracketed by 32-byte
0xCCcanary 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;5f5e100is stale r9 junk. The intended111/222222never print. %pprints the actual&apkernel 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.%sconsumes&apas achar *; the va_list struct begins withgp_offset = 16(two named GP args before the varargs), so it prints the single byte0x10(^P) β a deterministic ABI artifact, nothi.- Canary zones INTACT in every case: both
ksnprintfandkvsnprintfbound-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.
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). %pprints the full&apβ a kernel stack address, observed0xfffff8011800b650vs the rig witness frame0xfffff8011800b6ec(0x9c bytes apart, same thread stack). Stable in shape across 3 runs:0xfffff801166db650,0xfffff8011800b650,0xfffff80118005650β always a canonical kernel-stack address inseq_printf's own frame region. That is the KASLR-defeating disclosure primitive the finding predicted for%p.%sprints exactly one byte,0x10= the va_list struct'sgp_offset(16, becausemandfoccupy 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 argument222222is 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
fixedConfirmed 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.
No comments yet.