⬢ DragonFlyBSD Kernel Audit
DF-1359 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-1359",
  "status": "reproduced",
  "verdict": "REPRODUCED (pass 3, 2026-10-11): full unprivileged-to-root escalation through the REAL production mps ioctl handler of the shipped kernel. An operator-group user (uid=2/gid=5, no other privilege) opened the rigs 0640 root:operator node O_RDONLY and issued MPTIOCTL_PASS_THRU; the real mps_user_pass_thru (0xffffffff805b0be0 in kernel #0, md5 4625046e...) executed copyin(PtrRequest, &tmphdr, RequestSize) at mps_user.c:802 into its 12-byte stack tmphdr (rbp-0x3c) with the only bounds check (:806, facts->IOCRequestFrameSize*4) running AFTER the copy. RequestSize=132 wrote 120 attacker-controlled bytes through the frame, overwriting the saved RIP at &tmphdr+0x44 (epilogue at mps_user_pass_thru+0x62d pops rbx,r12,r13,r14,r15,rbp,ret entirely from the copyind bytes). The EINVAL error return then executed the attackers RIP: a user RWX page (no SMEP) whose shellcode walked gs:0 -> gd_curthread(+8) -> td_proc(+168) -> p_ucred(+16), zeroed cr_uid/cr_ruid/cr_svuid/cr_groups[0]/cr_rgid/cr_svgid, and swapgs+iretqd to a userspace continuation reporting getuid()=0 and spawning a root shell. 5/5 probe successes (incl. 1 from a fresh vm.sh reset with-src snapshot) + 1/1 evaded (RequestSize=96, which the post-hoc check ACCEPTS -- dmesg even records the real production unknown: mps_user_pass_thru: no mps requests ENOMEM print immediately before the takeover). Deterministic destructive proofs: rip mode -> Fatal trap 9 at mps_user_pass_thru+0x63e: ret with the attacker tag as the popped RIP; smash mode (RequestSize=4096, unbounded because the check runs after the copy) -> same trap with frame pointer 0x4546474841424344. The single root-staged element is the KLD rig standing in for the LSI Fusion-MPT PCI attach QEMU cannot emulate (fabricated softc with facts block at +0x148 holding IOCRequestFrameSize=32, mps_lock +0x190, free-list head +0xd0 NULL, 0640 make_dev exactly as mps_attach_user); the rig byte-verifies the production handler prologue AND the vulnerable copyin site before every dispatch, and everything from dev_dioctl down is shipped-kernel code. On real mps hardware the HBA performs that attach and the whole chain is attacker-only from open(\"/dev/mps0\", O_RDONLY). Fix validated on a rebuilt kernel (fix.diff 3/3 hunks, nativekernel rc=0, kernel #1 md5 06407b51...): all four modes return cleanly (EINVAL x5, ENOMEM x1), zero traps, guest healthy; disassembly confirms the range check now precedes the copyin, which is limited to sizeof(tmphdr)=12 bytes.",
  "reproduced": true,
  "impact": "uid0_privesc",
  "confidence": "certain",
  "exploit_chain": "operator uid=2/gid=5 -> open 0640 root:operator node O_RDONLY (ioctls not gated on open mode) -> ioctl(MPTIOCTL_PASS_THRU, RequestSize=132) -> dev_dioctl -> REAL mps_ioctl_devsw @0xffffffff805b16d0 -> mps_user_pass_thru @0xffffffff805b0be0 -> copyin into 12-byte tmphdr at rbp-0x3c (site +0x7b) smashes 120 bytes -> saved RIP at &tmphdr+0x44 = user RWX page 0x10000000 (no SMEP, no KASLR, no stack canary) -> post-hoc check rejects 132>128, EINVAL path converges at +0x723 -> epilogue ret through smashed RIP -> shellcode: mov rax,[gs:0]; rax+8=curthread; +168=proc; +16=ucred; zero cr_uid/cr_ruid/cr_svuid/cr_groups/cr_rgid/cr_svgid; swapgs; push iretq frame (ss,rsp,rflags,cs,rip) -> userspace continuation -> getuid()=0 -> execl /bin/sh -> uid=0(root) gid=0(wheel). Evaded variant: RequestSize=96 passes the post-hoc check, ENOMEM path (free-list head sc+0xd0 NULL) returns through the same smashed RIP -> same uid=0.",
  "evidence": "run.log (consolidated canonical log); rig_probe_first.log + rig_probe_uid0.log (uid=0 outputs, first + fresh-snapshot); rig_evaded_uid0.log; rig_rip_control_trap.txt (Fatal trap 9 at mps_user_pass_thru+0x63e: ret, ip=0xffffffff805b121e, frame pointer 0x0); rig_smash_panic.txt (same trap, frame pointer 0x4546474841424344); rig_fixed_runs.log + rig_fixed_disasm_order.txt + rig_env_patched.txt (patched kernel: EINVAL/ENOMEM matrix, check-before-copyin with mov $0xc,%edx); env.txt (symbols/offsets/disassembly ground truth); VERDICT.md pass-3 addendum",
  "kernel_refs": [
    "sys/dev/raid/mps/mps_user.c:802",
    "sys/dev/raid/mps/mps_user.c:806",
    "sys/dev/raid/mps/mps_user.c:747",
    "sys/dev/raid/mps/mps_user.c:830",
    "sys/dev/raid/mps/mps_user.c:878",
    "sys/dev/raid/mps/mps_user.c:188",
    "sys/dev/raid/mps/mps_user.c:2102",
    "sys/dev/raid/mps/mps_user.c:2387",
    "sys/dev/raid/mps/mpsvar.h:332"
  ],
  "poc_changes": "Re-derived the proven DF-1326 (mpr) rig for the mps twin rather than reusing it: (1) new symbol addresses from nm of the shipped kernel (mps_ioctl_devsw @0xffffffff805b16d0 -- mps_user_pass_thru is a real out-of-line call at 0xffffffff805b0be0, not inlined as in mpr); (2) mps-specific softc offsets from disassembly: mps_dev +0x0, mps_flags +0x10, mps_debug +0x14, free-list head +0xd0, facts POINTER +0x148 (key delta: mpss check dereferences sc->facts->IOCRequestFrameSize, U16 @facts+0x24 -- fabricated facts block set to 32 so the post-hoc bound is 128, matching the twin), mps_lock +0x190 (mpr twin: +0x1d8, reqframesz +0x1c); (3) saved-RIP offset re-derived: 5 pushes + sub 0x28 with tmphdr at rbp-0x3c gives RIP at &tmphdr+0x44 (coincidentally identical to mpr); (4) added a byte-check of the vulnerable copyin site (lea -0x3c(%rbp),%rsi; call copyin at +0x7b) plus a kern.df1359rig_vulncheck toggle so fix-validation can dispatch the PATCHED handler explicitly rather than silently; (5) trigger renamed to mps_pass_thru_t / /dev/df1359rig / kern.df1359rig_* sysctls; evaded-mode Function byte 0x20 (anything != MPI2_FUNCTION_SCSI_TASK_MGMT 0x01 takes the mps_alloc_command ENOMEM path).",
  "attempts": 1,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026  root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64",
  "runtime_sec": 1500,
  "guest_dirty": 0,
  "build_cmd": "sh rig_build.sh  (make -f rig.Makefile SYSDIR=/usr/src/sys MACHINE=x86_64 MACHINE_ARCH=x86_64; cc -O -Wall -o trigger df1359rig_trigger.c; kldload ./df1359rig.ko; sysctl -w kern.df1359rig_target=0x$(nm /boot/kernel/kernel | awk / t mps_ioctl_devsw$/{print $1}))",
  "run_cmd": "su operator -c /root/rig1359/trigger probe",
  "code_hash": "sha256:610385d670dc832011b58edf95c302480f306c58a4703bf2254e8835eb97f95a (df1359rig_trigger.c); sha256:6db24070f6aacec39adb98b7d265f9936ff883feec2497266fdad2b695cc8141 (df1359rig.c)",
  "notes": "Twin verification of DF-1326 with the same production-code dispatch technique; escalation runs 6/6 (5 probe incl. fresh snapshot + 1 evaded), plus 2 deterministic trap proofs. Kernel-side dispatch line in dmesg per run: df1359rig: dispatch REAL mps_ioctl_devsw @ 0xffffffff805b16d0 cmd=0xc0384904. Baseline kernel md5 4625046e8061b34e1758d291bc6cad18; patched fix-validation kernel #1 md5 06407b51b4faf526c26e1c25c198d2fd. Full nativekernel fix-build log of this session was in guest /tmp (cleared at reboot); BUILD_RC=0/INSTALL_RC=0 recorded in run.log, and the packs fix_build.log (pass 1) holds the full -Werror build log of the same fix.diff. Guest left clean: vm.sh reset with-src after validation.",
  "recommended_fix": "Bounds-check RequestSize BEFORE the copyin and limit the copy to sizeof(tmphdr); re-read the full request directly from userspace into the sized HW buffer at the bcopy sites (see fix.diff).",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fix.diff applied cleanly (3/3 hunks) to the in-guest /usr/src; single-fix nativekernel X86_64_GENERIC BUILD_RC=0 and installkernel INSTALL_RC=0; patched kernel #1 (md5 06407b51b4faf526c26e1c25c198d2fd) boots with the rig re-primed to the patched mps_ioctl_devsw @0xffffffff805b16f0 (prologue byte-identical, dispatching into PATCHED production code with kern.df1359rig_vulncheck=0). Same unprivileged operator, same trigger: probe/rip/smash return rc=-1 EINVAL (5 runs total), evaded returns rc=-1 ENOMEM (12-byte copyin only, flow proceeds and returns normally); zero Fatal traps in dmesg; guest healthy. objdump proves the new ordering: cmp $0xb,%r8d + facts-deref range check reach the EINVAL block BEFORE the copyin site, which is now mov $0xc,%edx (sizeof(tmphdr) only). The exact behavior that produced uid=0 and two fatal traps on the baseline kernel is GONE on the patched kernel.",
  "fix_evidence": "rig_fixed_runs.log (EINVAL/ENOMEM matrix + uptime + dmesg trap count 0); rig_fixed_disasm_order.txt (patched check-before-copyin disassembly with mov $0xc,%edx copyin); rig_env_patched.txt (patched kernel identity); run.log section 7"
}