CCDIOCCLR busy-guard is unsatisfiable (permanent make_only_dev reference): a configured ccd unit can never be unconfigured and ccd.ko can never unload
Summary
cs->sc_dev is the RAW device and make_only_dev takes a permanent creator reference on it, so dev_drefs >= 2 with ZERO open descriptors - the busy guard is always true: CCDIOCCLR always fails, CCDF_INITED stays set for the boot, every component vnode stays exclusively held, the itable stays allocated, and kldunload ccd is denied forever. Demonstrated live (same-fd CLR, separate-fd CLR, stock ccdconfig -u, kldunload - all EBUSY with fstat showing no holders); fixed with disk_getopencount and re-validated. disk_getopencount fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4280 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | β | 2.1 KB | view raw | |
| run.sh | β | 104 B | view raw | |
| build.sh | β | 99 B | view raw | |
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 2.6 KB | β raw | |
| run.log | β | 1.3 KB | view raw | |
| fix_patched.log | β | 681 B | view raw | |
| fix.diff | β | 583 B | view raw | |
| env.txt | β | 284 B | view raw | |
| verdict.json | β | 3.2 KB | view raw | |
| manifest.json | β | 783 B | view raw |
DF-4280 β ccd(4) CCDIOCCLR busy-guard can never be satisfied (unit permanently stuck)
Build
cc -O -o /tmp/df4280 /root/df4280/trigger.c # on the guest, as root
Run (guest, root; ccd module + a CCD-labelled component required)
kldload ccd vnconfig -c -s labels vn0 /root/comp0.img # 8MB file-backed vnode disk disklabel32 -R -r vn0 labelfile # with partition 'd: fstype ccd' /tmp/df4280 /dev/ccd0 /dev/vn0s0d # SET then same-fd CLR ccdconfig -u ccd0 # stock tool β also EBUSY kldunload ccd # "In use, cannot detach"
Expected (vulnerable kernel)
[+] CCDIOCSET ok size=16352 [-] CCDIOCCLR(same fd, no other users): Device busy [-] BUG: unit can never be unconfigured this boot ccdconfig: ioctl (CCDIOCCLR): /dev/ccd0: Device busy ccd0: In use, cannot detach
Expected (patched kernel β fix.diff hunk 2: disk_getopencount guard)
[+] CCDIOCSET ok size=16352 [+] CCDIOCCLR ok
Root cause
ccdattachstorescs->sc_dev = disk_create(...)β the RAW device (_disk_create_namedreturnsdp->d_rawdev, subr_disk.c:763).make_only_dev()ends withreference_dev(devfs_dev)(kern_conf.c:294) β a permanent creator reference (destroy_only_dev releases TWICE, kern_conf.c:316-320, confirming the +1 convention).- Therefore
dev_drefs(cs->sc_dev) >= 2at all times; the guarddev_drefs(cs->sc_dev) > 1(ccd.c:1434) is unconditionally true.
Impact
Once configured, a ccd unit is stuck for the boot: components are held exclusively (cannot be reused/relabelled via other ccds), sc_cinfo / sc_itable stay allocated, CCDF_INITED blocks reconfiguration, and the ccd module can never be unloaded. Triggering requires write access to /dev/ccdN (root, or the operator group β nodes are 0640 root:operator).
DF-4280 VERDICT
Status: REPRODUCED (functional lockout / local DoS). Fixed by fix.diff hunk 2 and re-verified.
What was observed (baseline, vulnerable kernel β run.log)
- Configure a unit via raw ioctl (no I/O at all), close the fd, then
CCDIOCCLR through a fresh descriptor:
CCDIOCCLR: Device busy. - CCDIOCCLR through the SAME still-open descriptor with zero other
users:
CCDIOCCLR(samefd): Device busy. - Stock
ccdconfig -u ccd0:Device busywithfstatshowing no holder. kldunload ccd:ccd0: In use, cannot detachβ forever.
Once any unit is configured it can never be unconfigured during the
boot: CCDF_INITED stays set, every component vnode stays exclusively
held (ccd.c:1526 v_opencount > 1 blocks reuse), sc_cinfo/sc_itable/
ii_index stay allocated, the module cannot unload.
Root cause (path:line)
- sys/kern/subr_disk.c:763 β
_disk_create_namedreturnsdp->d_rawdev; ccdattach stores it incs->sc_dev. - sys/kern/kern_conf.c:294 β
make_only_dev()ends withreference_dev(devfs_dev): a PERMANENT creator reference (destroy_only_dev releases twice, kern_conf.c:316-320). - Therefore
dev_drefs(cs->sc_dev) >= 2always, and the guard sys/dev/disk/ccd/ccd.c:1434if (dev_drefs(cs->sc_dev) > 1)is unconditionally true β CCDIOCCLR can never pass it.
Fix
fix.diff hunk 2 replaces the guard with
disk_getopencount(&cs->sc_disk) > 1 β d_opencount counts real opens
of every node (whole/slice/partition) of the disk (subr_disk.c:1121/
1150), which is exactly the semantic the guard wanted; in-flight raw
I/O remains covered because the caller's descriptor stays open for the
duration of physread/physwrite.
Fix validation (patched ccd.ko, same guest, fix_patched.log)
[+] CCDIOCSET ok size=16352 [+] CCDIOCCLR ok <- was EBUSY ccdconfig -u ccd0 -> ok <- was EBUSY kldunload ccd -> ok <- was "In use, cannot detach"
No regression: configuration and I/O still work (DF-4279 control).
Impact
Not memory corruption. Local denial of service against the ccd subsystem (units wedge until reboot, module unload denied) by any user able to open /dev/ccdN for write (root or operator; the nodes are 0640 root:operator). Normal operational pain: ccd reconfiguration requires a reboot on affected kernels.
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 2 replaces dev_drefs(cs->sc_dev) > 1 with disk_getopencount(&cs->sc_disk) > 1. Validated on the patched module: CCDIOCCLR succeeds through the same descriptor, stock ccdconfig -u succeeds, kldunload ccd succeeds; configuration and I/O unregressed (same build as DF-4279 validation).
findings/poc/DF-4280/fix_patched.log and run.log (baseline EBUSY vs patched success)
Confirmed kernel references
Detail
Exploit chain
root/operator configures any ccd unit (normal use of the driver) -> unit and its components are wedged until reboot; kldunload ccd denied. Local DoS of the ccd subsystem / permanent resource hold. No memory-corruption chain.
Evidence (decisive lines)
run.log (baseline: same-fd CLR EBUSY, separate-fd CLR EBUSY, stock ccdconfig -u EBUSY, kldunload 'In use, cannot detach', fstat empty); fix_patched.log (patched: CCDIOCCLR ok, ccdconfig -u ok, kldunload ok); VERDICT.md root-cause trail (subr_disk.c:763 -> kern_conf.c:294 -> ccd.c:1434)
PoC changes
Authored from scratch during verification (pass-2 discovery); matrix probes A/D from the same session captured in run.log.
Verified recommended fix
Use disk_getopencount(&cs->sc_disk) > 1 (real open count across whole/slice/partition nodes) instead of dev_drefs() on the raw device, whose count includes the permanent make_only_dev reference.
Verdict
REPRODUCED: the CCDIOCCLR busy-guard dev_drefs(cs->sc_dev) > 1 (ccd.c:1434) is unsatisfiable because cs->sc_dev is the raw device created by make_only_dev(), which takes a permanent creator reference (kern_conf.c:294 reference_dev; destroy_only_dev double-releases at kern_conf.c:316-320). dev_drefs >= 2 even with zero open descriptors, so a configured ccd unit can NEVER be unconfigured: CCDIOCCLR always returns EBUSY (proven with the ioctl's own descriptor as the only holder, and via stock ccdconfig -u with fstat showing no holders), CCDF_INITED stays set, component vnodes stay exclusively held, and kldunload ccd is denied forever. With fix.diff hunk 2 (disk_getopencount(&cs->sc_disk) > 1, the real per-disk open counter) CCDIOCCLR succeeds, ccdconfig -u succeeds, and the module unloads cleanly.
No comments yet.