Missing bounds check on cesr_element_type in chgetelemstatus allows OOB array index into sc_firsts/sc_counts
Summary
chgetelemstatus() takes int chet=cesr->cesr_element_type from u_int16_t user field and uses it directly as array index into softc->sc_counts[] and softc->sc_firsts[] (each int[CHET_MAX+1]==int[4]) without any > CHET_DT bounds check. Every sibling handler (chmove chexchange chposition chsetvoltag) performs this check first; chgetelemstatus is the sole outlier. Any chet in 4..65535 reads past array bounds. OOB-read value used to authorize rest of call and as sea argument to READ ELEMENT STATUS. cesr_element_count bounded only by OOB-read sc_counts[chet] so if OOB memory yields large positive value kmalloc(M_INTWAIT) can hang LWP forever providing reliable local DoS. [VERIFIED pass2: reproduced via verbatim-driver harness with canary-backed softc - sc_counts[8] returned 0x5a5a5a5a authorizing CHIOGSTATUS type 8 and 1000 to SUCCESS, and the bypassed count with desclen 0xffff drove a 4GiB kmalloc M_INTWAIT that killed the guest via pagedaemon spiral; root-only reach 0600 plus SYSCAP gate; one-hunk sibling-idiom fix validated - OOB types EINVAL, in-range controls unchanged]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2270 Β· 28 filesDF-2270 β Missing bounds check on cesr_element_type in chgetelemstatus
File: sys/bus/cam/scsi/scsi_ch.c:1066-1200
Claim: chet = cesr->cesr_element_type (u_int16_t from user, values 4..65535)
indexes int sc_firsts[4] / int sc_counts[4] with NO bounds check β the only
ioctl handler in the driver without one β so the range check that gates
cesr_element_count is evaluated against out-of-bounds heap words.
Verdict: REPRODUCED β OOB-read-driven validation bypass + guest-killing DoS
Verified on the CAM verbatim-interposition harness kernel: the byte-identical
scsi_ch.c compiled into a DragonFly 6.5-DEVELOPMENT INVARIANTS kernel
(rig/, sha256-gated check_verbatim.sh), a fabricated ch_softc placed
inside a 512KB 0x5a-canary M_DEVBUF region, and a real /dev/ch0 created
by the drivers own make_dev call. All tests drove the REAL chioct/
chgetelemstatus code through a real userland ioctl(2) on /dev/ch0.
Reproduce
cd rig ./build.sh # push harness, gen TU, nativekernel, install, reboot ./run_tests.sh # baseline suite (run_all.log / run_dmesg.log) ./run_hang.sh # DESTRUCTIVE 4GiB M_INTWAIT test (hang_serial.log) ./make_fix.sh # apply fix227x_all.diff on guest /usr/src ./build.sh # fixed kernel ./run_fixed.sh # A/B suite (run_fixed_all.log)
Expected (vulnerable kernel)
ch_user ges 1 0 16-> SUCCESS (in-range control works)ch_user ges 1 0 17-> EINVAL (in-range validation works)ch_user ges 8 0 300-> SUCCESS (element type 8 does not exist; the gate consultedsc_counts[8]= canary0x5a5a5a5a= 1515870810)ch_user ges 1000 0 1000-> SUCCESS (OOB read +4000 bytes past array)sysctl debug.dfch.map=1prints exactly which OOB words authorize (canary) and which reject (sc_movemaskbytes read as-1)sysctl debug.dfch.hang=1(or a userlandges 8 0 65535against a target reporting desclen 0xffff) computessize = 0xffff*0xffff+16β 4GiB and callskmalloc(size, M_DEVBUF, M_INTWAIT):
DFH2270HANG: thread calling REAL chgetelemstatus() with type=8 count=65535 ... DFH2270HANG: *** THREAD DID NOT RETURN within 8s: kmalloc(..., M_INTWAIT) of ~4GiB is stuck (watchdog fired; system must be reset) pagedaemon: WARNING! Continuous paging for 1 minutes pagedaemon: WARNING! Continuous paging for 1 minutes
after which the guest dies outright (ssh banner timeout, force-kill + reset).
Expected (fixed kernel)
ges 8 0 300andges 1000 0 1000-> EINVAL (gate added)ges 1 0 16still SUCCESS,ges 1 0 17still EINVAL (no regression)- the same
debug.dfch.hang=1direct trigger returns instantly:chgetelemstatus RETURNED (no hang),RES ccbs=0
Impact ceiling
Privesc: NONE. /dev/ch0 is created UID_ROOT/GID_OPERATOR 0600
(scsi_ch.c:364-366) and chopen requires SYSCAP_RESTRICTEDROOT
(scsi_ch.c:396) β verified crw------- root operator on the guest. Only
root reaches the bug. Consequence = root-triggerable kernel heap OOB read
(4..65535 ints past two arrays, values used as authorization AND as SCSI
element addresses in the CDB, scsi_ch.c:1100/1137) plus the demonstrated
guest-killing M_INTWAIT allocation hang. Medium severity as filed stands.
Fix
fix.diff (= rig/fix2270.diff, included in rig/fix227x_all.diff): the exact
sibling idiom from chmove (scsi_ch.c:805):
/* perform argument checking */
+ if (cesr->cesr_element_type > CHET_DT)
+ return (EINVAL);
Files
rig/full harness (df_pre.c / verbatim scsi_ch.c / df_post.c, gen.sh, check_verbatim.sh, build.sh, make_fix.sh, run_*.sh, ch_user.c)run_all.log,run_dmesg.logβ baseline suitehang_serial.log,hang_pagedaemon.logβ destructive DoS run (serial)hang_iteration1.txtβ harness-artifact panic of iteration 1 (honesty)run_fixed_all.log,run_fixed_dmesg.logβ A/B on the fixed kernelfix.diffβ the one-hunk fixenv.txt,verdict.json,manifest.json
DF-2270 β VERDICT
Status: REPRODUCED (dos, certain)
The missing bounds check is real, is the outlier in its own file, and the
full consequence chain was reproduced mechanically on a stock INVARIANTS
DragonFly 6.5-DEVELOPMENT guest with the byte-identical driver code
compiled into the kernel (CAM verbatim-interposition rig, rig/).
Root cause (line-accurate)
chgetelemstatus() (sys/bus/cam/scsi/scsi_ch.c:1053):
- :1066
int chet = cesr->cesr_element_type;β u_int16_t from the copied-in ioctl struct; user-controlled 0..65535. NO bounds check follows. - :1077-1079 the only range gate:
c if ((softc->sc_counts[chet] - cesr->cesr_element_base) <= 0 || (cesr->cesr_element_base + cesr->cesr_element_count) > softc->sc_counts[chet]) return (EINVAL);sc_countsisint sc_counts[CHET_MAX + 1]=int[4](scsi_ch.c:158-159). For chet 4..65535 the gate is evaluated against out-of-bounds heap words (twice β the array is read in both clauses). - :1100 and :1137-1138
softc->sc_firsts[chet]β two more OOB reads whose values are written into the READ ELEMENT STATUS CDB sent to the device (element address fully heap-derived). - :1120-1122
size = 8 + 8 + desclen * cesr_element_countwith the count that the OOB word just authorized. - :1129
kmalloc(size, M_DEVBUF, M_INTWAIT).
Every sibling handler checks: chmove :805, chexchange :859, chposition
:924, chsetvoltag :1261 (if (xxx_type > CHET_DT) return (EINVAL/ENODEV)).
chgetelemstatus is the only one that does not.
Mechanical evidence
Rig: fabricated ch_softc zeroed at offset 256KB of a 512KB M_DEVBUF
region filled with 0x5a; periph + make_dev(&ch_ops, ..., UID_ROOT,
GID_OPERATOR, 0600, "ch%d", 0) exactly as chregister does (:364-366);
fake CAM transport answers MODE SENSE (element address page 0x1d:
firsts 0x1000/0x2000/0x3000/0x4000, counts 2/16/2/4; cap page 0x1f) and
READ ELEMENT STATUS. Tests drove the REAL code through real userland
ioctl(2) on /dev/ch0.
- OOB map (
sysctl debug.dfch.map=1, run_dmesg.log): chet 0..3 in-range; chet 4-5 readsc_movemask/sc_exchangemaskbytes as0xffffffff(-1, gate rejects); chet 6 reads settledelay+pad (0); chet >= 7 reads the canary0x5a5a5a5a(1515870810) β the gate ACCEPTS any base/count for a nonexistent element type. - Validation bypass through the real device (run_all.log):
-
ges 1 0 16-> SUCCESS (control, 16 statuses returned) -ges 1 0 17-> EINVAL (control: in-range validation works) -ges 8 0 300-> SUCCESS: 300 element statuses returned for element type 8, authorized solely by the OOB canary word -ges 1000 0 1000-> SUCCESS (OOB read 4000 bytes past the array) -ges 4 0 1-> EINVAL (heap-dependent: movemask bytes as -1) - M_INTWAIT consequence (hang_serial.log / hang_pagedaemon.log,
captured on the serial console): the same call with count=65535 and a
target desclen of 0xffff computes
size = 0xffff*0xffff+16 β 4GiB;kmalloc(..., M_DEVBUF, M_INTWAIT)never completes:DFH2270HANG: *** THREAD DID NOT RETURN within 8s ... (watchdog fired) pagedaemon: WARNING! Continuous paging for 1 minutes pagedaemon: WARNING! Continuous paging for 1 minutesThe guest then died outright (ssh banner exchange timeout; the VM had to be force-killed and reset). sysctl run_rc=124. This is a guest-killing local DoS from a single ioctl. (Iteration 1 of this test panicked on a harness calling-convention artifact β kthread omittedcam_periph_lockβ documented in hang_iteration1.txt and fixed; iteration 2 is the valid evidence.) - OOB-derived CDB content: with chet=8 the fake transport received
READ ELEMENT STATUS with sea=0xffff (
sc_firsts[8]OOB read) β the element address handed to the device is heap garbage (visible in rig transport counters).
Privesc assessment (team directive)
CLOSED β no unprivileged escalation:
- /dev/ch0 is crw------- root operator (make_dev at scsi_ch.c:364-366;
observed live on the guest, run_all.log). Only uid 0 can open it.
- chopen independently requires SYSCAP_RESTRICTEDROOT
(scsi_ch.c:396) β verified present in the verbatim code compiled here.
- The OOB reads are reads; no write primitive exists in this function
(the kmalloc target is fresh memory; user_data/data writes are
driver-internal). The impact is authorization-from-garbage + DoS.
Medium severity (privileged-user DoS + heap-dependent OOB read in a
default-config-reachable driver) is the correct ceiling.
Fix validation (A/B)
fix.diff (one hunk, the chmove idiom at :805 applied to
chgetelemstatus, placed right after /* perform argument checking */):
- vulnerable:
ges 8 0 300SUCCESS,ges 1000 0 1000SUCCESS, hang test kills the guest - fixed (run_fixed_all.log):
ges 8 0 300EINVAL,ges 1000 0 1000EINVAL,ges 1 0 16SUCCESS,ges 1 0 17EINVAL (no regression), and the identical direct-call hang trigger returns instantly:DFH2270HANG: chgetelemstatus RETURNED (no hang),RES ccbs=0.
fix_status: fixed.
Honest notes
- The
debug.dfch.hangtrigger calls the real chgetelemstatus with a kernel-side cesr after taking the periph lock exactly as chioctl does; the userspacegespath (real ioctl) demonstrates the same gate bypass without any harness shortcut. - The first baseline rig iteration had a shim bug (MODE SENSE guard
len >= 64vs actual param_len 30) that zeroed the real counts; fixed and the full suite re-run β the bypass results were identical across both iterations (they never depended on the in-range values).
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
not_testableConfirmed kernel references
- sys/bus/cam/scsi/scsi_ch.c:1066
- sys/bus/cam/scsi/scsi_ch.c:1077
- sys/bus/cam/scsi/scsi_ch.c:1079
- sys/bus/cam/scsi/scsi_ch.c:1100
- sys/bus/cam/scsi/scsi_ch.c:1137
- sys/bus/cam/scsi/scsi_ch.c:1120
- sys/bus/cam/scsi/scsi_ch.c:1129
- sys/bus/cam/scsi/scsi_ch.c:158
- sys/bus/cam/scsi/scsi_ch.c:159
- sys/bus/cam/scsi/scsi_ch.c:805
- sys/bus/cam/scsi/scsi_ch.c:364
- sys/bus/cam/scsi/scsi_ch.c:396
Detail
Exploit chain
open /dev/ch0 (root only) -> ioctl CHIOGSTATUS with cesr_element_type=8 (no bounds check) -> gate reads sc_counts[8] OOB = adjacent heap word (canary 0x5a5a5a5a) -> count-validation bypass (base+count accepted for nonexistent type) -> READ ELEMENT STATUS issued with OOB sc_firsts[8] as element address -> desclen*count size math -> kmalloc(~4GiB, M_DEVBUF, M_INTWAIT) -> pagedaemon death spiral -> guest-killing DoS. No write primitive, no unpriv path: escalation closed.
Evidence (decisive lines)
run_all.log (ges matrix: type 8/1000 SUCCESS vs type 1 count 17 EINVAL), run_dmesg.log (DFH2270MAP OOB value table), hang_serial.log + hang_pagedaemon.log (watchdog fired + Continuous paging + guest death), hang_iteration1.txt (honest harness-artifact panic note), run_fixed_all.log (A/B: OOB types EINVAL, in-range SUCCESS), rig/ for the full harness and scripts
PoC changes
No seed existed; built the CAM verbatim-interposition kernel harness from scratch per the established scsi_sa/scsi_cd scaffolding (sha256-gated verbatim scsi_ch.c compiled into the kernel, interposed xpt/cam_periph externals, fabricated canary-backed softc, real /dev/ch0 via the drivers own make_dev, fake MODE SENSE / READ ELEMENT STATUS transport, userland ioctl driver). Fixed two rig bugs found during bring-up: MODE SENSE shim length guard (64 -> 30) and the hang-test kthread missing cam_periph_lock (iteration-1 lockmgr panic, documented).
Verified recommended fix
Add the sibling bounds-check idiom in chgetelemstatus: if (cesr->cesr_element_type > CHET_DT) return (EINVAL); before the sc_counts range check (fix.diff)
Verdict
Missing bounds check on the user-supplied cesr_element_type (scsi_ch.c:1066) reproduced in full on the verbatim-driver harness kernel: the range gate at :1077-1079 evaluates against out-of-bounds heap words past int sc_counts[4], so with chet>=7 landing on heap value 0x5a5a5a5a the real /dev/ch0 CHIOGSTATUS returned SUCCESS for 300 and 1000 elements of element types that do not exist, while in-range controls validate correctly; driving the same bypassed count to 65535 with a 0xffff desclen computes a ~4GiB size and the kmalloc(M_DEVBUF, M_INTWAIT) at :1129 wedged the whole guest (pagedaemon continuous-paging warnings on serial, ssh death, force-kill + reset). No privesc: /dev/ch0 is root:operator 0600 (scsi_ch.c:364-366) and chopen requires SYSCAP_RESTRICTEDROOT (scsi_ch.c:396), verified live; impact is a root-triggerable OOB-read-driven authorization bypass plus guest-killing DoS. The one-hunk sibling-idiom fix (return EINVAL for type > CHET_DT) makes ges 8/1000 fail with EINVAL, keeps in-range behavior identical, and defuses the identical direct-call hang trigger (returns instantly, RES ccbs=0).
No comments yet.