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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5277 Β· 9 files
FileTypeDescriptionSize
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 has vpd_ident == vpd_ros == vpd_w == NULL β†’ first kfree(NULL) panics.
  • Variant B (failed parse): pci_read_vpd()'s error cleanup (pci.c:1297-1320) frees and NULLs vpd_ros/vpd_w/vpd_ident but does not reset vpd_rocnt / vpd_wcnt, which were already set by a completed section β†’ vpd_ros[0].value dereferences *(NULL+8) β†’ page-fault panic. The same stale-count state also NULL-derefs pci_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().

VERDICT.md
↓ download raw

DF-5277 β€” VERDICT

Status: reproduced (impact: kernel panic / NULL-pointer dereference; confidence: certain)

Chain of evidence

  1. The parser produces the crash state (userspace verbatim harness, mode t2, ../DF-5276/run.t2.log): a VPD stream of 90 04 00 'X' 'Y' 01 'Z' (a completing VPD-R section β€” sets vpd_rocnt = 1 at pci.c:1223-1227) followed by 90 03 00 'R' 'V' 00 (an "RV" keyword with length 0 β†’ "bad keyword length" β†’ cksumvalid = 0; state = -1 at pci.c:1165-1178) drives the real cleanup at pci.c:1297-1305 to free+NULL vpd_ros while leaving vpd_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

  1. The real in-kernel pci_freecfg() panics on that state β€” kmod/freecfgcrash.ko calls the actual exported pci_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's kfree(NULL) panics (sys/kern/kern_slaballoc.c:1406) but pci_freecfg() (pci.c:2146-2153) kfrees vpd_ident/vpd_ros/vpd_w unconditionally β€” FreeBSD's free(NULL) no-op semantics were assumed.

  • Variant B β€” completed section then error (stale vpd_rocnt, NULL vpd_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),%rdi exactly the vpd_ros[0].value load at *(NULL+8).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

With 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
↓ fix.diffper-fix-DF-5277

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