DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2262

XPT_DEV_MATCH trusts user-supplied EDT cookie kernel pointers: arbitrary kernel-address deref UAF KASLR leak

Summary

cdm->pos.cookie.{bus target device periph pdrv} fields are raw kernel object pointers kernel writes into user CCB on partial-match return (info leak of kernel heap addresses) and trusts verbatim on next call. xptedtmatch passes cdm->pos.cookie.bus straight to xptbustraverse which dereferences at TAILQ_NEXT(bus links) and CAM_SIM_LOCK(bus->sim). Only generation check trivially bypassed by setting generations[0]=0. Three impacts: (1) reliable kernel panic DoS by setting cookie.bus=(void*)0x4141...41 (2) KASLR bypass legitimate XPT_DEV_MATCH returns CAM_DEV_MATCH_MORE causes kernel to write live kernel heap pointers into user-shared CCB user reads cdm->pos.cookie.* (3) UAF controlled-deref primitive capture valid cookie.bus force underlying cam_eb freed (USB unplug bus deregister) resubmit stale cookie xptbustraverse operates on freed memory fake cam_eb->sim pointer dereferenced by CAM_SIM_LOCK lockmgr on attacker-chosen lock. /dev/xpt0 mode 0600 root-only. Attacker: root on host or root inside jail given /dev/xpt0.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2262 · 9 files
FileTypeDescriptionSize
VERDICT.md verdict root-level panic confirmation + perm-gate analysis + source trace 5.0 KB ↓ raw
xpt_devmatch_poison.c trigger-source XPT_DEV_MATCH cookie-poison trigger (root) 3.4 KB view raw
build.sh build-log cc -I/usr/src/sys build line 411 B view raw
run.sh run-log root-panic / maxx-gate dispatch 535 B view raw
panic.txt panic-signature Fatal trap 9 at xptbustraverse+0x61 (from boot.log) 1.6 KB view raw
fix.diff suggested-fix make generation check mandatory when cookie.bus != NULL 1.7 KB view raw
fix_build.log build-log nativekernel rc=0 with all fixes (-Werror -DINVARIANTS) 5.6 MB ↓ download
fix_run.log run-log PATCHED #1 kernel: trigger returns cdm.status=2, no panic, guest up 420 B view raw
env.txt environment guest uname, kldstat, /dev perms, maxx groups 2.7 KB view raw
VERDICT.md verdict root-level panic confirmation + perm-gate analysis + source trace
↓ download raw

DF-2262 — XPT_DEV_MATCH trusts user-supplied EDT cookie (cam_xpt.c)

Verdict: REPRODUCED at root level (panic confirmed) — NOT an unprivileged escalation; fix.diff compiles.

Classification: reproduced (the source bug is real and panics the kernel when triggered) / impact=panic (root→kernel DoS; the finding's own attacker model is "root on host or root inside jail given /dev/xpt0") / the unprivileged (maxx) path is permission-gated (/dev/xpt0 is 0600 root:operator, maxx not in operator). The defense-in-depth fix.diff compiles cleanly (nativekernel rc=0, -Werror, -DINVARIANTS).

What was reproduced (root-level live confirmation)

The trigger harness xpt_devmatch_poison.c opens /dev/xpt0 and issues CAMIOCOMMAND with func_code = XPT_DEV_MATCH, position_type = EDT|BUS, generations[CAM_BUS_GENERATION] = 0 (bypassing the generation check), and cookie.bus = 0x4141414141414141. The kernel traps immediately in xptbustraverse:

Fatal trap 9: general protection fault while in kernel mode
instruction pointer = 0x8:0xffffffff802acbf1
current process       = 996
Stopped at      xptbustraverse+0x61:    movq    0x10(%r15),%r13

(see panic.txt). The faulting instruction dereferences the fake struct cam_eb * the kernel took verbatim from the user CCB.

Build (in-guest, root): cc -I/usr/src/sys -o xpt_devmatch_poison xpt_devmatch_poison.c → builds clean (9120-byte binary). Run as root panics the kernel (ssh dies, vm.sh status ⇒ down); run as maxx fails with Permission denied on /dev/xpt0.

The source bug (cited path:line)

sys/bus/cam/cam_xpt.c: - xptioctl CAMIOCOMMAND accepts XPT_DEV_MATCH from userspace, maps the user buffers, and calls xpt_action(inccb) (cam_xpt.c:1125-1179). - xpt_action XPT_DEV_MATCH (:3262-3323) picks the EDT path and calls xptedtmatch(cdm) (:3307). - xptedtmatch (:2427-2461): the generation guard at :2437-2439 is bypassable — it only fires when generations[CAM_BUS_GENERATION] != 0 AND differs from xsoftc.bus_generation, so setting generations[0] = 0 skips it entirely. Then, if position_type & CAM_DEV_POS_BUS and cookie.bus != NULL (:2444-2445), it passes the raw user pointer to xptbustraverse((struct cam_eb *)cookie.bus, …) (:2446) with no validation that it names a live cam_eb. - xptbustraverse (:2631+) dereferences it: TAILQ_NEXT(bus, links), CAM_SIM_LOCK(bus->sim) (:2642, :2645) → controlled deref → trap. This confirms finding claim (1): reliable kernel panic DoS.

The other two claims (info leak of kernel heap pointers via legitimate CAM_DEV_MATCH_MORE returns; UAF via captured-valid-cookie + bus deregister) were not separately exercised — claim (1) is sufficient to confirm the bug is live and the trust-the-cookie defect is real.

Why this is a root→kernel finding, NOT an unprivileged escalation

/dev/xpt0 is crw------- root operator (mode 0600). maxx (uid 1001, groups=1001(maxx), not in operator) cannot open it:

$ id maxx                    -> uid=1001(maxx) ... groups=1001(maxx)
$ su -m maxx -c "camcontrol devlist"
camcontrol: couldn't open /dev/xpt0: Permission denied

The finding's own stated attacker is "root on host or root inside jail given /dev/xpt0". Root→kernel is game-over by definition, so the unprivileged-escalation impact is none (gated) while the source bug is reproduced (panic) and the fix is warranted as defense-in-depth.

Exploit chain

No unprivileged chain: the device is 0600 root-only, so maxx has no entry point. The root-level panic is the demonstrated effect (claim 1 of the finding). A fuller exploit (claims 2/3: KASLR-bypass info leak, UAF via bus-deregister) would also be root-only and is not pursued — they add no unprivileged impact.

Fix (defense-in-depth, VALIDATED — built, booted, re-tested)

fix.diff makes the generation check mandatory whenever cookie.bus != NULL: a caller that wants to resume from a cookie must supply a current generations[CAM_BUS_GENERATION], closing the trivial generations[0]=0-plus-arbitrary-cookie bypass. (Full hardening would additionally re-validate that cookie.bus names a live cam_eb; the shipped fix closes the trivial controlled-deref.) Validated in the combined nativekernel build (fix_build.log).

Phase 8 before/after (single-fix cam_xpt.c kernel, built + booted): - baseline #0 (unpatched): ./xpt_devmatch_poisonFatal trap 9 ... Stopped at xptbustraverse+0x61, guest down (panic.txt). - patched #1: ./xpt_devmatch_poisonioctl returned 0, cdm.status=2 (CAM_DEV_MATCH_LIST_CHANGED — the fix's safe return), guest stays up (fix_run.log). The controlled-deref panic is gone.

Reproduce (root only)

cc -I/usr/src/sys -o xpt_devmatch_poison xpt_devmatch_poison.c   # builds clean
./xpt_devmatch_poison                                            # as root -> panic

As maxx: Permission denied on /dev/xpt0 (gate).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: cookie-poison trigger PANICS unpatched #0 baseline (Fatal trap 9 at xptbustraverse+0x61, guest down) and does NOT panic single-fix #1 kernel (ioctl returns 0, cdm.status=2=CAM_DEV_MATCH_LIST_CHANGED, guest stays up). Fix closes controlled-deref panic by refusing stale cookie when generation absent/mismatched.

baseline (#0): ./xpt_devmatch_poison -> 'Fatal trap 9 ... Stopped at xptbustraverse+0x61' / db> (guest down). patched (#1): ./xpt_devmatch_poison -> 'ioctl returned 0, cdm.status=2' / RUN_EXIT=0 / guest up.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Aug 9 12:03:06 UTC 2026 (sha256 d426e80ada7c7af2760e0d0585cfd8f3a9cbbe31c46759c227dba41af6f2a88e)

Confirmed kernel references

Detail

Exploit chain

Confirmed primitive: controlled kernel-pointer dereference from root-only ioctl (/dev/xpt0 0600). Trigger sets cdm->pos.cookie.bus to arbitrary non-NULL, kernel dereferences it in xptbustraverse -> fatal trap. root->kernel DoS. No unprivileged chain: maxx cannot open /dev/xpt0 (EACCES). Info-leak and UAF claims also root-only.

Evidence (decisive lines)

BEFORE (unpatched #0, root): Fatal trap 9: general protection fault / ip=0x8:0xffffffff802acbf1 / current process=996 / Stopped at xptbustraverse+0x61: movq 0x10(%r15),%r13 / db> (guest down). AFTER (patched #1, root): ioctl returned 0, cdm.status=2 (CAM_DEV_MATCH_LIST_CHANGED) / RUN_EXIT=0 / guest up. GATE (maxx): id uid=1001 groups=1001; camcontrol devlist -> couldn't open /dev/xpt0: Permission denied.

PoC changes

Created findings/poc/DF-2262/{xpt_devmatch_poison.c, VERDICT.md, fix.diff, manifest.json, build.sh, run.sh, env.txt, panic.txt, fix_run.log, fix_build.log}. fix.diff makes generation check mandatory when cookie.bus != NULL.

Verified recommended fix

fix.diff makes generation check MANDATORY whenever cdm->pos.cookie.bus != NULL in xptedtmatch (cam_xpt.c:2437-2446). Full hardening would re-validate cookie.bus names a live cam_eb. VALIDATED end-to-end: panic on #0 -> clean return on #1. Full diff in findings/poc/DF-2262/fix.diff.

Verdict

REPRODUCED at root level (panic confirmed); NOT unprivileged escalation. XPT_DEV_MATCH cookie-trust bug (cam_xpt.c:1125 xptioctl CAMIOCOMMAND accepts XPT_DEV_MATCH; xptedtmatch :2437-2439 generation guard bypassable by setting generations[CAM_BUS_GENERATION]=0; :2444-2446 passes raw user cdm->pos.cookie.bus pointer verbatim to xptbustraverse; xptbustraverse :2631/:2642/:2645 dereferences it via TAILQ_NEXT/CAM_SIM_LOCK) is REAL and LIVE. Trigger xpt_devmatch_poison.c (open /dev/xpt0, CAMIOCOMMAND position_type=EDT|BUS, generations[0]=0, cookie.bus=0x4141...) panics: 'Fatal trap 9 ... Stopped at xptbustraverse+0x61: movq 0x10(%r15),%r13'. HOWEVER /dev/xpt0 is 0600 root:operator; maxx gets EACCES. root->kernel DoS, NOT unprivileged.