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

Type confusion in hpt_get_info: goto is_array passes RAID-1 member disk to hpt_copy_array_info -> punned union fields (bArnMember=df_on_line=1, bit21/22 of disk capacity as rf_rebuilding/rf_verifying, pMember[0]=Device.mv) cause kernel SIGFPE panic on unprivileged sysctl hptmv.status read

Summary

goto is_array passes the member DISK instead of pParent; VDevice.u puns RaidArray/Device so bArnMember=1 and capacity bits 21/22 become the rebuild/verify flags - for ~half of disk capacities (1TB/500GB tested) the % ksprintf divides by (bArnMember-1)==0 -> Fatal trap 18. REPRODUCED on guest via KLD harness with verbatim copies + CTASSERT layout proof: panic vs fix-survives A/B. Any unpriv user reading world-readable hptmv.status detonates (requires admin-built RAID-1 from enumerated disk). Secondary: adjacent VDevice memory read as ArrayName.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4764 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 5.0 KB ↓ raw
h1_harness.c β€” 18.7 KB view raw
build.sh β€” 304 B view raw
run.sh β€” 140 B view raw
build.log β€” 1.2 KB view raw
run.log β€” 1.0 KB view raw
run.2.log β€” 774 B view raw
run.3.log β€” 355 B view raw
run.fixed.log β€” 692 B view raw
panic.txt β€” 1.0 KB view raw
env.txt β€” 258 B view raw
fix.diff β€” 2.4 KB view raw

DF-4764 β€” hptmv type confusion in hpt_get_info (goto is_array passes RAID-1 member disk) β†’ kernel SIGFPE panic on unprivileged sysctl hptmv.status read

File: sys/dev/raid/hptmv/hptproc.c (pass 2) Bucket: local DoS (panic) via world-readable sysctl; secondary adjacent-memory misparse.

What this pack contains

  • h1_harness.c β€” KLD harness built on the audit guest against the real driver headers (<dev/raid/hptmv/global.h>). Embeds verbatim copies of hpt_copy_array_info (hptproc.c:402-478), hpt_copy_disk_info (:372-399), get_disk_name (:339-349) and the hpt_get_info device-list loop (:515-549). CTASSERTs prove the union layout the analysis relies on.
  • triggers:
  • 1 = DF-4764 baseline (expect panic)
  • 2 = DF-4765 baseline (expect panic)
  • 3 = DF-4766 baseline (expect overread output)
  • 4/5/6 = fixed variants (fix.diff applied to the copies; expect survival)
  • build.sh / run.sh, build.log, run.log (H1 panic), run.2.log (H2 panic), run.3.log (H3 overread), run.fixed.log (H1F/H2F/H3F), panic.txt, env.txt, fix.diff, manifest.json, verdict.json.

Build

On the DragonFly guest (kernel source at /usr/src):

mkdir -p /root/h1 && cp h1_harness.c /root/h1/
printf 'SYSDIR=\t/usr/src/sys\nKMOD=   h1harness\nSRCS=   h1_harness.c\nCFLAGS+= -I/usr/src/sys\n.include "/usr/src/sys/conf/kmod.mk"\n' > /root/h1/Makefile
cd /root/h1 && make SYSDIR=/usr/src/sys

Run

kldload /root/h1/h1harness.ko
sysctl h1harness.trigger=1     # DF-4764 baseline: panics the guest
# after vm.sh reset with-src: reload, then
sysctl h1harness.trigger=4     # DF-4764 fix: survives

Expected output (baseline trigger 1, from run.log)

H1:   bArnMember     = df_on_line    = 1
H1:   rf_broken      = bit16(dDeRealCapacity) = 0
H1:   rf_rebuilding  = bit21(dDeRealCapacity) = 1
H1:   pMember[0]     = Device.mv    = 0xffffffff82607140
Fatal trap 18: integer divide fault while in kernel mode
Stopped at hpt_copy_array_info.constprop.3+0x130: divq %rdi,%eax

Honest scope note

The mechanism (verbatim kernel function, real driver structs, verbatim call path including the buggy hptproc.c:535 goto) is reproduced on the guest. End-to-end reachability through the live sysctl additionally requires hptmv controller hardware with a RAID-1 array whose source disk remains listed in _vbus_p->pVDevice[] β€” the exact state the driver's own comment at hptproc.c:530-533 describes. No hptmv controller exists on the audit guest, so the live path was verified statically (see VERDICT.md). The sysctl itself is present and world-readable on the guest (hptmv.status).

VERDICT.md
↓ download raw

DF-4764 VERDICT

Status: REPRODUCED (mechanism, source+harness on guest) β€” kernel panic (SIGFPE) via type confusion on an unprivileged sysctl read path; end-to-end detonation needs hptmv hardware (absent from guest), statically established.

The bug

sys/dev/raid/hptmv/hptproc.c:515-549 β€” hpt_get_info() walks the VBus logical-device list. For each _vbus_p->pVDevice[i] that is not itself an array but has been added to a RAID-1 after driver load (the driver's own comment at :530-533: "it may be add to an array after driver loaded, check it"), the code jumps into the array-display branch:

534:  if (pVDev->pParent->VDeviceType==VD_RAID_1 && pVDev==pVDev->pParent->u.array.pMember[0])
535:      goto is_array;            /* ← passes the member DISK */
...
523:  is_array:
524:      hpt_copy_array_info(pinfo, j, pVDev);

The label invokes hpt_copy_array_info() with the member disk, not with pVDev->pParent. VDevice.u is a union of RaidArray and Device (vdevice.h:71-76), so the disk's fields are re-parsed as array fields (LP64 layout proven by CTASSERT in the harness, matching array.h:54-99 / global.h:70-88 / vdevice.h):

punned array field overlaid disk field value for an online disk
bArnMember (u+0) df_on_line (u+0) 1
rf_broken (bit16 of u+26) bit16 of dDeRealCapacity (u+24) disk-size dependent
rf_rebuilding (bit21), rf_verifying (bit22) bits 21/22 of dDeRealCapacity disk-size dependent
RebuildSectors (u+32) HeadPosition/QueueLength junk
pMember[0] (u+40) Device.mv (u+40) valid kernel pointer (MV_SATA_CHANNEL*)

In hpt_copy_array_info, with sStatus still NULL (online, bit16=0), the member loop runs bArnMember = 1 time; mIsArray(pMember[0]) dereferences the mv channel pointer (valid, so no fault), reading its first byte (channelNumber ≀ 3 on RR18xx β†’ false) so pTmpArray = pArray (the disk); then bit21/bit22 of the disk capacity is read as rf_rebuilding / rf_verifying and the "Rebuilding/Verifying %" ksprintf at hptproc.c:457/462 divides by

(pTmpArray->VDeviceCapacity / (pTmpArray->u.array.bArnMember - 1)) >> 11

with bArnMember = df_on_line = 1 β†’ x / 0 β†’ integer-divide trap (#DE) in kernel mode β†’ panic.

Disk sizes with bit16=0 and bit21 or bit22 set detonate; e.g. the common 1 TB sector count 0x7470DA70 (bit16=0, bit21=1, bit22=1), 500 GB 0x3A38A5D0 (bit16=0, bit21=1) β€” roughly half of all capacities. Sizes with bit16=1 instead display "Critical" (no crash); sizes with bits 21/22 clear print a garbage row (misparse of live kernel objects, plus ArrayName read from u+104 past the 48-byte disk union tail).

Trigger

Unprivileged: sysctl hptmv.status (world-readable sysctl; confirmed on the guest β€” unprivileged read works, unprivileged write is EPERM via kern_sysctl.c:1446). Precondition: a RocketRAID 18xx system whose admin built a RAID-1 whose source disk remains enumerated (the state hptproc.c:530-535 explicitly handles).

Reproduction (guest, DragonFly 6.5-DEVELOPMENT #0, cc 8.3)

  • h1harness.ko built against /usr/src/sys with the real driver headers; CTASSERT layout proof passed at load.
  • sysctl h1harness.trigger=1 β†’ exact predicted state printed, then Fatal trap 18: integer divide fault while in kernel mode, Stopped at hpt_copy_array_info.constprop.3+0x130: divq %rdi,%eax (run.log, panic.txt). Guest down.
  • Fix variant (trigger 4, pVDev = pVDev->pParent per fix.diff): renders the proper RAID-1 row and survives (run.fixed.log).

Reachability gap (honest)

The live path sysctl hptmv.status β†’ hpt_status β†’ hpt_get_info β†’ (arrays present) requires hptmv controller hardware. The audit guest has none (hptmv.status prints only the driver-version line β€” gIal_Adapter is NULL), so the final hop (real RAID-1 on real hardware) is static: hptproc.c:530-533's comment documents that member disks stay listed in pVDevice[]; the whole pParent/pMember[0] special-case exists only because of it. Everything downstream of that state was executed verbatim in the guest kernel via the harness.

Kernel references

Pass the parent array (fix.diff): pVDev = pVDev->pParent; goto is_array;. Validated in-harness: trigger 4 survives. Also harden the % (DF-4765) and the name terminator (DF-4766).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Harness-level A/B: unpatched verbatim copy panics (Fatal trap 18, run.log); copy with fix.diff applied renders the proper RAID-1 row and survives (run.fixed.log trigger 4). Kernel-rebuild validation not possible on this guest (no hptmv hardware to reach the live path).

run.log vs run.fixed.log; fix.diff applies cleanly to pristine sys/dev/raid/hptmv/hptproc.c (verified with patch(1)).
↓ fix.diffper-fix-DF-4764

Confirmed kernel references

Detail

Exploit chain

admin creates RAID-1 on RocketRAID 18xx from an enumerated disk (source disk stays in pVDevice[], pParent set) -> any local user runs 'sysctl hptmv.status' -> hpt_get_info goto is_array(disk) -> hpt_copy_array_info puns disk fields as RaidArray -> ksprintf divides by (bArnMember-1)==0 -> SIGFPE -> kernel panic (local DoS). No write primitive; wild-read is confined to the live MV_SATA_CHANNEL object (valid pointer) plus union-tail reads.

Evidence (decisive lines)

run.log (punned-state printout then Fatal trap 18, Stopped at hpt_copy_array_info.constprop.3+0x130: divq %rdi,%eax); panic.txt; run.fixed.log (trigger 4: with fix.diff's pVDev=pVDev->pParent the loop renders the RAID-1 row and survives); VERDICT.md for the full layout derivation; env.txt (guest state, hptmv.status live, gIal_Adapter NULL).

PoC changes

Seed sketch rewritten entirely: built a KLD harness that compiles the real driver headers and embeds verbatim function copies with CTASSERT layout proofs; sysctl h1harness.trigger=1..3 (baseline) and 4..6 (fix.diff applied) select the cases.

Verified recommended fix

Pass the parent array at hptproc.c:535: 'pVDev = pVDev->pParent; goto is_array;' (plus the DF-4765 divisor guard and DF-4766 terminator in the same fix.diff).

Verdict

REPRODUCED at source+harness (same fidelity as the accepted DF-1851 precedent for this driver): a KLD built on the audit guest compiles the REAL hptmv headers and VERBATIM copies of hpt_copy_array_info (hptproc.c:402-478) and the hpt_get_info device-list loop (hptproc.c:515-549); CTASSERTs prove the union layout. Trigger 1 walks a VBus that still lists a RAID-1 source disk, executes the hptproc.c:535 'goto is_array' with the member DISK, prints the punned state exactly as predicted (bArnMember=df_on_line=1, rf_rebuilding=bit21(dDeRealCapacity)=1, pMember[0]=Device.mv=0xffffffff82607140) and dies with 'Fatal trap 18: integer divide fault' at hpt_copy_array_info+0x130 divq. In production the detonator is an UNPRIVILEGED 'sysctl hptmv.status' read (world-readable; unpriv read confirmed on guest, unpriv write is EPERM per kern_sysctl.c:1446); the remaining precondition - hptmv controller hardware with a RAID-1 whose source disk remains enumerated - cannot exist on this guest (no hptmv PCI device, gIal_Adapter==NULL) and is established statically: hptproc.c:530-533's own comment documents that member disks stay listed, and the entire pParent/pMember[0] special-case exists only for that state. Roughly half of common disk capacities (bit16=0, bit21 or bit22 set, e.g. 1TB 0x7470DA70, 500GB 0x3A38A5D0) satisfy the crash condition; other capacities yield garbage display reading the MV_SATA_CHANNEL object as an array plus ArrayName read past the 48-byte disk union.