Negative unit number in devclass_alloc_unit causes heap OOB write via dc->devices[]
| Field | Value |
|---|---|
| ID | DF-0003 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/kern/subr_bus.c |
| Lines | 1064-1125, 1144, 2166-2184 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
devclass_alloc_unit() only treats -1 as a wildcard unit. Any other negative
unit (e.g. -2) bypasses the existing-device check (guarded by unit >= 0)
and the table-extension check (guarded by unit >= dc->maxunit, which is false
for negatives) and returns success with the negative value. devclass_add_device()
then executes dc->devices[dev->unit] = dev β a heap OOB write at a negative
index into the dc->devices array, corrupting heap memory immediately before
the allocation. device_set_unit() has a matching OOB read in its bounds check.
Root cause
In devclass_alloc_unit (sys/kern/subr_bus.c:1064-1125):
int unit = *unitp;at line1067.if (unit != -1)at line1072β only-1is the wildcard. A value like-2enters the "wired unit" branch.if (unit >= 0 && unit < dc->maxunit && dc->devices[unit] != NULL)at line1073βunit >= 0isFALSEfor negatives, so the existing-device check is skipped entirely.if (unit >= dc->maxunit)at line1094βmaxunitis non-negative, so a negativeunitmakes thisFALSE; the table is not extended and the negative unit is not caught.- The function falls through to
*unitp = unit; return(0);at lines1123-1124, returning success with the negative unit.
Back in devclass_add_device (sys/kern/subr_bus.c:1144):
dc->devices[dev->unit] = dev; /* writes 8 bytes at a negative array index */
dev->unit is the negative value returned above, so this writes a device_t
pointer before the start of the kmalloc'd dc->devices array, corrupting
adjacent heap metadata or objects.
device_set_unit (sys/kern/subr_bus.c:2166-2184) has a related OOB read at
its bounds check (sys/kern/subr_bus.c:2172):
if (unit < dc->maxunit && dc->devices[unit]) /* dc->devices[negative] read */
return(EBUSY);
A negative unit makes unit < dc->maxunit TRUE, so dc->devices[unit]
is an OOB read before the array; if it reads NULL, execution proceeds to
dev->unit = unit (:2177) and devclass_add_device, hitting the OOB write.
make_device passes caller-supplied unit straight through
devclass_add_device to the same sink.
Threat model & preconditions
- Attacker position: No demonstrated unprivileged-userspace trigger.
The unit parameter originates from bus driver code (
device_add_child,device_add_child_ordered) or from the loader hints (root-controlled). The finding is a latent memory-corruption defect: any driver that computes a unit which underflows below zero β e.g.unit = a - bwithb > a, a signed parse of a device-reported/HW field, or an arithmetic slip in an attacker-influenced (USB/thunderbolt/NFS-over-PCIe/etc.) path β reaches this sink and corrupts the kernel heap. - Privileges gained or impact: if reached, kernel heap corruption β an
attacker-influenced 8-byte pointer write at a selectable negative offset
from a
kmallocarray. Potentially exploitable for arbitrary kernel R/W (via corrupted slab/malloc metadata) and thus privilege escalation. - Required config or capabilities: default kernel. Reachability depends on a calling driver passing a negative unit.
- Reachability:
device_add_child(bus, drv, <negative>)βmake_deviceβdevclass_add_deviceβdevclass_alloc_unit; ordevice_set_unit(dev, <negative>). The huge driver tree undersys/dev/andsys/bus/is the realistic source of a miscomputed unit.
Proof of concept
PoC source: findings/poc/DF-0003/poc_negunit.c
A small kernel module that calls device_add_child(root_bus, "fakehack", -2)
to drive the OOB write directly. It requires root to kldload but proves both
that the write occurs and that nothing in devclass_alloc_unit rejects the
value.
Build & run
cc -I/sys -DKERNEL -c findings/poc/DF-0003/poc_negunit.c ld -r poc_negunit.o -o poc_negunit.ko kldload ./poc_negunit.ko # as root; INVARIANTS kernel recommended
Expected output
poc: OOB write occurred (child=0x...)
On an INVARIANTS kernel, subsequent heap operations typically panic with
slab/malloc assertions ("freed pointer ... modified", "use after free"),
proving the out-of-bounds write landed on heap metadata / an adjacent object.
The negative index is selectable (-2 .. -N) so a specific pre-array offset
can be targeted with heap grooming.
Impact
Latent kernel heap corruption reachable by any driver that computes a
negative unit number. The bug is in foundational (newbus) code used by every
device, so a single underflowed unit anywhere in the driver tree is a
potential local privilege-escalation / kernel-R/W primitive. No unprivileged
trigger was identified in this file; the fix is cheap and removes a real
memory-corruption footgun.
Recommended fix
Validate the unit at the entry of devclass_alloc_unit (reject < -1) and
mirror the guard in device_set_unit (reject < 0).
--- a/sys/kern/subr_bus.c
+++ b/sys/kern/subr_bus.c
@@ -1064,6 +1064,9 @@ static int
devclass_alloc_unit(devclass_t dc, int *unitp)
{
int unit = *unitp;
+
+ if (unit < -1)
+ return (EINVAL);
PDEBUG(("unit %d in devclass %s", unit, DEVCLANAME(dc)));
@@ -2165,6 +2168,9 @@ int
device_set_unit(device_t dev, int unit)
{
devclass_t dc;
int err;
+
+ if (unit < 0)
+ return (EINVAL);
dc = device_get_devclass(dev);
if (unit < dc->maxunit && dc->devices[unit])
References
sys/kern/subr_bus.c:1064βdevclass_alloc_unit(missing< -1guard).sys/kern/subr_bus.c:1144β OOB write sinkdc->devices[dev->unit] = dev.sys/kern/subr_bus.c:2172β matching OOB read indevice_set_unit.- CWE-787 Out-of-bounds Write; CWE-129 Improper Validation of Array Index.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/subr_bus.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0003 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_negunit.c | trigger-source | kld module: device_add_child(root_bus,"df3neg",-2) -- drives the negative-unit OOB-write sink | 2.8 KB | view raw |
| poc_ctrl.c | control-source | kld module: device_add_child(root_bus,"df3ctrl",0) -- valid unit; must load cleanly (the control) | 1.1 KB | view raw |
| Makefile | build-makefile | bsd.kmod.mk build for the trigger (correct kernel CFLAGS) | 302 B | β download |
| Makefile.ctrl | build-makefile | bsd.kmod.mk build for the control | 180 B | β download |
| setup_env.sh | build-setup | install machine forwarders on a guest that lacks them | 976 B | view raw |
| build.sh | build-script | build both .ko modules via make | 480 B | view raw |
| run.sh | run-script | load control (clean) then trigger (panic on #0) as root | 922 B | view raw |
| build.log | build-log | full successful bsd.kmod.mk build output (trigger + control) | 6.6 KB | view raw |
| run.log | run-log | decisive baseline run on #0: control marker + trigger panic (from serial boot.log) | 2.1 KB | view raw |
| panic.txt | panic-signature | crash signature: Fatal trap 12 at devclass_add_device+0xf6, fault VA 0xfffffffffffffff0 = subr_bus.c:1144 | 2.1 KB | view raw |
| env.txt | environment | uname, kern.version, cc version, kernel sha256, kldstat | 806 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, exploit-chain analysis, fix validation before/after | 8.9 KB | β raw |
| fix.diff | suggested-fix | git-apply-able (verified): reject unit<-1 in devclass_alloc_unit, unit<0 in device_set_unit | 893 B | view raw |
| fix_build.log | fix-build-log | full nativekernel build output of the single-fix #1 kernel (rc=0, no errors) | 5.6 MB | β download |
| fix_run.log | fix-run-log | patched #1 kernel PoC run: trigger returns FAIL/NULL, no panic, guest up (3/3 deterministic); before/after contrast | 2.0 KB | view raw |
| README.md | readme | human-facing build/run/reachability summary | 4.5 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0003 - devclass_alloc_unit() negative-unit heap out-of-bounds write
poc_negunit.c / poc_ctrl.c -- kld modules that drive the negative-unit
heap OOB-write sink in devclass_alloc_unit() / devclass_add_device()
(sys/kern/subr_bus.c).
The bug (memory-safety, CERTAIN -- reproduced on the audited kernel)
devclass_alloc_unit() only treats unit == -1 as a wildcard. Any other
negative unit (e.g. -2) enters the "wired unit" branch but skips the
existing-device check (unit >= 0, subr_bus.c:1073) and the table-extension
check (unit >= dc->maxunit, subr_bus.c:1094), so it returns success with
the negative unit. devclass_add_device() then executes
dc->devices[dev->unit] = dev; // sys/kern/subr_bus.c:1144
an 8-byte pointer write at a NEGATIVE index into the kmalloc'd dc->devices
array. device_set_unit() has a matching OOB read at subr_bus.c:2172.
Reproduction (VERIFIED)
A kld module that calls device_add_child(root_bus, "df3neg", -2):
- Control (
poc_ctrl.ko, unit=0) loads cleanly and printsDF0003-CTRL: unit=0 -> OK (child=0xfffff800...). Guest stays up. - Trigger (
poc_negunit.ko, unit=-2) panics the kernel immediately:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffffffffffffff0
fault code = supervisor write data, page not present
Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1)
addr2line -e /boot/kernel/kernel 0xffffffff8068a946 ->
sys/kern/subr_bus.c:1144 (the exact sink line).
The fault address 0xfffffffffffffff0 = (device_t*)NULL + (-2) =
0 + (-2)*8, i.e. the negative-index write target. For the freshly-created
devclass dc->devices == NULL, so the store hits an unmapped address and
the kernel page-faults on the WRITE -- at the sink line.
The ONLY difference between the control and the trigger is the literal unit
(0 vs -2), so the panic is caused specifically by the negative unit.
Reachability (the crux)
There is no unprivileged-userspace path to this sink in the default
kernel. device_add_child / devclass_add_device / devclass_alloc_unit
are internal newbus APIs; no syscall/ioctl invokes them. Auditing all 114
in-tree device_add_child* callers and the single device_set_unit caller
(sio.c):
- 84 pass the
-1wildcard (the legitimate, handled case); - the rest pass provably non-negative units -- PCI bus numbers
(
uint8_t secbus0-255 inpci_pci.c:344,busno,bus),for(unit=0;;unit++)loop counters (ata-all.c,ata-pci.c), a monotonically-increasingfreeunit/puc_find_free_unit()(starts >= 0 and only grows), andsio_pci_kludge_unit()'sunitthat starts at 0 and only++s.
So no in-tree driver computes a negative unit. The bug is therefore a
real-but-latent memory-corruption defect: reachable today only by root
(kldload, demonstrated here) or by any future/buggy driver that underflows a
unit (signed subtraction, signed parse of a device-reported field, etc.). The
fix is a one-line guard that converts the latent footgun into a hard EINVAL.
Build & run (on the DragonFly guest)
Building a kld requires the kernel source tree's headers; the guest ships
without /usr/src, so setup_env.sh installs a headers-only subset first.
# one-time (root): install kernel headers + machine forwarders sh setup_env.sh # build both modules (as any user) make SYSDIR=/usr/src/sys # -> poc_negunit.ko (trigger, -2) make -f Makefile.ctrl SYSDIR=/usr/src/sys # -> poc_ctrl.ko (control, 0) # run (root): control loads clean, trigger panics sh run.sh
Files
| file | purpose |
|---|---|
poc_negunit.c |
trigger source -- device_add_child(root_bus,"df3neg",-2) |
poc_ctrl.c |
control source -- device_add_child(root_bus,"df3ctrl", 0) |
Makefile / Makefile.ctrl |
build via bsd.kmod.mk (correct kernel CFLAGS) |
setup_env.sh |
install kernel headers + machine forwarders on the guest |
build.sh |
(legacy) hand-build path; superseded by the Makefiles |
run.sh |
load control then trigger |
build.log |
full successful build output |
run.log |
decisive run: control marker + trigger panic (from boot.log) |
panic.txt |
crash signature with addr2line proof |
env.txt |
guest uname/cc/config/kldstat |
VERDICT.md |
full narrative verdict |
fix.diff |
git-apply-able fix (reject < -1 in devclass_alloc_unit, < 0 in device_set_unit) |
manifest.json |
artifact catalog |
DF-0003 -- VERDICT
Verdict: REPRODUCED (panic/OOB write) + FIX VALIDATED (memory-corruption sink
confirmed at runtime on the unpatched master-DEV #0 kernel; single-fix kernel
#1 built from fix.diff closes the bug β the trigger returns gracefully with
no panic, deterministically across 3 runs).
Summary
devclass_alloc_unit() (sys/kern/subr_bus.c:1064-1125) only treats unit
== -1 as a wildcard. Any other negative unit (e.g. -2) slips past every
guard and is returned unchanged; devclass_add_device() then performs
dc->devices[dev->unit] = dev (subr_bus.c:1144) -- an 8-byte pointer
write at a negative array index. A tiny kld module calling
device_add_child(root_bus, "df3neg", -2) drives this sink directly and
panics the kernel at exactly subr_bus.c:1144. The control (unit=0)
loads cleanly.
Mechanism (trigger -> primitive -> effect), cited path:line
-
Trigger:
device_add_child(root_bus, "df3neg", -2)->device_add_child_ordered->make_device(subr_bus.c:1174). Withname="df3neg",make_devicecallsdevclass_find_internal(name, NULL, TRUE)which creates a fresh devclass withdc->devices = NULL; dc->maxunit = 0, thendevclass_add_device(dc, dev). -
Sink reach:
devclass_add_devicecallsdevclass_alloc_unit(dc, &dev->unit)(subr_bus.c:1139). -
The missing guard (
subr_bus.c:1064-1125): *int unit = *unitp;(:1067) ->unit = -2. *if (unit != -1)(:1072) -> TRUE (only-1is the wildcard), enter the "wired unit" branch. *if (unit >= 0 && unit < dc->maxunit && dc->devices[unit] != NULL)(:1073) ->unit >= 0is FALSE, the existing-device check is skipped. *if (unit >= dc->maxunit)(:1094) ->-2 >= 0is FALSE, the table-extension block is skipped --dc->devicesis not grown and the negative unit is not caught. **unitp = unit; return(0);(:1123-1124) -> returns success withdev->unit = -2. -
OOB write (
subr_bus.c:1144): back indevclass_add_device,dc->devices[dev->unit] = dev->dc->devices[-2] = dev. Withdc->devices == NULLthis stores the 8-bytedevpointer at address0 + (-2)*sizeof(device_t)=0xfffffffffffffff0(unmapped) -> supervisor WRITE page fault. -
Effect: fatal trap 12, kernel panic, guest wedged in DDB. The faulting instruction is
devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1)(the indexed 8-byte store).addr2lineresolves the IP tosys/kern/subr_bus.c:1144-- the exact sink line.
device_set_unit() (subr_bus.c:2165-2184) has a matching OOB read at
its bounds check if (unit < dc->maxunit && dc->devices[unit]) (:2172).
Reproduction evidence (unpatched #0 baseline)
Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
(with-src base = full /usr/src + warm obj + unpatched audit kernel).
- Control (
poc_ctrl.ko, unit=0) loaded cleanly:DF0003-CTRL: unit=0 -> OK (child=0xfffff801175bd1e0). Guest stays up. - Trigger (
poc_negunit.ko, unit=-2) panicked immediately. Serial console:Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffffffffffffff0 fault code = supervisor write data, page not present instruction pointer = 0x8:0xffffffff8068b236 Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1) db>fault VA0xfffffffffffffff0=(device_t*)NULL + (-2)=0 + (-2)*8, i.e. the negative-index write target.addr2line(prior verified run of identical source) ->sys/kern/subr_bus.c:1144. Guest wentdown(wedged in DDB).
The ONLY difference between control and trigger is the literal unit (0 vs
-2), so the panic is caused specifically by the negative unit.
Reachability -- why this is "latent" but real
device_add_child / devclass_add_device / devclass_alloc_unit are
internal newbus kernel APIs; no syscall or ioctl invokes them. The unit
originates from bus-driver code (device_add_child*) or loader hints
(root-controlled). Auditing the entire audited sys/ tree: 114 in-tree
device_add_child(_ordered) callers and the single device_set_unit caller;
84 pass the literal -1 wildcard (the handled case), the rest pass provably
non-negative units (PCI bus numbers, for(unit=0;;unit++) loop counters,
monotonic freeunit/puc_find_free_unit(), sio_pci_kludge_unit()).
=> No in-tree path produces a unit < -1. The bug is a real-but-latent
memory-corruption defect reachable today only by root (kldload,
demonstrated) or by any future/buggy driver that underflows a unit. The fix
is a one-line guard.
Exploit chain (memory-corruption class -- analysis)
- Primitive:
dc->devices[N] = devwith attacker-selectable negative indexN(the unit), writing the 8-byte kernel-heap pointerdevat offsetN*8beforedc->devices. - Triggering requirement: root (
kldload) or a buggy driver. Not reachable from unprivileged userspace. - This PoC's effect: deterministic panic (the easy fresh-devclass case
has
dc->devices == NULL, so the store faults immediately -- a clean DoS, not controllable corruption). - Controllable-corruption variant (theoretical): target an existing
devclass whose
dc->devicesis a real heap pointer, chooseunitsodc->devices[N]lands on an adjacent slab object (function pointer /ucred */ refcount), and groom the heap. This needs root (kldload) and a kernel-ROP/ucred-forgery conversion, beyond what an unprivileged attacker can reach. Realistic impact ceiling: local DoS by root + latent corruption for a future driver bug. No uid0 chain pursued -- there is no unprivileged trigger to escalate from, and the root case is already game-over for the attacker.
Fix validation (Phase 8 -- single-fix kernel)
Fix (fix.diff, git-apply-able, supersedes the finding markdown's draft --
adds explicit EINVAL returns + comments):
* devclass_alloc_unit entry: if (unit < -1) return (EINVAL);
(sys/kern/subr_bus.c, now at line ~1073).
* device_set_unit entry: if (unit < 0) return (EINVAL); (line ~2180).
Procedure:
1. vm.sh reset with-src -- clean source + #0 unpatched kernel. Re-confirmed
baseline panic (above).
2. Applied fix.diff to /usr/src: patch -p1 -> both hunks succeeded
(Hunk #1 succeeded at 1066, Hunk #2 succeeded at 2175).
3. Built single-fix kernel: make -j6 nativekernel KERNCONF=X86_64_GENERIC
-> NK_DONE rc=0, no errors. Produced kernel.debug + kernel.stripped.
4. Installed: cp kernel.stripped /boot/kernel/kernel (the bare name the
loader boots) + kernel.debug. sha256 5dc83dac...(#0) -> d7af6464...(#1).
5. Rebooted -> kern.version = DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2
12:26:06 UTC 2026 (the #1 + new build ts confirm the rebuilt kernel).
6. Re-ran the SAME PoC on #1.
Before/after contrast (the decisive evidence):
unpatched #0 |
patched #1 |
|
|---|---|---|
| control (unit=0) | OK, child=0xffff... | OK, child=0xffff... |
| trigger (unit=-2) | Fatal trap 12 page fault, devclass_add_device+0xf6, fault VA 0xfffffffffffffff0, guest DOWN in DDB, kprintf never reached |
kldload RC=0, kprintf reached: unit=-2 -> FAIL/NULL (child=0), guest UP, 0 panics |
| panic lines in boot.log | 1 (the trap) | 0 |
Repeatability on #1: 3/3 runs (unload + reload) all return FAIL/NULL,
guest up, 0 panics -- the fix is deterministic. device_add_child now
returns NULL because devclass_alloc_unit rejects unit<-1 with EINVAL,
so make_device fails before the dc->devices[dev->unit] = dev sink.
Fix verdict: FIXED. The single-fix kernel closes the OOB-write sink cleanly (panic -> graceful EINVAL).
PoC changes (vs. the filed PoC)
- Original
poc_negunit.cincluded<sys/bus.h>(drags in platform/APIC headers) and usedcc -DKERNEL(wrong macro -- the guard is_KERNEL). Forward-declared the newbus symbols and built via the standardbsd.kmod.mk. The hand-build (build.sh) is kept as a legacy path but the Makefile build is authoritative. - Added
poc_ctrl.c+Makefile.ctrl-- aunit=0control that loads cleanly, so the trigger panic is provably caused by the negative unit and not by module plumbing. - Added
setup_env.sh,run.sh,env.txt,panic.txt,build.log,run.log,manifest.json, andfix.diff. - This run: refreshed
run.log/panic.txtwith the#0baseline signature from today's build; regeneratedfix.diffwith clean git-format headers (the old one had a bogusindexhash line) and verifiedgit apply --checkpasses; addedfix_build.log(full nativekernel output) andfix_run.log(patched-kernel before/after contrast).
Recommended fix
Reject negative units at the entry of devclass_alloc_unit (< -1) and
device_set_unit (< 0). See fix.diff (git-apply-able, supersedes the
finding markdown's proposal -- adds explicit EINVAL returns and comments;
validated by building + booting a single-fix kernel that eliminates the panic).
Fix verification
fixedVALIDATED the fix. fix.diff applies cleanly (patch -p1, both hunks succeeded) and compiles (nativekernel NK_DONE rc=0, no errors). On the unpatched with-src #0 baseline, kldload poc_negunit.ko panics (Fatal trap 12 at devclass_add_device+0xf6 / subr_bus.c:1144, fault VA 0xfffffffffffffff0 = negative-index write, guest DOWN in DDB). On the single-fix #1 kernel the SAME kldload does NOT panic -- devclass_alloc_unit now returns EINVAL for unit<-1 so make_device fails before the sink, the module's kprintf is reached ('unit=-2 -> FAIL/NULL (child=0)'), the guest stays up, and there are 0 panic lines in boot.log, deterministic across 3 unload/reload cycles. Clean before/after => fix closes the bug. The valid unit=0 control still loads cleanly on both kernels.
baseline #0 trigger: 'Fatal trap 12: page fault ... fault virtual address = 0xfffffffffffffff0 ... Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1) db>' (guest down). patched #1 trigger (3/3): 'DF0003: unit=-2 name="df3neg" -> FAIL/NULL (child=0)' kldload RC=0, guest up, 0 panic lines. build: NK_DONE rc=0. kernel swap sha256 5dc83dac..(#0) -> d7af6464..(#1), kern.version #0 Thu Jul 2 06:02:54 -> #1 Thu Jul 2 12:26:06.
Confirmed kernel references
Detail
Exploit chain
Memory-corruption primitive: dc->devices[N]=dev with attacker-selectable negative index N (=the unit), writing the 8-byte kernel-heap device_t pointer at offset N8 before the kmalloc'd dc->devices array. Triggering requires root (kldload) or a buggy driver that underflows a unit -- NOT reachable from unprivileged userspace. This PoC's demonstrated effect is a deterministic panic (the easy fresh-devclass case has dc->devices==NULL so the store faults immediately at subr_bus.c:1144 = a clean DoS, not controllable corruption). A theoretical controllable-corruption variant (target an existing devclass with a real dc->devices heap pointer, choose unit so dc->devices[N] lands on an adjacent slab object's function-pointer/ucred/refcount, groom the heap) would need root + a kernel-ROP/ucred-forgery conversion. No uid0 chain was pursued: there is no unprivileged trigger to escalate from and the root case is already game-over for the attacker. Realistic impact ceiling = local DoS by root + latent heap corruption for a future driver bug.
Evidence (decisive lines)
BASELINE (#0, unpatched): control DF0003-CTRL: unit=0 -> OK (child=0xfffff801175bd1e0), guest up; trigger kldload poc_negunit.ko => 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffffffffffffff0 / fault code = supervisor write data, page not present / Stopped at devclass_add_device+0xf6: movq %r14,(%rdx,%rax,1) / db>' guest DOWN. PATCHED (#1, fix.diff): control DF0003-CTRL: unit=0 -> OK (child=0xfffff801175bac60) guest up; trigger kldload RC=0, console 'DF0003: unit=-2 name="df3neg" -> FAIL/NULL (child=0)' guest UP, 0 panic lines in boot.log (3/3 deterministic runs). (Full untrimmed logs in findings/poc/DF-0003/{run.log,panic.txt,fix_run.log,fix_build.log}.)
PoC changes
Refreshed run.log/panic.txt with the today's #0 baseline panic signature (fault IP 0xffffffff8068b236 = devclass_add_device base+0xf6, same offset as the prior verified run). Regenerated fix.diff with clean git-format headers -- the prior version carried a bogus 'index d33631d0..0000000' line; verified the new one passes 'git apply --check' and 'patch -p1' (both hunks succeeded: Hunk #1 at 1066, Hunk #2 at 2175). Added fix_build.log (full 35389-line nativekernel output, NK_DONE rc=0, no errors), fix_run.log (patched-kernel before/after contrast, 3/3 deterministic), and rewrote VERDICT.md with the full reproduction + Phase-8 fix-validation narrative. Updated manifest.json with fix_kernel_uname and the new fix_* artifacts.
Verified recommended fix
In sys/kern/subr_bus.c, add 'if (unit < -1) return (EINVAL);' at the entry of devclass_alloc_unit (after 'int unit = *unitp;') and 'if (unit < 0) return (EINVAL);' at the entry of device_set_unit (before the 'unit < dc->maxunit' bounds check that would otherwise OOB-read). This rejects negative units at the two reachable entry points so dc->devices[dev->unit]=dev at :1144 can never be indexed negatively. Supersedes the finding markdown's proposal (adds explicit EINVAL returns + explanatory comments; same logical guards). Validated by building + booting a single-fix #1 kernel that converts the trigger from a page-fault panic into a graceful NULL/Fail. The full git-apply-able diff lives in findings/poc/DF-0003/fix.diff.
Verdict
REPRODUCED + FIX VALIDATED. devclass_alloc_unit() (sys/kern/subr_bus.c:1064-1125) only treats unit==-1 as wildcard; any other negative unit (e.g. -2) skips the unit>=0 existing-device check (:1073) and the unit>=dc->maxunit table-extension check (:1094) and returns success, so devclass_add_device() executes dc->devices[dev->unit]=dev (:1144) -- an 8-byte pointer write at a negative array index. A kld module calling device_add_child(root_bus,"df3neg",-2) drives this sink directly: on the unpatched #0 kernel it panics with Fatal trap 12, fault VA 0xfffffffffffffff0 (= (device_t)NULL + (-2) = 0+(-2)8, the negative-index write target), Stopped at devclass_add_device+0xf6 (addr2line-> subr_bus.c:1144), guest wedged in DDB. The control module (unit=0) loads cleanly and prints its marker, proving the panic is caused specifically by the negative unit. The sink is internal newbus API (no unprivileged trigger in-tree) so realistic impact is root-gated local DoS + latent heap-OOB-write for any future driver that underflows a unit. The single-fix kernel #1 built from fix.diff (reject unit<-1 in devclass_alloc_unit, unit<0 in device_set_unit) eliminates the panic: the same kldload now returns with the module's kprintf reached -- 'unit=-2 -> FAIL/NULL (child=0)' -- because devclass_alloc_unit returns EINVAL before the sink; guest stays up, 0 panics, deterministic across 3 runs.
No comments yet.