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

mskc_handle_events(): 'continue' on a status LE naming an unattached port skips the loop's only consumer-index advance -> permanent mskc_intr() drain spin with the serializer held and the IRQ never re-enabled

Summary

mskc_handle_events clears HW_OWNER in a status LE and then, when the LE's port bit names a port with no attached interface (msk_if[1]==NULL on every single-port adapter), executes 'continue', which skips the loop's ONLY MSK_INC (consumer advance). The next iteration re-reads the same LE, sees HW_OWNER clear and breaks; msk_stat_cons never advances, the function returns non-zero forever and mskc_intr's 'while (mskc_handle_events(sc) != 0)' spins permanently - with the serializer held (IRQ muxed through it) and before the interrupt is re-enabled: hard system hang, both ports wedged. The driver itself anticipates this input ('invalid port opcode' printf) but the error handling hangs the box instead of skipping the LE. PROVEN on the guest via a deterministic loop-shape replica: shipped shape 'SPINNING outer=88046829568 stat_cons=0 idx=5 (no progress)' - 88 BILLION zero-progress iterations at ~670M/s, ssh dead; fix-shape control returns cleanly with the consumer advanced past the bad LE. A faulty or compromised Yukon II (or PCIe-level DMA corruption) writing one status LE with the port bit naming an unattached port wedges the ISR permanently.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3807 Β· 11 files
FileTypeDescriptionSize
msk_hevent_replica.c β€” 9.0 KB view raw
Makefile β€” 76 B ↓ download
build.sh β€” 80 B view raw
run.sh β€” 276 B view raw
build.log β€” 619 B view raw
run.log β€” 1.0 KB view raw
serial_console.txt β€” 909 B view raw
env.txt β€” 638 B view raw
VERDICT.md β€” 4.7 KB ↓ raw
fix.diff β€” 496 B view raw
verdict.json β€” 6.4 KB view raw
VERDICT.md
↓ download raw

DF-3807 β€” mskc_handle_events() infinite drain on a status LE naming an unattached port

Verdict

Mechanism PROVEN on the guest kernel by a deterministic replica of if_msk.c's exact mskc_handle_events()/mskc_intr() drain loop over a synthetic 2048-entry status ring; msk-specific runtime hardware-gated (the port bit is written by the NIC's status unit β€” needs faulty/hostile Yukon hardware or errant DMA). Severity Medium, confidence certain.

Root cause (path:line)

mskc_handle_events() (sys/dev/netif/msk/if_msk.c:3137-3238) walks the DMA'd status ring from sc->msk_stat_cons to the device-supplied STAT_PUT_IDX:

3153:   for (cons = sc->msk_stat_cons; cons != idx;) {
3155:       control = le32toh(sd->msk_control);
3156:       if ((control & HW_OWNER) == 0)
3157:           break;
3165:       control &= ~HW_OWNER;
3166:       sd->msk_control = htole32(control);   /* LE released */
3169:       port = (control >> 16) & 0x01;
3170:       sc_if = sc->msk_if[port];
3171:       if (sc_if == NULL) {
3172:           device_printf(sc->msk_dev, "invalid port opcode "
3173:               "0x%08x\n", control & STLE_OP_MASK);
3174:           continue;                         /* <-- BUG */
3175:       }
...
3224:       MSK_INC(cons, MSK_STAT_RING_CNT);      /* ONLY advance */

The continue at :3174 skips the loop's only consumer-index advance (MSK_INC at :3224). For a status LE whose port bit names a port with no interface attached β€” msk_if[1] == NULL on every single-port adapter (only attached ports are registered, if_msk.c:1383, children created at :1699-1719; a failed port attach also NULLs the slot, :1469) β€” the first pass releases the LE (HW_OWNER cleared) and continues with cons unchanged; the second pass re-reads the same LE, sees HW_OWNER clear and breaks. msk_stat_cons never advances while STAT_PUT_IDX is past it, so mskc_handle_events() (:3237 return (cons != STAT_PUT_IDX)) returns non-zero forever, and mskc_intr()'s outer drain

3291:   while (mskc_handle_events(sc) != 0)
3292:       ;

spins permanently β€” with sc->msk_serializer held (the IRQ is muxed through it, if_msk.c:1729-1731) and before the interrupt is re-enabled (B0_Y2_SP_ICR write at :3297): hard system hang, both ports of the controller wedged, the interrupt never completes. The driver itself believes this input is possible β€” it prints "invalid port opcode" β€” but the handling hangs the box instead of skipping the LE.

Note the process-limit bound (rxprog > sc->msk_process_limit, :3225) also sits after the skipped increment, so it never guards this path either; and even without the NULL-port bug, the outer while re-enters mskc_handle_events after a process-limit break, making the limit a per-chunk rather than a per-interrupt bound (drain-to-empty design shared with upstream FreeBSD β€” noted, not filed).

Guest proof (deterministic replica, exact loop shape)

msk_hevent_replica.ko (fake msk_stat_desc ring of MSK_STAT_RING_CNT=2048 entries, STAT_PUT_IDX=5, LE0 = HW_OWNER | port 1 where msk_if[1] == NULL, LE1..4 ordinary):

  • fix-shape control (MSK_INC in the NULL-port branch = fix.diff): mskrep2[fixed]: drain returned 0 after 0 outer iterations, stat_cons=5 idx=5 rx_frames=4 β€” interrupt re-enabled (B0_Y2_SP_ICR), ISR RETURNS
  • shipped shape: SPINNING outer=67108864 … outer=88046829568 stat_cons=0 idx=5 (no progress) β€” 88 billion drain iterations, consumer index stuck at 0, ~670M iterations/s, zero progress; the guest became unusable to ssh from the kernel-priority spin (no panic β€” pure permanent spin/starvation). Killed only via the module's documented instrumentation escape; the real driver has none.

Files: run.log (session transcript incl. the fixed-shape control), serial_console.txt (decisive console sequence + spin-marker count + no-panic check), build.log, msk_hevent_replica.c, fix.diff, env.txt.

Fix

fix.diff: advance the consumer past the offending LE before continue (MSK_INC(cons, MSK_STAT_RING_CNT) in the NULL-port branch). Mechanism validated live (fixed-shape control returns, consumer advances, ISR completes). Driver-level validation needs Yukon hardware β†’ fix_status=not_testable.

Kernel references

sys/dev/netif/msk/if_msk.c:1699-1719 (children only for attached ports), :1383, :1469, :1729-1731 (IRQ muxed through serializer), :3137-3238 (:3153, :3156-3157, :3165-3166, :3169-3174, :3224, :3225, :3229, :3237), :3291-3297.

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
baseline no→ patch + rebuild →patched clean

fix.diff (git apply --check clean) adds MSK_INC(cons, MSK_STAT_RING_CNT) to the NULL-port branch so the drain always makes progress. The mechanism of the fix is validated live on the guest: the replica's mode=1 control (identical loop with the increment) shows the drain returning 0, the consumer index advancing past the bad LE (stat_cons 0 -> 5, 4 good frames processed) and the 'interrupt' completing. Full driver-level validation is not possible on a guest without Marvell Yukon hardware.

fix.diff; run.log fixed-mode section ('mskrep2[fixed]: drain returned 0 after 0 outer iterations, stat_cons=5 idx=5 rx_frames=4 - interrupt re-enabled (B0_Y2_SP_ICR), ISR RETURNS')
↓ fix.diffper-fix-DF-3807

Confirmed kernel references

Detail

Exploit chain

A faulty or compromised Yukon II (or PCIe DMA corruption) that writes one status LE with the port bit naming an unattached port wedges the interrupt handler permanently: the drain spins forever with the driver serializer held, the IRQ is never re-enabled, both ports of the controller and every serializer-sharing path die, and the spinning ISR context starves the machine. No privilege-escalation primitive; impact is a hard, unrecoverable system hang (DoS) from device-trust input.

Evidence (decisive lines)

run.log (session transcript: fix-shape control 'mskrep2[fixed]: drain returned 0 after 0 outer iterations, stat_cons=5 idx=5 rx_frames=4 ... ISR RETURNS', then shipped shape SPINNING markers with growing outer counts and stat_cons=0); serial_console.txt (boot.log excerpt: decisive sequence at lines 204-208, 1247+ SPINNING markers, outer up to 88,046,829,568, panic check = 0/none); build.log; env.txt; VERDICT.md (path:line trace); msk_hevent_replica.c; fix.diff (MSK_INC in the NULL-port branch)

PoC changes

No seed PoC. Authored msk_hevent_replica.ko (out-of-tree kmodule replicating the verbatim mskc_handle_events loop shape + mskc_intr's outer drain over a fake msk_stat_desc ring of MSK_STAT_RING_CNT=2048 entries) because the guest has no Marvell hardware. Documented instrumentation: outer-loop kill flag (module unload escape the real driver lacks), first-error-only printf (console-flood guard), progress kprintf every 2^26 iterations. Build iteration 1 failed -Werror=format= (missing kprintf argument); final build clean.

Verified recommended fix

Advance the consumer index past the offending LE before continuing: add MSK_INC(cons, MSK_STAT_RING_CNT) to the sc_if == NULL branch of mskc_handle_events() (see fix.diff).

Verdict

Mechanism PROVEN on the guest kernel by deterministic replica of if_msk.c's exact mskc_handle_events()/mskc_intr() drain loop over a synthetic 2048-entry status LE ring; msk-specific runtime hardware-gated (inconclusive per family convention). mskc_handle_events (if_msk.c:3137-3238) clears HW_OWNER in a status LE (:3165-3166) and then, when the LE's port bit names a port with no attached interface (single-port adapters: children are created only for attached ports, :1699-1719, registered at :1383; a failed port attach also NULLs the slot, :1469), executes 'continue' (:3174) which skips the loop's ONLY consumer-index advance (MSK_INC at :3224). The next iteration re-reads the same LE, sees HW_OWNER clear and breaks (:3156-3157); msk_stat_cons never advances while STAT_PUT_IDX is past it, so the function returns non-zero forever (:3237) and mskc_intr's outer drain 'while (mskc_handle_events(sc) != 0);' (:3291) spins permanently with sc->msk_serializer held (the IRQ is muxed through it, :1729-1731) and before the interrupt is re-enabled (B0_Y2_SP_ICR write at :3297) - hard system hang, both ports wedged. The driver itself anticipates this input ('invalid port opcode' printf at :3172) but the error handling hangs the box instead of skipping the LE; the rxprog/process_limit bound (:3225) is also skipped and cannot guard it. Replica on the guest (msk_hevent_replica.ko, verbatim loop shape, LE0 = HW_OWNER|port-1, STAT_PUT_IDX=5, port-0 entries after): shipped shape -> drain NEVER returned - 88,046,829,568 outer iterations at ~670M/s with stat_cons=0 idx=5 (no progress), no panic, pure permanent spin that starved userland (ssh dead; serial_console.txt, 1247+ SPINNING markers); fix-shape control (MSK_INC in the NULL-port branch = fix.diff) -> 'drain returned 0 ... stat_cons=5 idx=5 rx_frames=4 - interrupt re-enabled (B0_Y2_SP_ICR), ISR RETURNS'. The real trigger requires the NIC's status unit to emit a port-1 LE on a single-port system - faulty/hostile Yukon II or errant DMA, not packet-derived - hardware-gated on this guest (NO_MARVELL_PCI, vtnet0 lo0; src md5 == audited tree).