Arbitrary kernel read/write from the CUSE server: proc_rwmem_dfly uses the server-controlled peer_ptr as the raw kernel side of copyin()/copyout()
Summary
cuse_proc2proc_copy/proc_rwmem_dfly implement the server-to-client cross-process copy by passing the peer address - pchk->peer_ptr, a fully server-controlled 64-bit value with no length cap - as the RAW kernel-side pointer of copyin()/copyout(). DragonFly's std_copyin/out validate only the user-side argument; the kernel-side pointer is used unchecked. WRITE_DATA yields arbitrary kernel write; READ_DATA arbitrary kernel read. Upstream FreeBSD uses proc_vm_rwmem() which walks the target's vm map - the DFly shim lost every safety property; additionally all legitimate transfers missing the <=4KB fast path are functionally broken. Attacker model: a compromised de-privileged cuse daemon (webcamd class - exactly what the caps design intends to sandbox) gets full kernel R/W. LIVE-REPRODUCED 4/4 incl. fresh boots: kernel kread/kwrite via READ_DATA/WRITE_DATA to kernel 'version', then full uid0 chain (sysctl kp_paddr -> kread p_ucred -> zero uid/gid fields -> nobody child becomes uid=0 reading /etc/spwd.db). Fix validated on rebuilt cuse.ko: both directions EFAULT, uid0 chain gone, legit >4KB copies repaired. vm_fault_page-walk fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4147 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| cuse_krw.c | β | 8.7 KB | view raw | |
| wprobe.c | β | 2.0 KB | view raw | |
| fixtest.c | β | 4.4 KB | view raw | |
| build.sh | β | 102 B | view raw | |
| run.sh | β | 109 B | view raw | |
| build.log | β | 5 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| run.2.log | β | 1020 B | view raw | |
| run.3.log | β | 1020 B | view raw | |
| env.txt | β | 466 B | view raw | |
| fix.diff | β | 2.8 KB | view raw | |
| fix_build.log | β | 75.9 KB | view raw | |
| fix_run.log | β | 346 B | view raw | |
| fix_run.2.log | β | 636 B | view raw | |
| fix_run_wprobe.log | β | 101 B | view raw | |
| fix_run_fixtest.log | β | 393 B | view raw | |
| fixtest_baseline.log | β | 390 B | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-4147 β cuse proc_rwmem_dfly: arbitrary kernel read/write from the CUSE server
What
sys/dev/misc/cuse/cuse.c:881-893 (proc_rwmem_dfly) implements the
serverβclient cross-process data copy for CUSE_IOCTL_READ_DATA /
CUSE_IOCTL_WRITE_DATA (dispatch at cuse.c:1352-1380, fallback at
cuse.c:949-1030). The peer address (pchk->peer_ptr, a fully
server-controlled 64-bit value) is passed as the raw kernel-side pointer
of copyin()/copyout():
if (uio->uio_rw == UIO_WRITE)
error = copyin(iov->iov_base, (void *)uio->uio_offset, iov->iov_len);
else
error = copyout((void *)uio->uio_offset, iov->iov_base, iov->iov_len);
DragonFly's std_copyin/std_copyout
(sys/platform/pc64/x86_64/support.s:228-328) validate only the
user-side argument (β€ VM_MAX_USER_ADDRESS); the kernel-side pointer is
used raw. Upstream FreeBSD's cuse.c uses proc_vm_rwmem() which walks the
target process's vm map; the DragonFly port replaced it with this shim and
lost every safety property:
CUSE_IOCTL_WRITE_DATAwithpeer_ptrβ₯CUSE_BUF_MAX_PTR(0x20000) and outside the client's bounce-buffer window takescuse_server_data_copy_lockedβcuse_proc2proc_copy(curproc, local_ptr, client_proc, peer_ptr, len)βcopyin(local_ptr /*validated user*/, peer_ptr /*RAW KERNEL DST*/, len)= arbitrary kernel write with server-controlled data and length (no length cap on this path).CUSE_IOCTL_READ_DATAanalogously lands incopyout(peer_ptr /*RAW KERNEL SRC*/, local_ptr /*validated user*/, len)= arbitrary kernel read into the server's userspace.
Preconditions: an open /dev/cuse server fd (mode 0600 β root) plus one
pending client command (any user opening a created /dev/<name> node and
blocking in open/read/write/ioctl). CUSE_IOCTL_CREATE_DEV requires
SYSCAP_NODRIVER β the intended deployment is exactly a caps-restricted
daemon (webcamd/uhidd style), so this is a sandbox/caps-jail escape to
full kernel compromise, not an unprivileged-local primitive.
Functional half of the bug: for a legitimate server passing the client's
real user VA, the copy touches the SERVER's own address space at that VA β
normally unmapped β EFAULT, silently corrupting the server otherwise.
All cuse data transfers that miss the β€4 KiB bounce-buffer fast path are
broken (fixtest_baseline.log proves it: WRITE_DATA returned 14 (Bad
address) and the client's buffer stays empty).
Reproduced (4/4 runs, incl. fresh vm.sh reset with-src boots)
- kernel READ of
versionat 0xffffffff81144960 returns the kernel version string (run.log) - kernel WRITE of the same symbol sticks, readback
CUSE-PWNED ...(run.log), self-restored afterwards - full chain to uid=0 for an unprivileged process: sysctl
kern.proc.pid.<child>βkp_paddr(struct proc) βproc+0x10(p_ucred) β zero all uid/gid fields that equal 65534 β child printsgetuid=0 geteuid=0, execs a shell βuid=0(root) gid=0(wheel), reads/etc/spwd.db
Fix validation
fix.diff replaces proc_rwmem_dfly with a procfs_rwmem()-style
page-by-page walk (vm_fault_page + pmap_kenter_quick + uiomove) of the
target process's map and rejects peer addresses β₯ VM_MAX_USER_ADDRESS.
Rebuilt cuse.ko only (incremental kernel-module build, fix_build.log),
installed, rebooted:
fix_run.log/fix_run.2.log: kernel READ β EFAULT, no uid0, PoC RC=1fix_run_wprobe.log: kernel WRITE β EFAULT (rc=-1 errno=14)fix_run_fixtest.log: positive control β a legitimate >4 KiB serverβclient transfer now WORKS (cross-process fallback copy WORKS), which the unpatched kernel could not do (fixtest_baseline.log)
Files
cuse_krw.cβ main PoC (read/write/uid0 chain)wprobe.cβ write-side one-shot probefixtest.cβ positive functional control of the fallback copybuild.sh,run.shβ exact commandsrun.log,run.2.log,run.3.logβ baseline reproductionsfix_build.log,fix_run*.log,fixtest_baseline.logβ fix validationenv.txtβ guest state
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff replaces proc_rwmem_dfly with a procfs_rwmem()-style page walk of the TARGET process's vm map (vm_fault_page + pmap_kenter_quick + uiomove) and rejects peer addresses outside the user range. Incremental cuse.ko rebuild (fix_build.log), installed to /boot/kernel, rebooted: CUSE_IOCTL_READ_DATA and WRITE_DATA against a kernel VA both return EFAULT (fix_run.2.log, fix_run_wprobe.log), the uid0 chain is gone (PoC RC=1, child released with ENXIO), and the positive control shows the legitimate server->client >4KiB fallback copy now works (fix_run_fixtest.log) whereas it EFAULTed on the stock kernel (fixtest_baseline.log).
fix_run.2.log: '[-] kernel read failed/invalid (error=-1 errno=14)' + PATCHED_POC_RC=1; fix_run_wprobe.log: 'WRITE_DATA ... rc=-1 errno=14 (Bad address)'; fix_run_fixtest.log: '[+] cross-process fallback copy WORKS'; fix_build.log: clean -Werror module rebuild.
Confirmed kernel references
Detail
Exploit chain
root/capped daemon opens /dev/cuse; CREATE_DEV makes a world-accessible /dev node; an unprivileged process opens it and blocks in open(); server GET_COMMAND takes the pending CUSE_CMD_OPEN; CUSE_IOCTL_READ_DATA{peer_ptr=KERNEL_VA} copies kernel memory to the server (arbitrary read); CUSE_IOCTL_WRITE_DATA{peer_ptr=KERNEL_VA} writes server data to kernel memory (arbitrary write, no length cap); to escalate any process to root: sysctl kern.proc.pid.
Evidence (decisive lines)
run.log/2/3: '[+] ARBITRARY KERNEL READ confirmed', '[+] ARBITRARY KERNEL WRITE confirmed (readback CUSE-PWNED...)', '[+] *** CHILD IS uid 0 - privilege escalation complete ***' + 'uid=0(root) gid=0(wheel)' + 'can read shadow db'; fix_run.2.log + fix_run_wprobe.log: EFAULT both directions on patched kernel; fixtest_baseline.log vs fix_run_fixtest.log: legit >4KB copy broken before, works after.
PoC changes
PoC authored from scratch (raw ioctl protocol per cuse_ioctl.h inlined; no libcuse on guest). Kernel VA of `version' taken from nm /boot/kernel/kernel (0xffffffff81144960, no KASLR). uid/gid fields located robustly by pattern-matching 65534 in a 256-byte ucred dump instead of trusting layout offsets. Child bails out of its open-retry loop on non-ENOENT errors so the patched-kernel run exits cleanly.
Verified recommended fix
Replace proc_rwmem_dfly's raw copyin/copyout with a vm_fault_page-based walk of the target process's map and reject peer addresses >= VM_MAX_USER_ADDRESS (see fix.diff).
Verdict
cuse_proc2proc_copy/proc_rwmem_dfly (cuse.c:881-893) passes the server-controlled peer_ptr of CUSE_IOCTL_{READ,WRITE}_DATA as the raw kernel-side pointer of copyin()/copyout(); DFly's std_copyin/std_copyout validate only the user side (support.s:228-328), so any process holding /dev/cuse open with one pending client command gets arbitrary kernel read AND write. Demonstrated 4/4 runs on DragonFly 6.5-DEVELOPMENT: read of kernel `version' symbol, write to the same (readback verified), and a full chain that turned a setuid(nobody) child process into uid=0 by walking kern.proc kp_paddr -> struct proc+0x10 p_ucred and zeroing its uid/gid fields. Threat model: /dev/cuse is 0600 root and CREATE_DEV needs SYSCAP_NODRIVER, so the attacker is a (possibly caps-jailed) userspace driver daemon - this is a sandbox escape to full kernel compromise; it is not reachable by an unprivileged user without such a daemon. The bug also functionally breaks all legitimate >4KiB cuse transfers (they EFAULT or land in the server's own address space). fix.diff (procfs_rwmem-style vm_fault_page walk + VM_MAX_USER_ADDRESS rejection of the peer address) was built into cuse.ko and validated: kernel read AND write now return EFAULT, and the legitimate cross-process fallback copy works for the first time.
No comments yet.