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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2216 Β· 15 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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)

  1. guest tree linux_dmi.c sha256 c0f7f7a5... == audited sys/ tree file.
  2. the rig #includes the tree file verbatim β€” dmi_match in the kernel is the production function, unmodified.
  3. dmi_match_fixed = the same bytes + fix.diff, def renamed only (fixed_dmi.c sha256 76433ec6...).
  4. assembled rig df_22x_rig.c sha256 c646506a... (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).

VERDICT.md verdict full narrative
↓ download raw

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

fixed
↓ fix.diffper-fix-DF-2216

Confirmed 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.