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

```diff
-	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.
