pci_freecfg() panics for any VPD-capable device: kfree(NULL) plus stale vpd_rocnt/vpd_wcnt NULL-derefs after parse-error cleanup
Summary
DFly kfree(NULL) panics but pci_freecfg unconditionally kfrees vpd_ident/vpd_ros/vpd_w - NULL whenever VPD was found but never parsed (the default!) or after read_vpd's error cleanup, which NULLs the pointers but leaves the counts stale. Variant A 'panic: trying to free NULL pointer' (CardBus insertion failure/removal/suspend, no malice needed); Variant B vpd_ros[0].value load from NULL+8. TWO REAL PANICS captured from the guest serial console via KLD driving the real exported pci_freecfg with kernel-produced states (panic-A.txt/panic-B.txt); patched kernel returns cleanly. NULL-guard + count-reset fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5277 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| build.sh | β | 91 B | view raw | |
| run.sh | β | 303 B | view raw | |
| kmod/freecfgcrash.c | β | 2.6 KB | view raw | |
| kmod/Makefile | β | 111 B | β download | |
| panic-A.txt | β | 1.1 KB | view raw | |
| panic-B.txt | β | 1017 B | view raw | |
| fix-validation.log | β | 2.4 KB | view raw |
DF-5277 β pci_freecfg() panics on VPD-capable devices (kfree(NULL) / NULL deref via stale counts)
What
sys/bus/pci/pci.c:2146-2153 β the VPD teardown in pci_freecfg():
if (dinfo->cfg.vpd.vpd_reg) {
kfree(dinfo->cfg.vpd.vpd_ident, M_DEVBUF); /* NULL if never parsed / after cleanup */
for (i = 0; i < dinfo->cfg.vpd.vpd_rocnt; i++)
kfree(dinfo->cfg.vpd.vpd_ros[i].value, M_DEVBUF); /* vpd_ros may be NULL */
kfree(dinfo->cfg.vpd.vpd_ros, M_DEVBUF); /* NULL after cleanup */
for (i = 0; i < dinfo->cfg.vpd.vpd_wcnt; i++)
kfree(dinfo->cfg.vpd.vpd_w[i].value, M_DEVBUF);
kfree(dinfo->cfg.vpd.vpd_w, M_DEVBUF);
}
Two DragonFly-specific landmines (upstream FreeBSD tolerates these because
free(NULL) is a no-op there β DragonFly's kfree(NULL, ...) panics,
sys/kern/kern_slaballoc.c:1406):
- Variant A (never parsed): any device whose VPD capability was found
at enumeration (
vpd_reg != 0) but whose VPD was never parsed hasvpd_ident == vpd_ros == vpd_w == NULLβ firstkfree(NULL)panics. - Variant B (failed parse):
pci_read_vpd()'s error cleanup (pci.c:1297-1320) frees and NULLsvpd_ros/vpd_w/vpd_identbut does not resetvpd_rocnt/vpd_wcnt, which were already set by a completed section βvpd_ros[0].valuedereferences*(NULL+8)β page-fault panic. The same stale-count state also NULL-derefspci_get_vpd_readonly_method()(pci.c:1354-1357).
Reachability: pci_freecfg() is called from the CardBus hotplug paths
(sys/dev/pccard/cardbus/cardbus.c:191,246 β card insertion failure, card
removal, and cardbus_suspend()). A CardBus card with a VPD capability
(very common for PCMCIA network cards) whose VPD is malformed β or simply
never queried β panics the system on removal/suspend.
Reproduce
The userspace harness (see ../DF-5276) mode t2 first proves the parser
produces the crash precondition (vpd_ros == NULL, vpd_rocnt == 1) via
its own error-cleanup path. Then a kernel module calls the REAL exported
pci_freecfg() on a devinfo carrying exactly that state:
cd kmod && make kldload ./freecfgcrash.ko # variant A: kfree(NULL) panic # (reboot) kenv DF5277_VARIANT=B && kldload ./freecfgcrash.ko # variant B: *(NULL+8)
Expected (vulnerable kernel): immediate panic β
panic: trying to free NULL pointer (A) or Fatal trap 12: page fault
while in kernel mode, fault VA 0x8 (B). Serial console captured in
panic-A.txt / panic-B.txt.
Expected (kernel patched with ../fix.diff): module loads, prints
DF5277: pci_freecfg() returned without crashing (fixed kernel), unloads.
Fix
fix.diff: NULL-guard every VPD kfree in pci_freecfg(), reset
vpd_rocnt/vpd_wcnt in the parse error cleanup, NULL-guard
pci_get_vpd_readonly_method().
DF-5277 β VERDICT
Status: reproduced (impact: kernel panic / NULL-pointer dereference; confidence: certain)
Chain of evidence
- The parser produces the crash state (userspace verbatim harness,
mode
t2,../DF-5276/run.t2.log): a VPD stream of90 04 00 'X' 'Y' 01 'Z'(a completing VPD-R section β setsvpd_rocnt = 1atpci.c:1223-1227) followed by90 03 00 'R' 'V' 00(an "RV" keyword with length 0 β "bad keyword length" βcksumvalid = 0; state = -1atpci.c:1165-1178) drives the real cleanup atpci.c:1297-1305to free+NULLvpd_roswhile leavingvpd_rocnt == 1:
t2: vpd_ros = 0x0 (freed and NULLed by cleanup)
t2: vpd_rocnt= 1 (STALE -- cleanup does not reset it)
t2: CRASH PRECONDITION REPRODUCED
- The real in-kernel
pci_freecfg()panics on that state βkmod/freecfgcrash.kocalls the actual exportedpci_freecfg()on a devinfo carrying exactly the states the kernel itself leaves behind:
-
Variant A β VPD capability present, never parsed (all pointers NULL, the state of any VPD-capable CardBus card that no driver queried):
panic-A.txt:DF5277(A): pci_freecfg() with all VPD pointers NULL, vpd_reg=0x40 (never parsed) panic: trying to free NULL pointer _kfree() at _kfree+0x558 pci_freecfg() at pci_freecfg+0xdc freecfgcrash_load() at ...Root cause: DragonFly'skfree(NULL)panics (sys/kern/kern_slaballoc.c:1406) butpci_freecfg()(pci.c:2146-2153) kfreesvpd_ident/vpd_ros/vpd_wunconditionally β FreeBSD'sfree(NULL)no-op semantics were assumed. -
Variant B β completed section then error (stale
vpd_rocnt, NULLvpd_ros):panic-B.txt:DF5277(B): pci_freecfg() with vpd_ros=NULL, vpd_rocnt=1 (stale after error cleanup) Fatal trap 12: page fault while in kernel mode fault virtual address = 0x8 Stopped at pci_freecfg+0xb3: movq 0x8(%rax),%rdiexactly thevpd_ros[0].valueload at*(NULL+8).
- The readonly accessor NULL-deref also fires in the real code: the
verbatim
pci_get_vpd_readonly_method()SEGVs at address 0x0 when handed the post-cleanup state (end of../DF-5276/run.t2.log, stage 2b).
Both panics are on the real, unmodified running kernel
(DragonFly 6.5-DEVELOPMENT #0 .../X86_64_GENERIC), captured from the
serial console.
Reachability
pci_freecfg() is called from the CardBus paths
(sys/dev/pccard/cardbus/cardbus.c:191 β device_add_child failure at
insertion; :246 β detach, including cardbus_suspend() β system
suspend). A VPD-capable 16-bit card (common for PCMCIA NICs) with
malformed VPD β or with VPD that no driver ever queried (variant A, no
malice required, merely removal of the card) β panics the system.
CardBus is legacy hardware, hence Medium rather than High.
Fix validation
With fix.diff (NULL-guarded frees + count resets + lookup guard) built
into the guest kernel, both freecfgcrash.ko variants load and return
cleanly β see fix-validation.log.
Caveats
The QEMU guest has no CardBus bridge, so the free path was driven by calling the real exported function with the exact states the kernel's own parse/cleanup produce (state production itself demonstrated by the verbatim harness in step 1).
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
fixedWith fix.diff built into the guest kernel (6.5-DEVELOPMENT #1 Oct 2 00:11:04), both freecfgcrash variants load and return cleanly: 'DF5277: pci_freecfg() returned without crashing (fixed kernel)' for A and B; harness t2 shows vpd_rocnt reset to 0 and the readonly lookup returning ENXIO gracefully instead of faulting. Baseline: two distinct panics.
fix-validation.log (kernel dmesg tail on patched kernel); panic-A.txt / panic-B.txt (baseline); fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
panic-A.txt and panic-B.txt (serial console, real kernel backtraces through pci_freecfg); ../DF-5276/run.t2.log 'CRASH PRECONDITION REPRODUCED: vpd_ros 0x0 / vpd_rocnt 1 (STALE)' and the stage-2b SIGSEGV at 0x0 in the verbatim readonly lookup
PoC changes
KLD fabricates a struct pci_devinfo carrying exactly the states the kernel's own parse/cleanup produce (state production independently demonstrated by the verbatim harness); devinfo is inserted into pci_devq so a fixed kernel survives STAILQ_REMOVE; variant selected via kenv DF5277_VARIANT.
Verified recommended fix
NULL-guard every VPD kfree in pci_freecfg() and reset vpd_rocnt/vpd_wcnt in the pci_read_vpd() error cleanup; NULL-guard pci_get_vpd_readonly_method(); see fix.diff hunks 5-8.
Verdict
Two real kernel panics via the exported pci_freecfg() on the unmodified running kernel. Variant A (VPD-capable device whose VPD was never parsed: all pointers NULL, the DEFAULT state): 'panic: trying to free NULL pointer' with backtrace _kfree+0x558 <- pci_freecfg+0xdc (panic-A.txt) -- DragonFly's kfree(NULL) panics (kern_slaballoc.c:1406) but pci_freecfg kfrees vpd_ident/vpd_ros/vpd_w unconditionally. Variant B (completed VPD-R section followed by parse error: cleanup NULLs vpd_ros but leaves vpd_rocnt stale): 'Fatal trap 12: page fault ... fault virtual address = 0x8 ... Stopped at pci_freecfg+0xb3: movq 0x8(%rax),%rdi' (panic-B.txt) -- exactly the stale-count deref. The crash-precondition state itself is produced by the real parser (harness t2: vpd_ros=0x0, vpd_rocnt=1 after 'bad keyword length' cleanup), and the verbatim pci_get_vpd_readonly_method() likewise SEGVs at 0x0 on that state. Reachability: pci_freecfg is called from CardBus attach-failure/detach/suspend (cardbus.c:191,246).
No comments yet.