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

Divide-by-zero kernel panic via CCDIOCSET with ccio_ndisks=0

Summary

CCDIOCSET only validates ccio->ccio_ndisks against upper bound (CCD_MAXNDISKS 65536) never rejects 0. With 0 disks ccdinit component loop runs zero times so maxsecsize remains 0 and subsequent pseudo-geometry setup performs integer division by zero - either at :515 (ileave>0: sc_ileave%(maxsecsize/DEV_BSIZE) => x%0) or :580 (1048576/ccg_secsize where ccg_secsize=maxsecsize=0). On x86-64 raises #DE kernel panics. kmalloc(0) returns non-NULL ZERO_LENGTH_PTR so all intervening allocations bail out check pass. Local DoS kernel panic. Attacker: open /dev/ccdN for write issue CCDIOCSET with ccio_ndisks=0. 100% reliable.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2432 Β· 12 files
FileTypeDescriptionSize
poc.c trigger-source CCDIOCSET with ccio_ndisks=0 -> divide-by-zero in ccdinit 2.9 KB view raw
build.sh build-script cc -O2 poc.c 80 B view raw
run.sh run-script kldload ccd && ./poc /dev/ccd0 (as root) 423 B view raw
panic.txt panic-signature Fatal trap 18 integer divide fault at ccdinit+0x578 divl 2.0 KB view raw
run.log run-log fixed-module run: CCDIOCSET rejected EINVAL, guest up 204 B view raw
run.2.log run-log determinism re-run #2: EINVAL, guest up 84 B view raw
run.3.log run-log determinism re-run #3: EINVAL, guest up 84 B view raw
fix.diff suggested-fix reject ndisks==0 at CCDIOCSET + guard maxsecsize==0 in ccdinit 892 B view raw
fix_build.log build-log standalone ccd.ko build, compiles clean 662 B view raw
fix_env.txt environment kern.version, fixed ccd.ko sha256, kldstat 385 B view raw
env.txt environment uname, cc, /dev/ccd0 perms (root:operator 0640), maxx groups 469 B view raw
VERDICT.md verdict full narrative + before/after validation 4.3 KB ↓ raw
VERDICT.md verdict full narrative + before/after validation
↓ download raw

DF-2432 β€” Divide-by-zero kernel panic via CCDIOCSET with ccio_ndisks=0

Verdict: REPRODUCED (local DoS / kernel panic), then FIXED and VALIDATED.

Mechanism (confirmed, path:line)

CCDIOCSET (sys/dev/disk/ccd/ccd.c:1309, sys/sys/ccdvar.h:238) only validates the component count against its upper bound:

if (ccio->ccio_ndisks > CCD_MAXNDISKS) {   /* ccd.c:1319 β€” no lower bound! */
    ccdunlock(cs);
    return (EINVAL);
}

It never rejects ccio_ndisks == 0. With zero disks:

  1. kmalloc(0 * sizeof(char *)) and kmalloc(0 * sizeof(struct vnode *)) return the non-NULL ZERO_LENGTH_PTR, so the allocation checks pass (ccd.c:1350-1353); copyin(..., 0) is a no-op (ccd.c:1355); and the component-lookup loop runs zero times (ccd.c:1372).
  2. ccdinit() then runs: cs->sc_nccdisks = ccd->ccd_ndev = 0 (ccd.c:402); kmalloc(0 * sizeof(struct ccdcinfo)) again yields ZERO_LENGTH_PTR (ccd.c:405); and the per-component loop (ccd.c:418) runs zero times, so maxsecsize stays 0 (ccd.c:416).
  3. The pseudo-geometry setup divides by it: c ccg->ccg_secsize = maxsecsize; /* = 0 */ ccg->ccg_nsectors = 1024 * 1024 / ccg->ccg_secsize;/* 1048576 / 0 -> #DE */ (ccd.c:578-580). If ccio_ileave > 0 the earlier divide sc_ileave % (maxsecsize / DEV_BSIZE) == x % 0 (ccd.c:515) faults first. Either way the CPU raises a divide error (#DE, trap 18) and the kernel panics.

Reproduction (unpatched #0 baseline)

poc.c opens /dev/ccd0 (O_WRONLY, satisfying the FWRITE check at ccd.c:1313) and issues ioctl(fd, CCDIOCSET, &ccio) with ccio_ndisks=0, ccio_ileave=0, ccio_flags=0. Run as root after kldload ccd.

Observed (panic.txt, serial console):

Fatal trap 18: integer divide fault while in kernel mode
instruction pointer = 0x8:0xffffffff82600e28
Stopped at      ccdinit+0x578:  divl    %esi,%eax
db>

Guest went down (ssh died). 100% reliable.

Impact ceiling (realistic)

This is a divide-by-zero DoS, not memory corruption — no escalation chain. The trigger requires: - kldload ccd (root), and - opening /dev/ccd0 with O_WRONLY — /dev/ccd0 is crw-r----- root:operator, so the caller needs root or membership in the operator group. The unprivileged test user (maxx, uid 1001) is not in operator, so it is a root→kernel DoS. (A real deployment that pre-loads ccd and grants an account operator would expose it as an unprivileged local DoS; that is a deployment choice, not the default.)

The fix (fix.diff)

Root-cause: reject a zero-component configuration at the CCDIOCSET layer, and add a defense-in-depth guard against maxsecsize == 0 in ccdinit():

  1. ccd.c:1330 β€” if (ccio->ccio_ndisks == 0 || ccio->ccio_ndisks > CCD_MAXNDISKS) returns EINVAL.
  2. ccd.c:515 β€” if (maxsecsize == 0) { error = EINVAL; goto fail; } before the divides at :519/:580.

Fix validation (rebuilt + reloaded ccd.ko, same #0 kernel base)

ccd is a loadable kld module, so the fix was validated without a kernel reboot: rebuilt ccd.ko standalone from /usr/src/sys/dev/disk/ccd, copied it to /boot/kernel/ccd.ko, and kldload-ed it on the running kernel.

result
before (unpatched ccd.ko, #0 kernel) Fatal trap 18: integer divide fault … Stopped at ccdinit+0x578: divl %esi,%eax β€” guest down
after (fixed ccd.ko, same #0 kernel) poc: CCDIOCSET rejected: Invalid argument (FIXED behavior) β€” guest up, Γ—3/3 runs

Fixed ccd.ko SHA256: 5189556a9a0aee32ac340f408e3a088d84a6cb1110c9c282ef5f43500047595f. The fix closes the bug cleanly (EINVAL replaces the #DE panic).

Files

  • poc.c β€” CCDIOCSET with ccio_ndisks=0 β†’ divide-by-zero.
  • build.sh / run.sh β€” cc the PoC; kldload ccd && ./poc /dev/ccd0.
  • panic.txt β€” Fatal trap 18 … ccdinit+0x578 serial-log signature.
  • run.log β€” fixed-module run: CCDIOCSET rejected: Invalid argument, guest up.
  • run.2.log, run.3.log β€” determinism re-runs (Γ—3/3 EINVAL).
  • fix.diff β€” git apply-able: reject ndisks==0 + guard maxsecsize==0.
  • fix_build.log β€” standalone ccd.ko build log (compiles clean).
  • fix_env.txt β€” kern.version, fixed ccd.ko sha256, kldstat.
  • env.txt β€” guest uname, cc, /dev/ccd0 perms, maxx groups.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. SAME PoC (CCDIOCSET ccio_ndisks=0) on SAME #0 kernel base panics with unpatched ccd.ko (Fatal trap 18 integer divide fault at ccdinit+0x578 divl, guest down) and does NOT panic with fixed ccd.ko (ioctl returns EINVAL: 'CCDIOCSET rejected: Invalid argument (FIXED behavior)', guest up, deterministic x3/3 runs). Fix closes divide-by-zero cleanly. fix.diff applies (Hunk #1 at 511, Hunk #2 at 1321) and module compiles standalone.

BEFORE (unpatched): 'Fatal trap 18: integer divide fault while in kernel mode' / 'Stopped at ccdinit+0x578: divl %esi,%eax' (guest down). AFTER (fixed ccd.ko, same #0 kernel): 'poc: CCDIOCSET rejected: Invalid argument (FIXED behavior)' POC_EXIT=2 GUEST_UP=6.5-DEVELOPMENT, repeated run.log/run.2.log/run.3.log (x3/3 EINVAL, guest up). Fixed ccd.ko sha256=5189556a9a0aee32ac340f408e3a088d84a6cb1110c9c282ef5f43500047595f.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; fix lives in ccd.ko loadable module, rebuilt standalone from /usr/src/sys/dev/disk/ccd and reloaded via kldload -- no kernel reboot. Fixed ccd.ko SHA256 = 5189556a9a0aee32ac340f408e3a088d84a6cb1110c9c282ef5f43500047595f)

Confirmed kernel references

Detail

Exploit chain

none -- non-corruption (divide-by-zero / #DE local DoS), no memory-corruption primitive and no escalation chain. Realistic impact ceiling: local root->kernel panic (DoS). Trigger requires root: kldload ccd (root) and open /dev/ccd0 O_WRONLY for FWRITE check ccd.c:1313. /dev/ccd0 crw-r----- root:operator, maxx (uid 1001) NOT in operator, so root->kernel DoS by default. Deployment that pre-loads ccd AND grants account operator membership would expose as unprivileged local DoS, but that's deployment choice not default config.

Evidence (decisive lines)

BEFORE (unpatched ccd.ko, #0 kernel), serial boot.log: Fatal trap 18: integer divide fault while in kernel mode / instruction pointer = 0x8:0xffffffff82600e28 / Stopped at ccdinit+0x578: divl %esi,%eax / db> (guest down). AFTER (fixed ccd.ko, same #0 kernel), run.log: poc: issuing CCDIOCSET on /dev/ccd0 with ccio_ndisks=0 ... / poc: CCDIOCSET rejected: Invalid argument (FIXED behavior) / POC_EXIT=2 ; GUEST_UP=6.5-DEVELOPMENT (deterministic x3/3 runs).

PoC changes

Authored findings/poc/DF-2432/ from scratch (no scaffolding): poc.c opens /dev/ccd0 O_WRONLY and issues ioctl(fd, CCDIOCSET, &ccio) with self-contained struct ccd_ioctl mirror (32 bytes amd64) and MY_CCDIOCSET = _IOWR('F',16,struct ccd_ioctl), setting ccio_ndisks=0/ccio_ileave=0/ccio_flags=0 to take ccd.c:580 divide path; prints clear FIXED-behavior marker (EINVAL) vs expected panic. build.sh; run.sh (kldload ccd && ./poc).

Verified recommended fix

Two-part fix in sys/dev/disk/ccd/ccd.c (full git-apply-able diff in findings/poc/DF-2432/fix.diff): (1) root-cause -- in ccdioctl(CCDIOCSET), change bound check to 'if (ccio->ccio_ndisks == 0 || ccio->ccio_ndisks > CCD_MAXNDISKS) return EINVAL;' so zero-component config rejected before any allocation/divide; (2) defense-in-depth -- in ccdinit(), add 'if (maxsecsize == 0) { error = EINVAL; goto fail; }' immediately before divides at ccd.c:515/580 so future caller cannot re-introduce #DE. Matches finding proposal intent.

Verdict

REPRODUCED then FIXED+VALIDATED. CCDIOCSET (sys/dev/disk/ccd/ccd.c:1309) validates ccio->ccio_ndisks only against UPPER bound (CCD_MAXNDISKS, ccd.c:1319) and never rejects 0. With ccio_ndisks=0: kmalloc(0) returns non-NULL ZERO_LENGTH_PTR so alloc checks at ccd.c:1350 and ccd.c:405 pass, copyin of 0 bytes is no-op (ccd.c:1355), component loops at ccd.c:1372 and ccd.c:418 run zero times, leaving maxsecsize=0 (ccd.c:416). ccdinit's pseudo-geometry setup then divides by it: ccg->ccg_secsize=maxsecsize=0 (ccd.c:578); ccg->ccg_nsectors = 1024*1024 / ccg->ccg_secsize == 1048576/0 (ccd.c:580) -> #DE trap 18 -> kernel panic. (If ccio_ileave>0, earlier divide sc_ileave%(maxsecsize/DEV_BSIZE)==x%0 at ccd.c:515 faults first.) Confirmed on unpatched #0: 'Fatal trap 18: integer divide fault while in kernel mode' / 'Stopped at ccdinit+0x578: divl %esi,%eax', guest down. 100% reliable. Fix (reject ndisks==0 at CCDIOCSET layer + defense-in-depth maxsecsize==0 guard in ccdinit) rebuilt as standalone ccd.ko, reloaded on running kernel (ccd loadable, no reboot), re-run x3: 'CCDIOCSET rejected: Invalid argument (FIXED behavior)', guest stays up.