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

FD_STYPE accepts unvalidated fd_type enabling divide-by-zero panics and shift UB

Summary

FD_STYPE ioctl stores fully attacker-supplied struct fd_type into fd->ft with no field validation. Type later used as divisor in geometry math (sectrac*heads) and left-shift count (128<<secsize). Root can plant sectrac=0/heads=0 or secsize>=25 causing kernel divide-by-zero trap or signed-shift UB on next open or I/O. Fdopen info.d_secpercyl=ft->sectrac*ft->heads then size/sectrac*heads div-by-zero. fdstate blknum/(sectrac*heads) and sec/sectrac same. 128<<secsize secsize>=24 signed-int overflow UB secsize>=32 shifts>=width. Crucially opening /dev/fd0 yields dkpart==WHOLE_SLICE_PART so Fdopen takes changetype=0 branch does NOT overwrite fd->ft so poisoned value consumed verbatim. Root can arm trap that unprivileged operator-group user trips on open.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2455 Β· 8 files
FileTypeDescriptionSize
fd_stype.c trigger-source FD_STYPE ioctl with sectrac=0 (arms div-by-zero) β€” NOT runnable here, no /dev/fd0 2.2 KB view raw
build.sh build-script cc -o fd_stype fd_stype.c 209 B view raw
run.sh run-script demonstrates the HW/config gate (open fails ENOENT) 992 B view raw
run.log run-log trigger run: /dev/fd0 absent, ENOENT, no div-by-zero 254 B view raw
env.txt environment uname, FDC probe (0 hits), config check 286 B view raw
fix_build.log build-log nativekernel build with device fd: NK_DONE rc=0 113 B view raw
fix.diff suggested-fix validate sectrac/heads/secsize in FD_STYPE (defense-in-depth) 1.1 KB view raw
VERDICT.md verdict HW+config gate analysis, code trace, defense-in-depth fix 3.5 KB ↓ raw
VERDICT.md verdict HW+config gate analysis, code trace, defense-in-depth fix
↓ download raw

DF-2455 β€” FD_STYPE accepts unvalidated fd_type β†’ kernel divide-by-zero / shift-UB (defense-in-depth fix)

Verdict

NOT REPRODUCED on this audit guest β€” valid hard blocker: the floppy path is HW-gated (no ISA FDC) AND config-gated (no device fd in X86_64_GENERIC), so /dev/fd0 does not exist and the fd driver is not even compiled in. The bug is real in source (confirmed by code trace) and the fix.diff is a defense-in-depth guard that compiles cleanly in-tree (verified by a full nativekernel build with device fd added).

Mechanism (code trace, sys/dev/disk/fd/fd.c)

The FD_STYPE ioctl stores a fully caller-supplied struct fd_type into fd->ft with no field validation (fd.c:2325-2330):

case FD_STYPE:                  /* set drive type */
    if (caps_priv_check(ap->a_cred, SYSCAP_RESTRICTEDROOT))
        return EPERM;            /* <-- root-only */
    fd->ft = *(struct fd_type *)ap->a_data;   /* no bounds check */
    break;

Those fields are then used as divisors and shift counts on the next open/I/O:

  • fd.c:1069-1071 and fd.c:1435-1437: d_secpercyl = ft->sectrac * ft->heads; d_ncylinders = ft->size / info.d_secpercyl; β†’ divide by zero if sectrac==0 or heads==0.
  • fdstate does blknum/(sectrac*heads) and sec/sectrac (same divisor family).
  • fd.c:1484: fdblk = 128 << (fd->ft.secsize); β†’ signed-shift overflow / UB for secsize >= 25, and shift-count >= width for secsize >= 32.

So a planted type with sectrac=0 (or heads=0) arms a kernel divide-by-zero trap; secsize>=25 is signed-shift UB. FD_STYPE is root-gated (caps_priv_check(SYSCAP_RESTRICTEDROOT)), so the unprivileged angle is the "root arms a trap, operator-group user trips it on open()" scenario from the finding summary β€” a real but narrow, root-assisted threat.

Why not reproduced (valid hard blocker)

The "dead/unreachable at runtime on this guest AND no harness can exercise it" blocker applies β€” double:

  1. HW gate: this QEMU audit guest has no ISA floppy controller (no -fda, no FDC). dmesg has zero fdc/floppy lines; /dev/fd0 does not exist.
  2. Config gate: sys/conf/files marks dev/disk/fd/fd.c as optional fd, and X86_64_GENERIC has no device fd (only natapifd, the ATAPI floppy). The floppy driver is therefore not compiled into the default kernel at all.

There is no way to reach the ioctl handler without both a compiled-in driver and a floppy device node. ./fd_stype /dev/fd0 fails at open() with ENOENT β€” the gate holds; no divide-by-zero.

Fix (fix.diff) β€” defense-in-depth

Adds validation to FD_STYPE before installing the type: require sectrac > 0, heads > 0, and 0 <= secsize <= 7 (FDC sector-size codes are 0-6; this also forbids the shift-UB range), returning EINVAL otherwise. This closes the div-by-zero and shift-UB paths regardless of HW presence. Matches the finding's intent (validate fd_type) as a defense-in-depth guard.

Fix validation

not_testable for runtime (no FDC), but the fix was verified to apply + compile in-tree: a full make -j6 nativekernel KERNCONF=X86_64_GENERIC with device fd added to the config (to actually compile fd.c) completed with rc=0, 0 compiler errors, producing fd.o and kernel.stripped containing the patched driver. By trace, the added EINVAL guard sits directly on the root-cause store path (fd->ft = *nft only after the bounds pass), so the div-by-zero/shift-UB can no longer be armed via FD_STYPE.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: floppy path is HW+config gated on this guest (/dev/fd0 absent, driver not compiled in), so PoC cannot run. Validated instead that fix.diff applies cleanly and compiles in-tree (nativekernel build with device fd: NK_DONE rc=0, 0 errors, fd.o produced), and by trace added EINVAL guard sits directly on root-cause store path so div-by-zero/shift-UB can no longer be armed via FD_STYPE.

gate: ls /dev/fd0 -> No such file or directory; dmesg fdc/floppy count=0. | fix compiles: nativekernel (device fd) NK_DONE rc=0, 0 errors; fd.o produced; fdioctl symbol present in fd.o.
↓ fix.diffn/a (no runtime test possible -- no FDC). Verified fix.diff applies + compiles in-tree: full 'make -j6 nativekernel KERNCONF=X86_64_GENERIC' with 'device fd' added to config (to actually compile fd.c) completed rc=0, 0 compiler errors, producing fd.o and kernel.stripped containing patched driver.

Confirmed kernel references

Detail

Exploit chain

none. Beyond HW/config unreachability, FD_STYPE is root-gated (caps_priv_check(SYSCAP_RESTRICTEDROOT) at fd.c:2327 returns EPERM for non-root), so unprivileged user cannot set type directly; finding's angle is 'root plants bad type, operator-group user trips trap on open()' -- narrow root-assisted DoS, not unprivileged->root escalation. No chain developed because path unreachable on this guest.

Evidence (decisive lines)

ls: /dev/fd0: No such file or directory | dmesg fdc/floppy grep count = 0 | open(/dev/fd0) -> ENOENT, RUN_EXIT=1, no divide-by-zero | config: X86_64_GENERIC has no 'device fd' (fd.c is 'optional fd').

PoC changes

Authored fd_stype.c (FD_STYPE ioctl with sectrac=0 arming div-by-zero -- documents trigger for host that has floppy), build.sh, run.sh (demonstrates HW/config gate: open fails ENOENT).

Verified recommended fix

Defense-in-depth: in FD_STYPE (sys/dev/disk/fd/fd.c) validate caller-supplied type before installing it -- require sectrac>0, heads>0, and 0<=secsize<=7 (FDC sector-size codes 0-6; also forbids shift-UB range), returning EINVAL otherwise. Closes div-by-zero and shift-UB paths regardless of HW. Matches finding intent as defense-in-depth guard. Full git-apply-able diff in findings/poc/DF-2455/fix.diff.

Verdict

NOT REPRODUCED -- valid hard blocker (genuinely not reachable on this kernel/guest, doubly gated). The bug is REAL in source: FD_STYPE (sys/dev/disk/fd/fd.c:2325-2330) stores a fully caller-supplied struct fd_type into fd->ft with no field validation; fd->ft.sectrac and .heads then used as divisors (d_secpercyl = sectrac*heads; d_ncylinders = size / d_secpercyl at fd.c:1069-1071 and 1435-1437) and fd->ft.secsize is left-shift count (128 << secsize at fd.c:1484), so sectrac=0/heads=0 arms kernel divide-by-zero trap and secsize>=25 is signed-shift UB. BUT: (1) HW gate -- this QEMU audit guest has no ISA floppy controller, dmesg has zero fdc/floppy lines, /dev/fd0 does not exist; (2) config gate -- sys/conf/files marks dev/disk/fd/fd.c as 'optional fd' and X86_64_GENERIC has no 'device fd' (only natapifd), so floppy driver not even compiled into default kernel. ./fd_stype /dev/fd0 fails at open() with ENOENT; no divide-by-zero reachable. Trigger PoC (fd_stype.c) documents what would fire on host WITH a floppy.