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

Unprivileged kernel-pointer leak via hw.bus.devices sysctl (dv_handle/dv_parent)

Summary

World-readable sysctl hw.bus.devices copies raw kernel virtual address of every device_t and parent into dv_handle/dv_parent fields of struct u_device returned to userland. Any local user can read it sysctl framework requires no privilege for reads only writes gated by SYSCAP_NOSYSCTL_WR. Defeats KASLR discloses kernel heap layout materially aiding exploitation of any other memory-corruption bug. Contrast /dev/devctl IS gated SYSCAP_RESTRICTEDROOT but device-tree introspection that hands out kernel addresses is not. struct u_device zeroed first so no uninit leak only intentional pointer fields leak.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2543 Β· 14 files
FileTypeDescriptionSize
poc.c trigger-source reads hw.bus.devices, extracts dv_handle/dv_parent, reports leaked pointer count 3.7 KB view raw
build.sh build-script cc -O2 -o poc poc.c 127 B view raw
run.sh run-script ./poc as unprivileged user 116 B view raw
README.md readme claim, build/run/expected 1.2 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, repro, fix, before/after 6.1 KB ↓ raw
fix.diff suggested-fix git-apply-able: drop raw device_t pointer assignments in sysctl_devices 1.0 KB view raw
build.log build-log PoC build output (clean) 13 B view raw
run.log run-log decisive unpatched-baseline run (219 pointers) 8.7 KB view raw
run.2.log run-log stress run 2 (byte-identical => stable addrs) 8.7 KB view raw
run.3.log run-log stress run 3 (byte-identical) 8.7 KB view raw
leak_sample.txt leak-sample sample of leaked pointers + 3-run summary 1.9 KB view raw
fix_build.log build-log full single-fix kernel build output (rc=0) 5.6 MB ↓ download
fix_run.log run-log PoC on patched kernel #1 (0 pointers leaked) 8.7 KB view raw
env.txt environment uname, cc version, sysctl state 383 B view raw
README.md readme claim, build/run/expected
↓ download raw

DF-2543 β€” hw.bus.devices kernel-pointer leak

Claim

World-readable sysctl hw.bus.devices (sys/kern/subr_bus.c, sysctl_devices()) copies the raw kernel virtual address of every device_t and its parent into the dv_handle/dv_parent fields of struct u_device (sys/sys/bus.h) returned to userland. Any local user can read it (sysctl framework gates only writes via SYSCAP, not reads). Defeats KASLR; discloses kernel-heap layout.

Build

cc -O2 -o poc poc.c        # (or ./build.sh)

Run (unprivileged)

./poc                      # (or ./run.sh)   as uid 1001 (maxx, NOT in wheel)

Expected

  • Bug present (unpatched #0): dv_handle / dv_parent are non-zero 0xffff... kernel pointers for every device; summary reports LEAK CONFIRMED -- N raw kernel pointers disclosed.
  • Fixed: dv_handle == 0 && dv_parent == 0 for every device; summary reports NO LEAK.

Notes

  • The device tree is enumerated by index: hw.bus.devices.<generation>.<index>. The generation is read first from hw.bus.info (which returns struct u_businfo).
  • struct u_device is bzero'd before copyout, so this is NOT an uninitialized-memory leak β€” it is the intentional pointer fields that leak.
VERDICT.md verdict full narrative: mechanism, repro, fix, before/after
↓ download raw

DF-2543 β€” VERDICT

Verdict: REPRODUCED (info leak) + FIX VALIDATED

Finding DF-2543 β€” Unprivileged kernel-pointer leak via hw.bus.devices sysctl
File sys/kern/subr_bus.c (sysctl_devices)
Severity Low (CWE-200; CVSS 3.1 AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N)
Class Information disclosure β€” kernel pointer leak (KASLR defeat)
Status REPRODUCED on 6.5-DEVELOPMENT #0 (unpatched baseline)
Impact leak:219 raw device_t kernel pointers per read (world-readable)
Fix fixed β€” single-fix kernel #1 leaks 0 pointers

Mechanism (confirmed by source trace + PoC)

sys/kern/subr_bus.c sysctl_devices() (the handler for the hw.bus.devices sysctl node) populates a struct u_device (sys/sys/bus.h:87) and copies it back to userland. The two pointer-shaped fields are filled with raw kernel virtual addresses:

sys/kern/subr_bus.c:3889   bzero(&udev, sizeof(udev));
sys/kern/subr_bus.c:3890   udev.dv_handle = (uintptr_t)dev;          /* raw device_t */
sys/kern/subr_bus.c:3891   udev.dv_parent = (uintptr_t)dev->parent;  /* raw parent device_t */

The node is created world-readable (CTLFLAG_RD):

sys/kern/subr_bus.c:3908   SYSCTL_NODE(_hw_bus, OID_AUTO, devices, CTLFLAG_RD, sysctl_devices, ...);

The DragonFly sysctl framework gates only writes via SYSCAP (sys/kern/kern_sysctl.c); reads require no privilege. Contrast /dev/devctl, which is gated SYSCAP_RESTRICTEDROOT β€” device-tree introspection that hands out kernel addresses is not. The struct is bzero'd first, so this is not an uninitialized-memory leak; it is the intentional pointer fields that disclose the kernel address space.

The dv_handle addresses are contiguous with a 0xa0 (160-byte) stride (0xfffff8008bb7f6c0, …f800, …f8a0, …), directly revealing the device_t slab/heap layout. On a KASLR-on system this single read defeats kernel-text randomization and materially aids exploitation of any other memory-corruption bug. (This audit guest runs with KASLR off, but the disclosure holds on any realistic deployment.)

Primitive type β€” read-only

The sysctl is read-only from userspace; there is no write/corruption primitive here. Per the DF-POCRUNNER hard-blocker rules, a pure read-only info leak has no escalation chain to uid=0 β€” the leak itself is the finding's impact ceiling (KASLR/heap-layout defeat). No Phase-6 escalation aplicable.

Reproduction (unpatched #0 baseline)

  • Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
  • User: maxx uid 1001, not in wheel.
  • Build: cc -O2 -o poc poc.c (clean).
  • Run: ./poc reads hw.bus.info for the generation, then enumerates hw.bus.devices.<gen>.<idx> for every index, printing dv_handle/dv_parent.

Result (3 byte-identical runs β€” the pointers are stable real kernel addresses, not random stack residue):

[  0] handle=0xfffff8008bb7f6c0* parent=0x0000000000000000   root0
[  1] handle=0xfffff8008bb7f800* parent=0xfffff8008bb7f6c0*  nexus0
[  2] handle=0xfffff8008bb7f8a0* parent=0xfffff8008bb7f800*  acpi0
...
devices enumerated : 110
leaked dv_handle   : 110  (raw device_t kernel pointers)
leaked dv_parent   : 109  (raw parent device_t kernel pointers)
RESULT: LEAK CONFIRMED -- 219 raw kernel pointers disclosed (KASLR defeated)

All leaked values fall in the canonical 0xfffff800... kernel-virtual range. The world-readability is proven empirically: maxx (not in wheel) read every record with no privilege.

PoC changes

The PoC was authored from scratch (the seeded dir was empty). One iteration was needed: the first version initialized the sysctlnametomib length argument to the expected MIB depth (2) instead of the array capacity (CTL_MAXNAME), which returned ENOMEM because the resolved MIB is 3 elements (6.279.257). Corrected to miblen = CTL_MAXNAME; the read then succeeded.

Fix (authored, built, validated)

fix.diff removes the two raw-pointer assignments at subr_bus.c:3890-3891. Because udev is already bzero'd at line 3889, dv_handle/dv_parent simply stay 0 β€” no raw kernel pointer is copied to userland. The fix is minimal, targeted at the root cause, and git apply --check clean.

A production follow-up (noted in the diff comment) could substitute a non-reversible per-boot cookie so that the only known userland consumer that relies on dv_handle/dv_parent for parent↔child correlation β€” devinfo(8) β€” keeps working without disclosing the raw pointer. Zeroing is sufficient to close the leak and does not affect device enumeration or any field except the two handle fields.

Fix validation (Phase 8) β€” clean before/after

Kernel kern.version dv_handle leaked dv_parent leaked result
unpatched baseline #0 Jul 2 06:02:54 110 109 LEAK (219 ptrs)
single-fix kernel #1 Aug 8 19:15:20 0 0 NO LEAK
  • Patched kernel built clean (make -j6 nativekernel, rc=0).
  • Installed via install ... kernel.stripped β†’ /boot/kernel/kernel (sha256 verified to match kernel.stripped exactly β€” an earlier manual cp corrupted the file due to shell quoting and produced a non-loadable image; the canonical install + sha check fixed it).
  • Booted cleanly to a login prompt (#1, no panic).
  • Same PoC, same unprivileged user, same 110 devices enumerated β€” only the two pointer fields changed: all 0x0. Deterministic across 2 runs.
-   bzero(&udev, sizeof(udev));
-   udev.dv_handle = (uintptr_t)dev;
-   udev.dv_parent = (uintptr_t)dev->parent;
+   bzero(&udev, sizeof(udev));
+   /* SECURITY comment: dv_handle/dv_parent intentionally left 0 */

fix_status = fixed (baseline reproduces, patched does not).

Notes for maintainers

  • This leak is the DragonFly analogue of the FreeBSD kern.device_tree / hw.bus hardening work; FreeBSD likewise moved away from exporting raw pointers in its bus sysctl.
  • Realistic impact ceiling: KASLR defeat + device_t heap-layout disclosure to any local user. Not directly corrupting, but a useful primitive for a local attacker chaining it with a separate memory-corruption bug.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: PoC leaks 219 raw device_t kernel pointers on unpatched #0 baseline (Jul 2 06:02:54) and leaks ZERO on single-fix #1 kernel (Aug 8 19:15:20) β€” dv_handle and dv_parent are 0x0 for all 110 devices while device tree still enumerates fully. Clean before/after on same PoC + same unprivileged user.

baseline #0: leaked dv_handle=110, dv_parent=109, RESULT: LEAK CONFIRMED -- 219 raw kernel pointers disclosed. patched #1: leaked dv_handle=0, dv_parent=0, RESULT: NO LEAK -- dv_handle/dv_parent sanitized (fixed) (devices enumerated=110 in both => fix surgical, only pointer fields changed).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 19:15:20 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none (read-only info leak -- no write/corruption primitive). The sysctl is read-only from userspace; primitive is pure KASLR-defeat / kernel-heap-layout disclosure. Per Phase-6 hard-blocker rules a genuinely read-only primitive has no escalation chain to uid=0; the leak itself (219 kernel pointers, world-readable) is the finding's impact ceiling. No escalation file authored because class is non-corruption.

Evidence (decisive lines)

[  0] handle=0xfffff8008bb7f6c0 parent=0x0000000000000000 root0 / [  1] handle=0xfffff8008bb7f800 parent=0xfffff8008bb7f6c0 nexus0 / [  2] handle=0xfffff8008bb7f8a0 parent=0xfffff8008bb7f800 acpi0 ... (3 runs byte-identical => stable KVA) / devices enumerated 110 / leaked dv_handle 110 (raw device_t kernel pointers) / leaked dv_parent 109 (raw parent device_t kernel pointers) / RESULT: LEAK CONFIRMED -- 219 raw kernel pointers disclosed (KASLR defeated) / user uid=1001(maxx) NOT in wheel.

PoC changes

PoC authored from scratch (seeded dir empty): reads hw.bus.info for generation count, then enumerates hw.bus.devices.. and prints dv_handle/dv_parent for each device. One iteration: first version initialized sysctlnametomib length to expected MIB depth (2) instead of array capacity (CTL_MAXNAME), returning ENOMEM since resolved MIB is 3 elements (6.279.257); corrected to miblen=CTL_MAXNAME. Added local struct u_device definition (matches sys/sys/bus.h layout) to avoid pulling heavy kernel bus.h into userland.

Verified recommended fix

In sys/kern/subr_bus.c sysctl_devices(), remove the two raw-pointer assignments udev.dv_handle=(uintptr_t)dev and udev.dv_parent=(uintptr_t)dev->parent at lines 3890-3891. Because udev is already bzero()'d at line 3889, the fields stay 0 and no kernel pointer copied to userland; device tree still fully enumerates. Production follow-up could substitute non-reversible per-boot cookie if devinfo(8) parent/child correlation must be preserved. Full git-apply-able diff in findings/poc/DF-2543/fix.diff.

Verdict

REPRODUCED. The bug is real and confirmed by source trace + PoC: sysctl_devices() at sys/kern/subr_bus.c:3890-3891 copies the raw device_t kernel virtual address (and its parent's) into the dv_handle/dv_parent fields of struct u_device (sys/sys/bus.h:88-89) returned to userland, and the node is CTLFLAG_RD (world-readable; sysctl framework gates only writes via SYSCAP, never reads β€” subr_bus.c:3908). As non-wheel user maxx (uid 1001), the PoC read 110 device records and extracted 110 dv_handle + 109 dv_parent = 219 raw 0xfffff800... kernel pointers (byte-identical across 3 runs => stable real addresses, not stack residue; the 0xa0 stride discloses the device_t slab layout). The struct is bzero'd first, so this is intentional pointer disclosure, not an uninit leak.