kvsscanf silently accepts unknown conversion specifiers, desynchronizing the varargument list
| Field | Value |
|---|---|
| ID | DF-2884 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N |
| CWE | CWE-476 / CWE-838 |
| File | sys/kern/subr_scanf.c |
| Lines | 148-253 (fallthrough :283) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The conversion switch has no default case: an unsupported specifier
(%f/%e/%g/%a/%z/typo) falls through, consumes leading whitespace as a
side effect, takes NO varargument, and silently does nothing β every
subsequent conversion then binds to a shifted argument (%d stores
through the pointer meant for %f; a later %s would write through a
scalar argument pointer = caller-object overflow hazard). Userland
vfscanf rejects unknown conversions; cc's __scanflike rejects such
formats at compile time, but runtime-built fmts and wrapper calls reach
the defect. No in-tree caller uses an unsupported specifier (21 sites
surveyed) β hardening. In-kernel: "5" with "%f%d" β ret=1, a=5
(written through &a); patched kernel #1 β ret=0, nothing written. Fix:
default: goto match_failure; (validated).
Timeline
- 2026-09-02 Discovered during pass-2 audit of subr_scanf.c (GLM 5.3); in-kernel argument-misassociation reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2884 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_dfscanf.c | β | 6.4 KB | view raw | |
| Makefile | β | 56 B | β download | |
| build.sh | β | 167 B | view raw | |
| run.sh | β | 219 B | view raw | |
| build.log | β | 835 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| run.2.log | β | 1.1 KB | view raw | |
| run_patched.log | β | 935 B | view raw | |
| env.txt | β | 530 B | view raw | |
| fix.diff | β | 578 B | view raw | |
| VERDICT.md | β | 1.4 KB | β raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-2884 VERDICT β REPRODUCED (impact: none β hardening)
Root cause
sys/kern/subr_scanf.c:148-253: the conversion-character switch
(case '%', '*', l, q, h, 0-9, d, i, o, u, x, s, [, c, p, n) lacks a
default: arm. An unrecognized specifier falls through with c set
to that character; the code then runs the input-failure check and the
whitespace skip (side effects on input), and the second switch at
:283-535 (cases CT_CHAR/CT_CCL/CT_STRING/CT_INT only) silently does
nothing. No varargument is consumed β all subsequent conversions bind
to shifted arguments.
Proof
Stock kernel #0 (E5, through kvsscanf because cc's __scanflike(2,3)
on ksscanf rejects "%f%d" outright β itself demonstrating the
specifier is unsanctioned):
dfpoc: E5 '5' %f%d: ret=1 a=5 b=-1 β %d stored through &a; &b never written
Patched kernel #1 (fix.diff hunk 1, default: goto match_failure):
dfpoc: E5 '5' %f%d: ret=0 a=-1 b=-1 β rejected, nothing written
Impact
Argument misassociation: a later %s would write a string through the
pointer intended for a scalar conversion (caller-object overflow
hazard), later %c/%d write through wrong pointers. Reachable only
via runtime-built formats or unannotated wrappers, and no in-tree
caller uses an unsupported specifier (21 sites surveyed) β Info
(hardening), certain confidence. Userland vfscanf rejects unknown
conversions; this aligns the kernel engine.
Fix verification
fixedfix.diff hunk 1 (default: goto match_failure) validated on kernel #1: the unknown-specifier format is rejected (ret=0) and no argument is written; all other experiments behave per their own fixes/controls.
['run_patched.log: "dfpoc: E5 \'5\' %f%d: ret=0 a=-1 b=-1"', 'run.log (baseline kernel #0): ret=1 a=5 b=-1', 'fix.diff hunk 1']
Confirmed kernel references
Detail
Exploit chain
none in-tree; for a hypothetical caller with a runtime-built fmt containing an unsupported specifier, later %s writes a NUL-terminated string through a non-buffer argument pointer
Evidence (decisive lines)
['run.log: "dfpoc: E5 \'5\' %f%d: ret=1 a=5 b=-1" + RESULT varargument list desynchronized REPRODUCED', 'run.2.log: identical on second load', 'run_patched.log: kernel #1 with fix.diff hunk 1 -- "E5 \'5\' %f%d: ret=0 a=-1 b=-1"', 'build attempt record: cc rejects ksscanf(..., "%f%d", int*, int*) via __scanflike -- %f is not a supported kernel conversion']
PoC changes
authored fresh (no seed): experiment E5 of the shared dfpoc.ko KLD harness; called through an unannotated kvsscanf wrapper because the __scanflike(2,3) annotation on ksscanf rejects '%f' at compile time
Verified recommended fix
add 'default: goto match_failure;' to the conversion switch at sys/kern/subr_scanf.c:252 (fix.diff hunk 1)
Verdict
kvsscanf's conversion switch (sys/kern/subr_scanf.c:148-253) has no default case, so an unsupported specifier (%f/%e/%g/%a/%z/typo) silently falls through: leading whitespace is consumed as a side effect, NO varargument is taken, and every subsequent conversion binds to a shifted argument -- %d then stores through the pointer meant for %f, and a following %s would write a string through a scalar pointer (caller-object overflow hazard). Proven in-kernel on the stock kernel via kvsscanf (E5: ksscanf-style '5' with '%f%d' returns ret=1, a=5, b=-1 -- %d wrote through &a, &b untouched); cc's own __scanflike(2,3) annotation on ksscanf rejects such formats at compile time (the PoC had to bypass it), confirming the specifier is unsanctioned, but runtime-built formats and unannotated wrappers reach the defect. No in-tree caller uses an unsupported specifier (21 sites surveyed) -- Info/hardening. fix.diff hunk 1 (default: goto match_failure, mirroring userland vfscanf) validated on kernel #1: ret=0, nothing written.
No comments yet.