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)
PoC verification
Evidence pack
findings/poc/DF-2543 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_parentare non-zero0xffff...kernel pointers for every device; summary reportsLEAK CONFIRMED -- N raw kernel pointers disclosed. - Fixed:
dv_handle == 0 && dv_parent == 0for every device; summary reportsNO LEAK.
Notes
- The device tree is enumerated by index:
hw.bus.devices.<generation>.<index>. The generation is read first fromhw.bus.info(which returnsstruct u_businfo). struct u_deviceisbzero'd before copyout, so this is NOT an uninitialized-memory leak β it is the intentional pointer fields that leak.
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:
maxxuid 1001, not inwheel. - Build:
cc -O2 -o poc poc.c(clean). - Run:
./pocreadshw.bus.infofor the generation, then enumerateshw.bus.devices.<gen>.<idx>for every index, printingdv_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 matchkernel.strippedexactly β an earlier manualcpcorrupted the file due to shell quoting and produced a non-loadable image; the canonicalinstall+ 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.bushardening work; FreeBSD likewise moved away from exporting raw pointers in its bus sysctl. - Realistic impact ceiling: KASLR defeat +
device_theap-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
fixedVALIDATED: 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).
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.
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.
No comments yet.