dmi_match() leaks kgetenv() buffer on every code path
Summary
dmi_match() calls kgetenv() on four switch arms (DMI_SYS_VENDOR DMI_BOARD_VENDOR DMI_PRODUCT_NAME DMI_BOARD_NAME) and stores result in s but never calls kfreeenv() on it. kgetenv() allocates fresh kernel heap buffer of strlen(value)+1 bytes from M_KENV whenever dynamic kernel environment active (production default). Each call leaks up to KENV_MVALLEN+1 bytes (~129 bytes). Every correct in-tree consumer honors kgetenv/kfreeenv pairing: kgetenv_string() kern_environment.c:369 does kfreeenv(tmp) sibling DMI helper dmi_found() drm_drv.c:1329-1336 calls kfreeenv() on all four fields. dmi_match() omits frees entirely on both return true and fall-through return false paths. s goes out of scope allocation unreachable. Sole in-tree caller radeon_atom_pick_dig_encoder() atombios_encoders.c:2251 gated by ASIC_IS_DCE32 (DCE3.2/HD4000 radeon). Invoked during encoder assignment on display init and DP/HDMI hotplug and mode-set events reachable through /dev/dri/cardN by user holding DRI device. Sustained triggering drains kernel heap no bound. No memory-corruption primitive. Impact: DoS via kheap exhaustion. Also exported LinuxKPI shim any future port inherits leak. [VERIFIED pass2: reproduced - 16384 calls leak exactly 104 bytes per call measured with kernel M_KENV accounting across all four return arms; sole in-tree caller radeon DCE32-gated; kfreeenv fix-validated delta 0]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2216 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rig_head.c | rig-source | shared batch rig head (includes, provenance comments) | 2.0 KB | view raw |
| rig_mid.c | rig-source | verbatim #includes of the four production files + kfree macro restore + fixed prototypes | 1.1 KB | view raw |
| rig_tail.c | rig-source | malloc-accounting meter + DF2216/2220/2223/2224 test sections + sysctl trigger | 18.1 KB | view raw |
| build.sh | build-script | sha256 gates, verbatim kvasprintf extraction, fix.diff scratch + fixed-variant extraction, kernel-compile | 166 B | view raw |
| run.sh | run-script | sysctl trigger + dmesg extraction | 172 B | view raw |
| fix.diff | suggested-fix | kfreeenv(s) on every path of all four dmi_match arms | 1.2 KB | view raw |
| build.log | build-log | FULL untrimmed nativekernel DFV22X build | 5.5 MB | β download |
| run.log | run-log | FULL decisive rig output (all four findings) | 7.8 KB | view raw |
| run_section.log | run-log | DF-2216 lines only | 953 B | view raw |
| run.first_meterbug.log | run-log | first iteration: ks_loosememuse double-count artifact, engineering record | 7.5 KB | view raw |
| env.txt | environment | guest env + all provenance hashes | 2.1 KB | view raw |
| VERDICT.md | verdict | full narrative | 4.6 KB | β raw |
| README.md | readme | build/run/expected | 3.5 KB | β raw |
| manifest.json | manifest | this file | 2.6 KB | view raw |
| verdict.json | verdict | machine verdict for persist_poc.py | 3.4 KB | view raw |
DF-2216 β dmi_match() leaks the kgetenv() buffer on every code path
Status: REPRODUCED (verbatim-code, kernel-compiled rig) β impact class:
unbounded kernel-heap (M_KENV) exhaustion = availability only. Fix: A/B-validated
in the same harness kernel (fix.diff, kfreeenv(s) on every arm).
Part of the DF-22X batch (DF-2216 / DF-2220 / DF-2223 / DF-2224) verified in ONE harness kernel; this pack documents the DF-2216 section.
What this pack contains
| file | role |
|---|---|
rig_head.c, rig_mid.c, rig_tail.c |
rig sources (shared by the batch; sha-pinned below) |
fix.diff |
git-apply-able unified diff against sys/dev/drm/linux_dmi.c |
build.sh |
provenance chain (sha256 gates on the four audited tree files, verbatim kvasprintf extraction, fix.diff-applied fixed variants, single harness kernel DFV22X) |
run.sh |
triggers the rig (sysctl kern.df22x_run=1) and extracts dmesg |
build.log |
FULL untrimmed nativekernel build transcript |
run.log |
FULL decisive rig output (all four findings) |
run_section.log |
the DF-2216 lines only |
run.first_meterbug.log |
first iteration: meter double-counted ks_loosememuse (up-only fold counter) β kept as the engineering record of why the meter sums only ks_use[].memuse |
env.txt |
guest environment + all provenance hashes |
VERDICT.md, manifest.json, verdict.json |
verdict narrative + machine-readable results |
How to reproduce
On the DragonFly audit guest (root, /usr/src = clean-source snapshot):
# push this directory to the guest as /root/poc/df22x (all batch files)
sh build.sh # builds harness kernel DFV22X (nativekernel, ~20 min cold)
cp /usr/obj/usr/src/sys/DFV22X/kernel.stripped /boot/kernel/kernel
cp /usr/obj/usr/src/sys/DFV22X/kernel.debug /boot/kernel/kernel.debug
shutdown -r now
sh run.sh # sysctl kern.df22x_run=1; extracts the DF22X dmesg section
Provenance (checked at build time, build aborts otherwise)
- guest tree
linux_dmi.csha256c0f7f7a5...== auditedsys/tree file. - the rig
#includes the tree file verbatim βdmi_matchin the kernel is the production function, unmodified. dmi_match_fixed= the same bytes +fix.diff, def renamed only (fixed_dmi.csha25676433ec6...).- assembled rig
df_22x_rig.csha256c646506a...(recorded in env.txt).
Expected output (baseline = unmodified production code)
DF2216 CONTROL paired kgetenv+kfreeenv x16384: memuse delta=0 calls delta=16384 DF2216 BASELINE dmi_match x16384 (matchret=1 mismatchret=0): memuse delta=1703936 ... leaked-bytes-per-call=104 DF2216 BASELINE arm smbios.system.maker single call: memuse delta=104 (x4 arms) DF2216 FIXED dmi_match_fixed x16384 (...): memuse delta=0 calls delta=16384 DF2216 VERDICT: baseline leaked 1703936 bytes in 16384 calls (control 0, fixed 0) -> LEAK CONFIRMED, FIX VALID
The meter is the kernels own per-type accounting (malloc_type.ks_use[].memuse,
aggregated exactly as kern_slaballoc.c:451 does).
Attacker input / reachability
dmi_match() has exactly one in-tree caller: radeon_atom_pick_dig_encoder()
(sys/dev/drm/radeon/atombios_encoders.c:2251), gated by ASIC_IS_DCE32
(radeon HD4000-class). It runs during encoder assignment β display init, DP/HDMI
hotplug, mode sets β reachable by a user holding the DRI device node. Each call
leaks one kmalloc(strlen(value)+1, M_KENV) buffer (~104 B slab-quantized);
there is no bound. No corruption primitive, no info disclosure β no privesc
route; ceiling is kernel-heap exhaustion (DoS).
DF-2216 β VERDICT
REPRODUCED β verbatim-code, kernel-compiled rig; leak quantified with the kernel's own malloc accounting; fix A/B-validated in the same kernel. Impact: unbounded kernel-heap (M_KENV) exhaustion β availability only. No privesc route (see below).
Rig class and why
Runtime kldload of drm GPFs on this audit guest, so the established
kernel-compile rig class was used (same as the DF-2212 ttm harness kernel):
one custom KERNCONF DFV22X (= stock X86_64_GENERIC + device dfverify)
compiles dev/dfverify/df_22x_rig.c, which #includes the production
sys/dev/drm/linux_dmi.c verbatim (build.sh aborts unless the guest
tree file sha256 equals the audited-tree hash c0f7f7a5...). The function
under test in the kernel is byte-for-byte the production dmi_match().
Line-accurate confirmation
sys/dev/drm/linux_dmi.c:35-67: each of the four switch arms
(DMI_SYS_VENDOR :44, DMI_BOARD_VENDOR :49, DMI_PRODUCT_NAME :54,
DMI_BOARD_NAME :59) does s = kgetenv(...) and return true or falls
through β no kfreeenv() anywhere. kgetenv() (kern_environment.c:229)
kmalloc(len, M_KENV, M_WAITOK)s a fresh copy of the value (:242) whenever the
dynamic kernel environment is active (production default; active on the guest β
kenv smbios.system.maker = QEMU). Every in-tree consumer pairs the two
(kgetenv_string() kern_environment.c:369 does kfreeenv(tmp)); dmi_match()
is the sole defector. Sibling DMI helper dmi_found() (drm_drv.c:1329-1336)
frees all four fields β the idiom this function forgot.
Mechanical reproduction (decisive run, run.log)
The rig fabricates the kenv boundary (100-char values for all four smbios.*
keys via ksetenv), then measures Ξ£ malloc_type.ks_use[cpu].memuse for
M_KENV β the exact aggregation the kernel itself uses (kern_slaballoc.c:451):
| phase | result |
|---|---|
CONTROL: 16384 paired kgetenv+kfreeenv |
delta = 0 (calls +16384) |
BASELINE: 16384 real dmi_match() (match/mismatch arms alternated) |
delta = 1,703,936 B = 104 B/call, both return paths leak |
| single-shot per arm (4 arms) | +104 B each |
FIXED (fix.diff applied, renamed def) 16384 calls |
delta = 0 |
104 = slab-chunk-quantized 101-byte request (strlen(value)+1). The finding is
confirmed line-accurately: per-call, per-arm, both return paths, unbounded.
Meter engineering note (recorded for reproducibility)
The first iteration double-counted ks_loosememuse β an up-only fold
counter that kfree() never decrements (kern_slaballoc.c:1136-1140). The
telltale artifact (init:+131104 = one 131072-byte fold + 32) identified it;
the corrected meter sums only ks_use[].memuse and the control/fixed loops
went to exactly 0. First-iteration log kept as run.first_meterbug.log.
Fix validation (A/B, same kernel)
fix.diff frees s with kfreeenv() on every path of all four arms (and
widens s to char *). The fixed function was extracted from a
fix.diff-applied scratch copy of the same tree bytes (sha chain in
build.sh/env.txt) and compiled into the same kernel as dmi_match_fixed.
Result: identical call counts, identical return values (matchret=1
mismatchret=0 in both), memuse delta 0 β leak eliminated, behavior
otherwise unchanged. fix_status: fixed.
Reachability (who can actually trigger this)
Sole in-tree caller: radeon_atom_pick_dig_encoder()
(atombios_encoders.c:2251), gated by ASIC_IS_DCE32(rdev) β radeon DCE3.2
(HD4000-class) hardware. Invoked during encoder assignment (display init,
DP/HDMI hotplug, mode sets) β reachable by a user holding the DRI device node
(/dev/dri/cardN). On such a system each event leaks ~104 B of M_KENV with no
bound. On the audit guest: no GPU β no runtime trigger; the defect is proven
through the verbatim code path (dynamic kenv real, values fabricated).
Quantified ceiling: 104 B Γ events β kenv-heap exhaustion after
~10^7 events (GBs) β a sustained-abuse DoS, matching Low severity.
Privesc assessment (standing directive)
None. The bug is a pure allocation leak: no overread, no overwrite, no use-after-free, no double-free, nothing copied to userspace. The leaked memory is never re-referenced. There is no primitive from which to build an escalation chain; impact is availability only.
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 DF2216 CONTROL/BASELINE/FIXED/VERDICT: control delta=0, baseline delta=1703936 (104/call), per-arm single calls +104 each, fixed delta=0; run.first_meterbug.log documents the first-iteration meter artifact (ks_loosememuse double count) that was corrected by summing only ks_use[].memuse as the kernel itself does (kern_slaballoc.c:451)
PoC changes
Rebuilt the finding PoC sketch as a verbatim-code kernel-compiled rig (rig_head/mid/tail.c + build.sh): sha256-gated verbatim inclusion of linux_dmi.c, kenv boundary fabricated with a 100-char value, deterministic in-kernel meter over malloc_type ks_use[].memuse; first iteration double-counted ks_loosememuse (up-only fold counter, never decremented on free) and was corrected after the +131104 = 131072+32 artifact identified the fold
Verified recommended fix
Free the kgetenv buffer with kfreeenv(s) on every path of all four arms of dmi_match (see fix.diff)
Verdict
Reproduced with the verbatim production dmi_match() compiled kernel-resident (harness kernel DFV22X; runtime kldload of drm GPFs on this guest). Over 16384 calls mixing the strcmp-match arm (returns true) and the fall-through arm (returns false), the M_KENV malloc accounting grows by exactly 1703936 bytes = 104 bytes per call (101 requested + slab quantization); the paired kgetenv+kfreeenv control loop returns to 0 delta and the fix-2216 patched variant (kfreeenv on every path, extracted from fix.diff-applied bytes in the same kernel) also returns to 0 delta. Each of the four kenv arms leaks 104 bytes in a single-shot sweep. The leak is real, per-call, on both return paths, and unbounded; impact is kernel-heap exhaustion (DoS) reachable only through the sole in-tree caller radeon_atom_pick_dig_encoder on DCE3.2 hardware via display hotplug/mode-set events, so practical severity stays Low. No memory-corruption or info-disclosure primitive exists (pure availability), so no privesc chain is possible; fix validated A/B in the same kernel.
No comments yet.