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)
PoC verification
Evidence pack
findings/poc/DF-2262 · 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_poison → Fatal trap 9 ... Stopped at
xptbustraverse+0x61, guest down (panic.txt).
- patched #1: ./xpt_devmatch_poison → ioctl 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
fixedVALIDATED: 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.
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.
No comments yet.