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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2270 Β· 28 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 5.9 KB ↓ raw
verdict.json β€” 4.3 KB view raw
manifest.json β€” 1.6 KB view raw
fix.diff β€” 296 B view raw
env.txt β€” 647 B view raw
hang_iteration1.txt β€” 1.1 KB view raw
rig/hang_serial.log β€” 132 B view raw
rig/hang_pagedaemon.log β€” 1.2 KB view raw
rig/df_pre.c β€” 3.6 KB view raw
rig/df_post.c β€” 15.5 KB view raw
rig/ch_user.c β€” 8.9 KB view raw
rig/gen.sh β€” 1.2 KB view raw
rig/check_verbatim.sh β€” 922 B view raw
rig/build.sh β€” 2.8 KB view raw
rig/make_fix.sh β€” 677 B view raw
rig/run_tests.sh β€” 1.6 KB view raw
rig/run_hang.sh β€” 709 B view raw
rig/run_fixed.sh β€” 1.2 KB view raw
rig/fix2270.diff β€” 296 B view raw
rig/fix227x_all.diff β€” 8.6 KB view raw
rig/build.log β€” 5.6 MB ↓ download
rig/build_user.log β€” 0 B ↓ download
rig/disasm_full.txt β€” 18.9 KB view raw
rig/disasm_fixed.txt β€” 117.2 KB view raw
rig/verbatim.log β€” 190 B view raw
build.log β€” 5.3 KB view raw
run.log β€” 1.6 KB view raw

DF-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 consulted sc_counts[8] = canary 0x5a5a5a5a = 1515870810)
  • ch_user ges 1000 0 1000 -> SUCCESS (OOB read +4000 bytes past array)
  • sysctl debug.dfch.map=1 prints exactly which OOB words authorize (canary) and which reject (sc_movemask bytes read as -1)
  • sysctl debug.dfch.hang=1 (or a userland ges 8 0 65535 against a target reporting desclen 0xffff) computes size = 0xffff*0xffff+16 β‰ˆ 4GiB and calls kmalloc(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 300 and ges 1000 0 1000 -> EINVAL (gate added)
  • ges 1 0 16 still SUCCESS, ges 1 0 17 still EINVAL (no regression)
  • the same debug.dfch.hang=1 direct 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 suite
  • hang_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 kernel
  • fix.diff β€” the one-hunk fix
  • env.txt, verdict.json, manifest.json
VERDICT.md
↓ download raw

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_counts is int 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_count with 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.

  1. OOB map (sysctl debug.dfch.map=1, run_dmesg.log): chet 0..3 in-range; chet 4-5 read sc_movemask/sc_exchangemask bytes as 0xffffffff (-1, gate rejects); chet 6 reads settledelay+pad (0); chet >= 7 reads the canary 0x5a5a5a5a (1515870810) β€” the gate ACCEPTS any base/count for a nonexistent element type.
  2. 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)
  3. 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 minutes The 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 omitted cam_periph_lock β€” documented in hang_iteration1.txt and fixed; iteration 2 is the valid evidence.)
  4. 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 300 SUCCESS, ges 1000 0 1000 SUCCESS, hang test kills the guest
  • fixed (run_fixed_all.log): ges 8 0 300 EINVAL, ges 1000 0 1000 EINVAL, ges 1 0 16 SUCCESS, ges 1 0 17 EINVAL (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.hang trigger calls the real chgetelemstatus with a kernel-side cesr after taking the periph lock exactly as chioctl does; the userspace ges path (real ioctl) demonstrates the same gate bypass without any harness shortcut.
  • The first baseline rig iteration had a shim bug (MODE SENSE guard len >= 64 vs 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_testable
↓ fix.diffper-fix-DF-2270

Confirmed kernel references

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).