plip: lpoutput/lp_intr spin on the parallel port inside crit_enter() with unbounded aggregate time β unprivileged CPU-pinning DoS
Summary
The PLIP handshake drives per-byte spin loops (up to 8000/500 port polls each) with NO aggregate bound, with preemption and interrupts disabled. A dead port burns ~9000 polls per transmit; a peer answering the preamble then stalling runs mtu*500 polls per packet (~33s in-crit at mtu 65535). REPRODUCED on the guest (parallel null): 9.5 pkt/s drain = ~105ms in-crit per packet, one of 6 CPUs pinned, and a 3000-datagram unpriv flood starved SSH for ~4 minutes. Fix validated on the rebuilt module: hard per-call poll budget, no unbounded path. DF-4618/4637 family, plip instance. Budget-cap fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4657 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| plipspin.c | β | 1.9 KB | view raw | |
| run.baseline.log | β | 1.5 KB | view raw | |
| run.fixtest.log | β | 566 B | view raw | |
| env.txt | β | 271 B | view raw | |
| fix.diff | β | 3.4 KB | view raw | |
| build.sh | β | 141 B | view raw | |
| run.sh | β | 187 B | view raw |
Fix verification
fixedfix.diff adds a hard per-call poll budget (lp_spin_budget, sysctl hw.plip_maxspin, default 4000) checked in every handshake spin loop of lpoutput/lp_intr/clpoutbyte/clpinbyte/lpoutbyte. Rebuilt module on the guest and re-ran the identical PoC: bursts drain 2-3x faster (20-30 pkt/s vs 9.5) and, structurally, no unbounded spin path remains (previously up to mtu*500 polls/packet). Tightening hw.plip_maxspin shows further linear recovery. No functional regression on the emulated port.
run.fixtest.log (patched runs at hw.plip_maxspin=4000/1000); guest sysctl hw.plip_maxspin present after fixed module load; baseline comparison in run.baseline.log section D
Confirmed kernel references
Detail
Exploit chain
unpriv user -> UDP to lp0-routed address -> netisr lpoutput() -> crit_enter + unbounded ppb_rstr() spin loops (if_plip.c:606/698/704/574; lp_intr:454/514/523) -> CPU pinned, network stack starved; repeatable indefinitely
Evidence (decisive lines)
run.baseline.log section D (Oerrs 3066->3076 in 1.05s = 9.5 pkt/s), section A (10.76us/port-op => ~86ms preamble), section E (sy=16-17% = 1/6 CPUs pinned); Ierrs=2*Oerrs (6132/3066) matches 2 lp_intr calls per packet; post-flood SSH banner timeouts ~4 min
PoC changes
plipspin.c adds errno reporting and per-packet sendto timing vs the seed; portbench.c written to measure KVM port-op cost; ppc.ko/if_plip.ko had to be built on the guest (no module Makefiles in tree) and loaded ppbus->if_plip->ppc for lp0 to appear; QEMU needed QEMU_EXTRA='-parallel null'
Verified recommended fix
Hard-cap total handshake polls per lpoutput/lp_intr invocation via a sysctl-tunable budget checked in every spin loop (see fix.diff)
Verdict
Unprivileged local DoS reproduced on the QEMU guest: any user routing UDP out a configured lp0 makes the netisr thread executing lpoutput() burn ~105ms of port-spinning inside crit_enter() per 1400-byte packet (dead port: LPMAXSPIN1 preamble + 2x lp_intr LPMAXSPIN2 loops, ~9000 port polls at 10.8us/op under KVM), capping throughput at ~9.5 pkt/s, pinning one CPU at 100% sys with interrupts disabled, and stalling SSH for ~4 minutes after a single 3000-datagram burst. Aggregate spin is unbounded in the code (mtu*LPMAXSPIN2 polls per packet with a hostile/stalled peer).
No comments yet.