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)
PoC verification
Evidence pack
findings/poc/DF-2455 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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-1071andfd.c:1435-1437:d_secpercyl = ft->sectrac * ft->heads;d_ncylinders = ft->size / info.d_secpercyl;β divide by zero ifsectrac==0orheads==0.fdstatedoesblknum/(sectrac*heads)andsec/sectrac(same divisor family).fd.c:1484:fdblk = 128 << (fd->ft.secsize);β signed-shift overflow / UB forsecsize >= 25, and shift-count >= width forsecsize >= 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:
- HW gate: this QEMU audit guest has no ISA floppy controller (no
-fda, no FDC).dmesghas zerofdc/floppylines;/dev/fd0does not exist. - Config gate:
sys/conf/filesmarksdev/disk/fd/fd.casoptional fd, andX86_64_GENERIChas nodevice fd(onlynatapifd, 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_testablenot_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.
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.
No comments yet.