pnp_read_bytes() error path abandons the grown buffer β caller frees the stale pre-growth pointer: kernel heap double free + leak of the replacement
Summary
On a short resource read after buffer growth the function returns EINVAL without updating *resourcesp, so pnp_read_resources propagates the already-freed pre-growth pointer and the isolation-protocol cleanup frees it AGAIN (grown block leaks): the ordinary 'failed to report resource data' condition = boot-time ring-0 double free of a 1024B M_TEMP block + guaranteed leak. pnprig-verified with VERBATIM pnp.c: 'panic: memory chunk ... is already free!'; publish-buffer-before-read fix validated on a rebuilt kernel.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5443 Β· 24 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| verdict.json | β | 6.7 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| rig/pnprig.c | β | 13.1 KB | view raw | |
| rig/rigmod.h | β | 1.1 KB | view raw | |
| rig/hack_dfa/wrap.c | β | 138 B | view raw | |
| rig/hack_dfa/Makefile | β | 110 B | β download | |
| rig/hack_dfb/wrap.c | β | 135 B | view raw | |
| rig/hack_dfb/Makefile | β | 110 B | β download | |
| rig/hack_dfc/wrap.c | β | 133 B | view raw | |
| rig/hack_dfc/Makefile | β | 110 B | β download | |
| rig/hack_dfd/wrap.c | β | 143 B | view raw | |
| rig/hack_dfd/Makefile | β | 110 B | β download | |
| rig/build.sh | β | 403 B | view raw | |
| rig/run_A.sh | β | 327 B | view raw | |
| rig/run_BCD.sh | β | 419 B | view raw | |
| run.log | β | 1.0 KB | view raw | |
| serial_runA_full.log | β | 2.2 KB | view raw | |
| panic.txt | β | 488 B | view raw | |
| env.txt | β | 382 B | view raw | |
| fix.diff | β | 3.6 KB | view raw | |
| fix_run.log | β | 1.0 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download |
DF-5443 β pnp_read_bytes() error path abandons the grown buffer β double free + leak (sys/bus/isa/pnp.c)
What
pnp_read_bytes() (sys/bus/isa/pnp.c:493-526) grows the resource buffer with
kmalloc/bcopy/kfree, but on a short read (pnp_get_resource_info(...) !=
amount, pnp.c:517-518) returns EINVAL without updating *resourcesp.
The caller pnp_read_resources() then propagates the caller-local pointer β
which still references the already-freed pre-growth buffer β through its
own out: block (pnp.c:576-580), and pnp_isolation_protocol() frees that
stale pointer at cleanup (pnp.c:673-674). Result: kernel heap double
free of the old buffer plus a leak of the newly-grown buffer.
Trigger model
Any ISA-PnP card (or emulated/counterfeit PnP device in a VM) that delivers more than 1024 bytes of resource data (forcing the growth path) and then stops asserting the resource-data-ready bit mid-descriptor. This is not limited to malicious hardware: the "PnP device failed to report resource data" path (pnp.c:218-221) exists precisely for flaky cards.
Rig
rig/ contains a KLD that compiles the real, unmodified
sys/bus/isa/pnp.c (included verbatim; only inb/outb/kmalloc/kfree and three
global symbol names are macro-substituted, and its DRIVER_MODULE neutered)
and drives it with a virtual PnP card implementing the serial-isolation
state machine (checksum-valid 72-bit ID, PNP_STATUS/PNP_RESOURCE_DATA byte
service). Module hack_dfa (script 1):
- card passes serial isolation (checksum-valid),
- serves a large vendor tag header (0x84, len 2048) β 3 bytes,
- then stalls (status never ready).
pnp_read_bytes(2048) grows 1024β3072 (freeing the 1024 block), the read
fails short, the stale pointer is freed again at isolation cleanup.
Build
# on the guest (root), sources pushed to /usr/src/sys/bus/isa/ (cd /usr/src/sys/bus/isa/hack_dfa && make obj && make depend && make)
Run
kldload /usr/obj/usr/src/sys/bus/isa/hack_dfa/rigdfa.ko
Expected (unfixed baseline kernel)
PNPRIG: kmalloc(1024, temp) = 0xfffff8008db3b400 <- buffer A PNPRIG: kmalloc(3072, temp) = 0xfffff8008dc05500 <- grown B PNPRIG: kfree(0xfffff8008db3b400, temp) <- growth free of A (pnp.c:512) PnP device failed to report resource data <- pnp.c:219 short read PNPRIG: kfree(0xfffff8008db3b400, temp) <- cleanup free of STALE A (pnp.c:674) panic: memory chunk 0xfffff8008db3b400 is already free! _kfree() <- rig_identify <- isa_driver_added <- devclass_add_driver
- baseline: kernel panic (allocator double-free check), B leaked.
- fixed kernel: the error path publishes the live buffer; the cleanup kfree targets the grown buffer exactly once, kldload returns 0.
Fix
fix.diff (also fixes DF-5444/DF-5445 in the same files β see those packs).
Validated on a rebuilt kernel: see VERDICT.md.
DF-5443 VERDICT β REPRODUCED (kernel panic; primitive = heap double-free)
Bottom line
The bug is real, attacker-reachable through card data, and was reproduced
end-to-end on the guest with the real, unmodified sys/bus/isa/pnp.c
engine: a card that delivers >1 KiB of resource data and then stalls
mid-descriptor makes pnp_isolation_protocol() free an already-freed buffer
β panic: memory chunk 0xfffff8008db3b400 is already free!
A single-fix kernel (fix.diff) makes the identical fault injection complete
cleanly (RC=0, exactly one free of the live grown buffer).
Root cause (path:line)
pnp_read_bytes()grows the buffer: kmalloc β bcopy β kfree(old) at sys/bus/isa/pnp.c:506-515.- The short-read error path returns EINVAL without updating
*resourcesp(pnp.c:517-518) β the out-params are only written on success (pnp.c:521-523). pnp_read_resources()propagates its (now stale) local pointer atout:(pnp.c:576-580).pnp_isolation_protocol()breaks on the error and runsif (resources) kfree(resources, M_TEMP);(pnp.c:673-674) β second free of the pre-growth buffer; the grown replacement is leaked.
Trigger precondition is exactly the "PnP device failed to report resource data" condition the code already anticipates for flaky cards (pnp.c:218-221) β no exotic state required, only a card (malicious OR merely misbehaving) that stalls after >1024 delivered bytes.
Reproduction (baseline kernel #0, Thu Jul 2 06:02:54)
Rig: KLD compiling pnp.c verbatim (only inb/outb/kmalloc/kfree and three global names macro-substituted; DRIVER_MODULE neutered) + a virtual PnP card implementing the serial-isolation protocol (checksum-valid 72-bit ID served LSB-first per pnp.c:192, PNP_STATUS/PNP_RESOURCE_DATA service). Script 1: large vendor tag header (0x84, len 2048) served, body stalled.
Observed (serial console, run.log / serial_runA_full.log / panic.txt):
PNPRIG: kmalloc(1024, temp) = 0xfffff8008db3b400 <- buffer A (pnp.c:503) PNPRIG: kmalloc(3072, temp) = 0xfffff8008dc05500 <- grown B (pnp.c:510) PNPRIG: kfree(0xfffff8008db3b400, temp) <- growth free (pnp.c:512) PnP device failed to report resource data <- short read (pnp.c:219) PNPRIG: kfree(0xfffff8008db3b400, temp) <- STALE free (pnp.c:674) panic: memory chunk 0xfffff8008db3b400 is already free! _kfree() <- rig_identify <- isa_driver_added <- devclass_add_driver
The stack is the genuine in-vivo kldload β isa_driver_added
(isa_common.c:816-831) β identify β engine path. DragonFly's slab
allocator (chunk_mark_free) converts the double free into a deterministic
panic; on the same evidence the free-list is poisoned twice β the classic
two-owners primitive (the witness probes in the rig demonstrate re-handout
mechanics when the allocator does not abort).
Why not uid0
There is no unprivileged-local-user input path: pnp.c runs only during ISA
bus autoconfiguration (boot) or isa_driver_added on driver kldload. The
attacker model is crafted/counterfeit PnP hardware or a VM-emulated device β
same class as DF-5392/5393/5394 (pccard CIS family analogy). For that
attacker the primitive is ring-0 boot-time heap corruption with a
fully-controlled stall point (attacker chooses which 1024-byte-aligned
growth boundary and when), i.e., arbitrary-code-execution-grade; severity
kept at Medium consistent with the hardware-trigger model of the sibling
findings.
Fix validation (kernel #1, Mon Oct 5 16:47:13, fix.diff applied)
Identical rig, identical card script, patched engine compiled in:
PNPRIG: kmalloc(1024, temp) = 0xfffff8008db3b800 PNPRIG: kmalloc(3072, temp) = 0xfffff8008dc05500 PNPRIG: kfree(0xfffff8008db3b800, temp) <- growth free (unchanged) PnP device failed to report resource data PNPRIG: kfree(0xfffff8008dc05500, temp) <- cleanup frees the LIVE buffer RUN_A_RC=0 ; witness: no double-freed address recorded ; guest healthy
The leak is fixed simultaneously (the grown buffer is now reachable by the caller and freed exactly once). Baseline behavior GONE.
Fix
fix.diff hunk in pnp.c:514-533 publishes *resourcesp/*spacep before
the read attempt (and drops the redundant success-path copies). The same
diff also carries the DF-5444/DF-5445 fixes (see those packs); full kernel
build log in fix_build.log (4299 compile steps, clean).
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
fixedSingle-fix kernel (nativekernel X86_64_GENERIC + fix.diff, #1 Mon Oct 5 16:47:13): the exact baseline-panic fault injection now completes with kfree of the LIVE grown buffer (0xfffff8008dc05500) exactly once at cleanup, no DOUBLE FREE detected, no panic, kldload RC=0, witness clean, guest healthy. Baseline bad behavior GONE; leak of the grown buffer also eliminated.
fix_run.log (patched-kernel dmesg sequence + RUN_A_RC=0), fix_build.log (full kernel build), fix.diff (git apply --check clean against the read-only sys/ tree)
Confirmed kernel references
Detail
Exploit chain
crafted/misbehaving ISA-PnP card (or emulated PnP device in a VM) -> pnp_identify read-port loop -> pnp_isolation_protocol -> pnp_get_serial (valid checksum) -> pnp_read_resources -> pnp_read_bytes growth (kmalloc B, kfree A at pnp.c:512) -> card stalls -> pnp_get_resource_info returns short -> EINVAL without out-param update (pnp.c:517-518) -> pnp_read_resources out: propagates stale A (pnp.c:577-579) -> pnp_isolation_protocol cleanup kfree(A) again (pnp.c:674) -> heap double free -> panic on this kernel (chunk_mark_free) / free-list poisoning (two live owners of one 1024-byte block) on kernels without the check -> boot-time ring-0 memory corruption under attacker control.
Evidence (decisive lines)
run.log + serial_runA_full.log (baseline: kmalloc(1024)=0xfffff8008db3b400, kmalloc(3072)=0xfffff8008dc05500, kfree(A) at growth, short-read message, kfree(A) again, panic); panic.txt (panic: memory chunk ... is already free!, full trace); fix_run.log (patched kernel: same stream, kfree of 0xfffff8008dc05500 at cleanup, RUN_A_RC=0, witness clean); fix.diff (out-param propagation hunk); fix_build.log (full 4299-step kernel build of the fixed tree); rig/ (pnprig.c harness + hack_dfa wrapper + build.sh + run_A.sh)
PoC changes
Self-authored rig (no seed). (1) The virtual card must serve serial bits LSB-first per byte β the kernel assembles data[i/8] by shifting right and ORing into 0x80 (pnp.c:192), so MSB-first serving yields bit-reversed IDs and checksum failure; fixed after attempt 1 (simulated the LFSR offline). (2) The kfree-witness initially counted per-address frees and false-positived on legal allocator address recycling during buffer growth; replaced with liveness tracking (alloc marks live, free of non-live chunk = double free). (3) pnp_identify() refuses to run on a DS_ATTACHED bus (pnp.c:698-701 rescan guard), so the rig drives the identical read-port loop (pnp.c:713-720) calling pnp_isolation_protocol() directly β the engine itself is untouched.
Verified recommended fix
In pnp_read_bytes(), publish resourcesp/spacep (and *lenp semantics) BEFORE the pnp_get_resource_info() attempt so the caller always tracks the live buffer β full unified diff in fix.diff (also fixes DF-5444/DF-5445).
Verdict
REPRODUCED on DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC, QEMU). A KLD compiling the REAL unmodified sys/bus/isa/pnp.c (verbatim #include; only inb/outb/kmalloc/kfree and three global names macro-substituted, DRIVER_MODULE neutered) was driven by a virtual PnP card implementing the serial-isolation protocol (checksum-valid 72-bit ID served LSB-first, PNP_STATUS/PNP_RESOURCE_DATA service). A card that streams a large-tag header (0x84, len 2048) and then stalls mid-descriptor β i.e. takes the ordinary 'PnP device failed to report resource data' path (pnp.c:218-221) after >1024 delivered bytes β forces pnp_read_bytes() through its growth path (kmalloc 3072, kfree of the 1024 buffer at pnp.c:512) and then into the short-read EINVAL return that does NOT update *resourcesp (pnp.c:517-518). pnp_read_resources() propagates the stale freed pointer at out: (pnp.c:576-580) and pnp_isolation_protocol() frees it again at cleanup (pnp.c:673-674): observed 'kfree(A) [growth]; kfree(A) [cleanup]' then 'panic: memory chunk 0xfffff8008db3b400 is already free!' with stack _kfree <- engine identify <- isa_driver_added <- devclass_add_driver β the genuine kldload path. Primitive = kernel-heap double free (CWE-415) of a 1024-byte M_TEMP block with attacker-chosen timing, plus a leak of the grown buffer; terminal outcome on this kernel is a deterministic panic (allocator chunk_mark_free check). uid0 not pursued: no unprivileged-user input path β trigger is PnP card data at boot/driver-load (crafted/counterfeit hardware or VM device), same attacker model as DF-5392/5393/5394; for that attacker the primitive is boot-time ring-0 heap corruption. FIX VALIDATED on rebuilt kernel #1 (fix.diff): identical fault injection now frees the LIVE grown buffer exactly once at cleanup (kfree(3072-buffer)), no DOUBLE FREE, no panic, kldload RC=0, witness clean β leak also fixed.
No comments yet.