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)
PoC verification
Evidence pack
findings/poc/DF-3807 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_INCin 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_testablefix.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')
Confirmed kernel references
- sys/dev/netif/msk/if_msk.c:1699
- sys/dev/netif/msk/if_msk.c:1383
- sys/dev/netif/msk/if_msk.c:1469
- sys/dev/netif/msk/if_msk.c:1729
- sys/dev/netif/msk/if_msk.c:3137
- sys/dev/netif/msk/if_msk.c:3156
- sys/dev/netif/msk/if_msk.c:3165
- sys/dev/netif/msk/if_msk.c:3174
- sys/dev/netif/msk/if_msk.c:3224
- sys/dev/netif/msk/if_msk.c:3225
- sys/dev/netif/msk/if_msk.c:3237
- sys/dev/netif/msk/if_msk.c:3291
- sys/dev/netif/msk/if_msk.c:3297
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).
No comments yet.