β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4280

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4280 Β· 11 files
FileTypeDescriptionSize
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

  • ccdattach stores cs->sc_dev = disk_create(...) β€” the RAW device (_disk_create_named returns dp->d_rawdev, subr_disk.c:763).
  • make_only_dev() ends with reference_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) >= 2 at all times; the guard dev_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).

VERDICT.md
↓ download raw

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)

  1. Configure a unit via raw ioctl (no I/O at all), close the fd, then CCDIOCCLR through a fresh descriptor: CCDIOCCLR: Device busy.
  2. CCDIOCCLR through the SAME still-open descriptor with zero other users: CCDIOCCLR(samefd): Device busy.
  3. Stock ccdconfig -u ccd0: Device busy with fstat showing no holder.
  4. 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_named returns dp->d_rawdev; ccdattach stores it in cs->sc_dev.
  • sys/kern/kern_conf.c:294 β€” make_only_dev() ends with reference_dev(devfs_dev): a PERMANENT creator reference (destroy_only_dev releases twice, kern_conf.c:316-320).
  • Therefore dev_drefs(cs->sc_dev) >= 2 always, and the guard sys/dev/disk/ccd/ccd.c:1434 if (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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-4280

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.