CCDIOCSET accepts a negative interleave: negative component transfer size drives an unbounded dispatch loop and a guaranteed kernel panic
Summary
ccio_ileave is a signed int copied into the kernel with no lower-bound validation; ccdconfig(8) rejects ileave<0 but the ioctl does not, and /dev/ccdN is 0640 root:operator. Every later guard tests only >0/==0, so the negative value reaches ccdbuffer: cbc = dbtob(ileave - cboff) is negative, b_bcount goes negative, the start loop grows bcount by |rcount| per iteration and walks b_data backwards out of the user buffer while dispatching unbounded negative-length bios - the first 512-byte read wedges the thread un-killably and the kernel panics (vm_object_hold_shared NULL assertion). REPRODUCED 2/2 with identical signature on the QEMU guest via vn(4) components; fix validated (EINVAL, no panic, control path unregressed). The write variant sprays component content at descending user addresses before the panic. Reject-negative fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4279 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | β | 2.3 KB | view raw | |
| run.sh | β | 103 B | view raw | |
| build.sh | β | 98 B | view raw | |
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| panic.txt | β | 2.6 KB | view raw | |
| run.2.log | β | 2.0 KB | view raw | |
| fix_patched.log | β | 681 B | view raw | |
| build.log | β | 951 B | view raw | |
| env.txt | β | 284 B | view raw | |
| fix.diff | β | 583 B | view raw | |
| verdict.json | β | 3.8 KB | view raw | |
| manifest.json | β | 930 B | view raw |
DF-4279 β ccd(4): negative interleave β negative component transfer size β kernel panic
Build
On the DragonFly guest (as root):
cc -O -o /tmp/df4279 /root/df4279/trigger.c
Run (see run.sh for the full self-contained sequence)
kldload ccd vnconfig -c -s labels vn0 /root/comp0.img # 8MB vnode disk disklabel32 -R -r vn0 <labelfile> # partition d: fstype ccd (19) /tmp/df4279 /dev/ccd0 /dev/vn0s0d 32 # control: config+read OK /tmp/df4279 /dev/ccd1 /dev/vn1s0d -1 # trigger: negative interleave
Expected (vulnerable kernel)
Control prints CCDIOCSET ok ... ileave=32 + pread returned 512 bytes.
Trigger prints CCDIOCSET ok ... ileave=-1 (kernel ACCEPTS the negative
interleave), the first 512-byte pread never returns, and the kernel
panics within seconds:
panic: assertion "obj != NULL" failed in vm_object_hold_shared at /usr/src/sys/vm/vm_object.c:330
cpuid = 2
vm_object_hold_shared() at vm_object_hold_shared+0x3f
vm_fault() at vm_fault+0x408
trap_pfault() at trap_pfault+0x9a
trap() at trap+0x17c
calltrap() at calltrap+0x9
--- trap 000000000000000c, rip = ... memmove() at memmove+0x10a
Debugger("panic")
Reproduced twice with identical signature (panic.txt, run.2.log).
Expected (patched β fix.diff hunk 1)
CCDIOCSET: Invalid argument
No panic, guest stays up, control path (ileave=32) unregressed (fix_patched.log).
Why it happens
ccio_ileaveisint; ccd.c:1326 stores it with no lower-bound check (ccdconfig(8) rejects ileave<0 at ccdconfig.c:254, but the ioctl is directly reachable; /dev/ccdN is 0640 root:operator).- All later guards test
sc_ileave > 0/== 0only, so the negative value flows intoccdbuffer(): - ccd.c:975-976
cboff = bn % ileave(β€0),cbn = bn / ileave - ccd.c:1068
cbc = dbtob((off_t)(sc_ileave - cboff))β negative - ccd.c:1071
b_bcount = (cbc < bcount) ? cbc : bcountβ negative - ccd.c:886-888
bcount -= rcountwithrcount = cbp[0]->cb_buf.b_bcountnegative βbcountgrows every iteration β the dispatch loop never terminates, andaddr += rcountwalksb_databackwards out of the user buffer; the resulting OOB transfer faults in memmove β panic.
DF-4279 VERDICT
Status: REPRODUCED (panic). Fixed by fix.diff hunk 1 and re-verified.
Environment
DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC), QEMU/KVM guest, ccd.ko module, vn(4) components labeled fstype=ccd (19). Trigger executed as root (also reachable by the operator group: /dev/ccdN is 0640 root:operator via make_only_dev defaults, subr_disk.c:699-701).
How it reproduces
kldload ccd; create two 8MB vn disks; disklabel32 each withd: 16368 16 ccd(fstype 19 = FS_CCD β required by ccd.c:458).- Positive control: raw
ioctl(CCDIOCSET)ileave=32 β config ok ("total blocks 16352" = 16368-16 skip), 512-byte pread returns. - Trigger: same ioctl with
ccio_ileave = -1β accepted; the first pread never returns and the kernel panics within seconds.
Observed (two runs, identical signature β panic.txt / run.2.log)
ccd0-3: Concatenated disk drivers ccd0: max component iosize is 65536 total blocks 16352 <- control ok ccd0: max component iosize is 65536 total blocks 16352 <- ileave=-1 ACCEPTED panic: assertion "obj != NULL" failed in vm_object_hold_shared vm_object.c:330 vm_object_hold_shared β vm_fault β trap_pfault β trap β calltrap --- trap 0xc, rip = memmove+0x10a ---
(Note: every config line says "ccd0" because ccd.c:507 prints cs->sc_unit before ccdinit sets it at ccd.c:593 β cosmetic, noted in the audit.)
Root cause chain (path:line)
- sys/dev/disk/ccd/ccd.c:1326 β
ccd.ccd_interleave = ccio->ccio_ileavewith no<= 0validation in the kernel (userland ccdconfig.c:254 rejects it, the ioctl does not). - sys/dev/disk/ccd/ccd.c:484,514 β only
> 1/> 0guards; negative values reach ccdinterleave/ccdbuffer. - sys/dev/disk/ccd/ccd.c:1068-1071 β
cbc = dbtob(ileave - cboff)is negative for ileave<0 βcb_buf.b_bcountnegative. - sys/dev/disk/ccd/ccd.c:886-888 β
rcountnegative βbcountgrows β unbounded dispatch loop;addr += rcountwalks b_data backwards (OOB relative to the user buffer) β OOB transfer β fault in memmove β vm_fault on a destroyed/absent object β panic.
Interleave-table arithmetic itself stays in bounds for negative ileave (the u64 two's-complement walk in ccd.c:981-985 is self-consistent); the kill shot is the negative transfer size, not the table walk.
Exploit chain
None to uid0. Primitive: privileged local user (root/operator) gets an un-killable kernel thread issuing unbounded negative-length device transfers whose data pointer walks backwards through user VA space, terminating in a guaranteed kernel panic. Class = local DoS/panic. The write variant sprays component-disk content at decreasing user addresses of the caller's own process before the panic.
Fix validation (same run, same guest)
- fix.diff hunk 1 rejects
ccio_ileave < 0with EINVAL. - Patched ccd.ko rebuilt from guest /usr/src (build.log), installed:
- control ileave=32: config + read OK (no regression);
- trigger ileave=-1:
CCDIOCSET: Invalid argument, no panic, guest stays up (fix_patched.log). - Baseline (unpatched): panic Γ2. Patched: 0/2.
fix.diff hunk 2 (DF-4280) was validated in the same build.
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 hunk 1 (reject ccio_ileave<0 in CCDIOCSET) validated on the guest: ccd.c rebuilt as a module from patched /usr/src, installed, ileave=-1 now returns EINVAL and the previously-guaranteed panic is gone across repeated runs while the ileave=32 control path is unregressed. Hunk 2 (DF-4280 guard) validated in the same build.
findings/poc/DF-4279/fix_patched.log (patched: 'CCDIOCSET: Invalid argument' + guest survives + uptime shown; baseline: panic.txt/run.2.log); build.log (nativekernel RC=0 + module link)
Confirmed kernel references
Detail
Exploit chain
root/operator user -> open /dev/ccdN (0640 root:operator) -> ioctl(CCDIOCSET, ileave=-1, CCD-typed component) -> pread 512 bytes -> negative b_bcount + backward b_data walk in ccdstart -> OOB transfer -> kernel panic (un-killable spin precedes the crash). No uid0 chain; class is privileged local DoS/panic.
Evidence (decisive lines)
panic.txt (full serial-console panic: 'assertion obj != NULL failed in vm_object_hold_shared', memmove fault); run.2.log (serial sequence: control config ok, ileave=-1 config accepted, immediate panic); README.md/run.sh (self-contained reproduce); fix_patched.log (patched module: ileave=-1 -> 'CCDIOCSET: Invalid argument', ileave=32 control still works, guest survives); build.log (kernel+module rebuild, RC=0)
PoC changes
Seed sketch was author-authored here: raw CCDIOCSET trigger with _KERNEL_STRUCTURES include fix (ccdvar.h is not userland-includable otherwise), vn(4)+disklabel32 fstype=ccd component setup (no mdconfig on guest), positive control included.
Verified recommended fix
Validate ccio_ileave >= 0 in CCDIOCSET (return EINVAL) before configuring; optionally make the ileave guards in ccdinit use unsigned math.
Verdict
REPRODUCED: CCDIOCSET accepts a negative interleave (signed int, no kernel-side lower-bound validation at ccd.c:1326; ccdconfig(8) rejects it but the raw ioctl does not). The negative ileave reaches ccdbuffer() where cbc=dbtob(ileave-cboff) is negative (ccd.c:1068), producing a negative cb_buf.b_bcount (ccd.c:1071); ccdstart() then grows bcount by |rcount| each iteration (ccd.c:886-888) and walks b_data backwards out of the user buffer, issuing unbounded negative-length device transfers that fault in memmove -> vm_fault NULL-object assertion -> kernel panic. Two identical panics on the stock kernel; with fix.diff hunk 1 (reject ileave<0) the ioctl returns EINVAL, no panic, control path unregressed.
No comments yet.