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

Unvalidated hw.icu.msi_start boot tunable: negative value -> negative-index OOB scan/write in icu_abi_msi_alloc_intern() + msi_intr[] negative OOB read + setidt() overwrite of exception gates 0..31 -> triple-fault cold reset (ICU twin of DF-1037)

Summary

The knob parses via strtoq (accepts '-') and masks with signed ~0x1f with NO range check, initmap running unconditionally every boot: hw.icu.msi_start=-32 poisons every later ICU MSI allocation - the scan starts at i=-32, aliases the previous cpu's row tail (passes the window check), OOB-writes im_type/im_msi_base, returns negative vectors, and msi_setup's msi_intr[-k] OOB rodata reads feed setidt(0..31, garbage) - exception-gate overwrite on any cpu. Boot-config-proven END-TO-END: with the knob in loader.conf the trigger alloc returned rc=0 with zone[0]=-32, then int3 on cpu1 produced a silent TRIPLE FAULT (serial: int3 line immediately followed by the loader menu - cold self-reset). No KASSERT guards this path on ANY config. Range-check-after-mask fix validated with the knob still set. The DF-1037 ioapic twin deserves re-verification.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5466 Β· 12 files
FileTypeDescriptionSize
icu_msi_start.c β€” 3.5 KB view raw
build.sh β€” 126 B view raw
run.sh β€” 208 B view raw
build.log β€” 11.2 KB view raw
run.log β€” 1.0 KB view raw
run_fixed.log β€” 524 B view raw
panic_triplefault.txt β€” 865 B view raw
env.txt β€” 2.3 KB view raw
VERDICT.md β€” 4.0 KB ↓ raw
fix.diff β€” 2.2 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 5.1 KB view raw
VERDICT.md
↓ download raw

DF-5466 VERDICT β€” reproduced (negative boot tunable β†’ negative vectors β†’ exception-gate smash β†’ triple fault), Low severity

Bottom line

REPRODUCED on the production-behaviour kernel (X86_64_NOINV), and β€” unlike DF-5464/DF-5465 β€” no INVARIANTS KASSERT guards this path at all, so every kernel config is equally affected. hw.icu.msi_start is parsed by icu_abi_initmap() (icu_abi.c:323-324) with kgetenv_int β†’ strtoq (accepts '-'), masked with signed ~0x1f (-32), and never range-checked. icu_abi_initmap() runs unconditionally at boot (machdep.c:2785), so a single hw.icu.msi_start="-32" in /boot/loader.conf poisons every later ICU MSI allocation:

  • the scan loop runs with i = -32 (icu_abi.c:473); on cpu1 the window icu_irqmaps[1][-32..-1] aliases the previous row's tail [0][160..191] (all UNUSED), so the "free window" check passes;
  • OOB writes im_type=MSI, im_msi_base=-32 into cpu0's row (icu_abi.c:495-496) and intrs[j] = -32+j (icu_abi.c:498);
  • msi_setup(-32..-1, 1) (icu_abi.c:499) reads msi_intr[-32..-1] OOB (msi.c:433-435 β€” the 192-entry table lives in .rodata at 0xffffffff80d9d800, so the "handlers" are rodata garbage) and calls setidt(0x20-32+j = 0..31, garbage, ..., cpu1) β€” overwriting cpu1's exception gates 0..31 (#DE/#PF/#GP/#BP/...).

The rig then pinned itself to cpu1 and executed int $3: the serial console shows icu_msi_start: on cpu1, triggering int3 ... followed immediately by the bootloader menu with zero trap text β€” #BP (gate 3, garbage RIP) β†’ instruction-fetch #PF (gate 14, garbage) β†’ #DF (gate 8, garbage) β†’ triple fault β†’ cold reset. The machine rebooted by itself (fresh dmesg, module gone).

This is the icu twin of DF-1037 (hw.ioapic.msi_start, ioapic_abi.c:753, Info, pass-1, unverified); this pack verifies the class end-to-end.

How it was reproduced (boot-config variant, run.log + panic_triplefault.txt)

# /boot/loader.conf: hw.icu.msi_start="-32"   (kenv confirms -32)
kldload icu_msi_start.ko
icu_msi_start: rc=0 zone[0]=-32 zone[31]=-1        <- NEGATIVE vectors
icu_msi_start: NEGATIVE vectors allocated ... gates 0..31 of cpu1 smashed
icu_msi_start: on cpu1, triggering int3 ...
<serial: next line is the boot loader menu β€” triple fault, no dump>

Primitive characterization

The poisoned tunable turns msi_setup() into an arbitrary-offset IDT gate writer (gates 0..31 of any online cpu, offsets read from OOB rodata before msi_intr[]) and the map writes corrupt the previous cpu's MSI bookkeeping. On a real ICU-ABI machine (no IOAPIC) the first PCI MSI allocation at boot hits this with no KLD involved. Trigger needs root (loader.conf / kenv) β€” hence Low severity despite the hard memory corruption + control-flow redirection.

Fix validation (fix.diff, applied to guest /usr/src, incremental rebuild)

Same boot config on the fixed kernel β€” knob still "-32" in /boot/loader.conf (kenv verified at runtime):

icu_msi_start: rc=0 zone[0]=32 zone[31]=63
icu_msi_start: allocation not OOB ... -- no gate smash, exiting cleanly

initmap clamps the poisoned start into range; allocation lands at the legal window 32..63; no OOB, no smash, guest healthy in the same boot.

Kernel references

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

fix.diff applied to guest /usr/src/sys/platform/pc64/icu/icu_abi.c, X86_64_NOINV rebuilt and rebooted WITH THE SAME BOOT KNOB still in /boot/loader.conf (kenv hw.icu.msi_start = -32 verified at runtime): the trigger module now gets rc=0 zone[0]=32 zone[31]=63 β€” a legal allocation at vectors 32..63 after the clamp β€” prints 'no gate smash, exiting cleanly' and returns; guest healthy in the same boot (run_fixed.log). Bad behavior gone with the boot config unchanged.

run_fixed.log ('rc=0 zone[0]=32 zone[31]=63', 'allocation not OOB (fixed or clamped kernel?) -- no gate smash, exiting cleanly'; uptime continuous)
↓ fix.diffper-fix-DF-5466

Confirmed kernel references

Detail

Exploit chain

root sets hw.icu.msi_start=-32 in loader.conf (or kenv) -> boot-time icu_abi_initmap() computes -32 (icu_abi.c:323-324) -> any ICU MSI alloc (on ICU-ABI machines: first PCI MSI probe at boot via nexus.c:595; here: KLD trigger) -> icu_abi.c:473 loop with negative i -> OOB read/write at icu_irqmaps[cpuid][-32..-1] (previous cpu's row) + intrs[j]=-32+j + msi_setup(-32+j) -> msi_intr[-k] OOB read (rodata) + setidt(0..31, garbage, cpu) = arbitrary-cpu exception-gate overwrite -> any exception on that cpu redirects RIP to OOB-read kernel data -> triple fault / RIP-controlled crash. On a real ICU-ABI machine this fires during boot with no KLD involved.

Evidence (decisive lines)

run.log + panic_triplefault.txt (serial: 'rc=0 zone[0]=-32 zone[31]=-1', 'gates 0..31 of cpu1 smashed', 'triggering int3' immediately followed by the bootloader menu β€” triple fault with no dump; post-reset fresh dmesg/uptime confirm the reset); run_fixed.log (same knob, clamped: zone[0]=32, no smash)

PoC changes

Boot-config variant written fresh: no ksetenv/initmap hacks β€” the knob is set in /boot/loader.conf and a reboot lets the real boot-time initmap() parse it; the trigger module only calls msi_alloc and, if negative vectors were allocated, pins to cpu1 (lwkt_migratecpu) and executes int3 to exercise the smashed gate deterministically (on a fixed kernel zone[0]>=0 and the int3 is skipped).

Verified recommended fix

Range-check icu_abi_msi_start after the mask in icu_abi_initmap() (if (<0 || >=IDT_HWI_VECTORS) = 0), mirroring DF-1037's suggested fix for the ioapic twin (see fix.diff).

Verdict

REPRODUCED on a production-behaviour kernel via the pure boot-config route: 'hw.icu.msi_start="-32"' in /boot/loader.conf (kenn confirmed -32; icu_abi_initmap() runs unconditionally at machdep.c:2785) poisons icu_abi_msi_start to -32 because kgetenv_int/strtoq accepts the sign and the &~0x1f mask never range-checks (icu_abi.c:323-324). The first ICU MSI allocation then scans from i=-32: on cpu1 the window icu_irqmaps[1][-32..-1] aliases cpu0's row tail [160..191] (all UNUSED) so it 'succeeds' β€” msi_alloc returns rc=0 with NEGATIVE vectors (zone[0]=-32 zone[31]=-1), performs OOB map writes (icu_abi.c:495-496) and calls msi_setup(-32..-1, 1) (icu_abi.c:499), whose msi_intr[-32..-1] OOB reads (msi.c:433, rodata garbage) feed setidt(0..31, ..., cpu1) β€” overwriting cpu1's exception gates 0..31. The rig's int3 on cpu1 then produced a TRIPLE FAULT: the serial console shows 'triggering int3' immediately followed by the bootloader menu with zero trap text (#BP->smashed gate->fetch #PF->smashed gate->#DF->smashed gate->cold reset). No INVARIANTS KASSERT exists on this path β€” every kernel config is equally affected. Fix (clamp msi_start into [0, IDT_HWI_VECTORS)) validated on a rebuilt kernel with the SAME boot knob still set: allocation lands legally at vectors 32..63 (zone[0]=32), no OOB, no smash, guest healthy.