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

vpoio: uninterruptible 500K/5M-iteration handshake spins (VP0_FAST_SPINTMO/VP0_LOW_SPINTMO) execute under vpo_intr's hard critical section β€” a malicious parallel-port ZIP emulator pins a CPU with interrupts masked for seconds per I/O (DF-4618 family, vpoio.c instance)

Summary

Every device wait is a raw MS_BRSET/MS_DBRA microseq spin (no-sleep port-I/O) with budgets of 500K/5M plus 5000 select iterations; all of it runs inside vpo_intr's crit_enter - worst case ~13M port-I/O iterations in ONE critical section against a device that withholds nBUSY: tens of seconds with preemption disabled and interrupts deferred, unkillable, repeatable per I/O. The interruptible ppb_poll_device path is stubbed '#if 0 broken'. Malicious-emulator/faulty-cable model. No ppbus on guest. Cut-ceilings (short-term) + drop-crit/interruptible-wait (proper) fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4637 Β· 7 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
trigger.sh β€” 934 B view raw
fix.diff β€” 988 B view raw
env.txt β€” 897 B view raw
verdict.json β€” 2.8 KB view raw
manifest.json β€” 1.0 KB view raw

DF-4637 β€” vpoio: uninterruptible 500K/5M-iteration handshake spins run under vpo_intr's hard critical section

File: sys/dev/disk/vpo/vpoio.c (pass 2, GLM 5.3) Family: DF-4618 (immio.c instance, just filed) β€” this is the vpoio.c instance of the same family; distinct code, distinct spin sites, shared caller crit_enter() wrapper.

Reproduce (requires hardware the audit guest does not have)

Build: nothing to build β€” the trigger is ordinary disk I/O.

Run (on a machine with a real or emulated parallel port, ppc attached, kldload vpo, a ZIP drive behind it enumerating as /dev/daN):

# Malicious ZIP emulator: attach a parallel-port device emulator that answers
# the VP0 select/connect handshakes but then simply never asserts nBUSY during
# the data/status phases.
dd if=/dev/da1 of=/dev/null bs=512 count=1     # any I/O works

Expected (per source trace): each command phase wait spins up to 500,000 port reads (VP0_FAST_SPINTMO), each completion-phase wait up to 5,000,000 (VP0_LOW_SPINTMO), all inside crit_enter()/crit_exit() (vpo.c:215/323) β€” the issuing CPU runs with preemption disabled and interrupts deferred for the full spin (order seconds per I/O on LPC-class port I/O), unkillable, repeatable per command. Watchdog/soft-lockup messages, missed timers, network RX drops on that CPU.

Status

untested β€” the audit guest has no parallel-port hardware at all (dmesg/pciconf empty for ppc/ppbus/lpt; vpo.ko is present but cannot attach). Mechanism is source-certain: see VERDICT.md for the line-by-line trace. Family convention: DF-4618 / DF-4508 (hardware-gated, untested, attempts 0).

VERDICT.md
↓ download raw

DF-4637 VERDICT β€” vpio handshake spins under vpo_intr's critical section

Status: untested (hardware-gated; no ppbus/ppc/lpt on the audit guest β€” env.txt). Mechanism: source-certain. Confidence: likely (same as DF-4618: the code path is unambiguous; the exploit premise β€” a malicious parallel-port ZIP emulator β€” is the driver's stated threat model and cannot be simulated on the QEMU guest).

Root cause (line-by-line)

vpoio_do_scsi() executes the entire SCSI-ish transaction synchronously in the caller's thread. Its callers in vpo.c wrap it in a hard critical section:

Inside vpoio_do_scsi() every device handshake is a pure busy-wait microsequence with no sleep, no yield, no interrupt escape:

  • sys/dev/disk/vpo/vpoio.c:59 β€” VP0_FAST_SPINTMO 500000 iterations
  • sys/dev/disk/vpo/vpoio.c:60 β€” VP0_LOW_SPINTMO 5000000 iterations
  • vpoio_wait() (vpoio.c:525-558) executes wait_microseq (vpoio.c:103-113): MS_SET(tmo); loop: MS_BRSET(nBUSY, ready) + MS_DBRA(-2). In the ppbus engine (sys/dev/misc/ppc/ppc.c):
  • MS_OP_BRSET (ppc.c:1453-1458) reads the status register each iteration (r_str β†’ bus_space_read_1),
  • MS_OP_DBRA (ppc.c:1447-1451) decrements ppc->ppc_accum and branches back. That is while (--tmo > 0) { if (status & nBUSY) break; } β€” nothing else.

Spin call sites inside vpoio_do_scsi, all reached under vpo_intr's crit:

  • vpoio.c:705 β†’ vpoio_select() β€” select wait, VP0_SELTMO=5000 (vpoio.c:498-500)
  • vpoio.c:716 β€” command phase, VP0_FAST_SPINTMO per CDB byte (≀16 per command)
  • vpoio.c:733 β€” data/completion phase, VP0_LOW_SPINTMO (5,000,000 iterations)
  • vpoio.c:774 β€” status phase, VP0_FAST_SPINTMO

Worst case per single I/O against a device that never asserts nBUSY: 5000 (select) + 16Γ—500,000 (command) + 5,000,000 (data) β‰ˆ 13M port-I/O iterations under one crit_enter(), plus a possible full second round for autosense (vpo.c:278-284). At ~0.5–2 Β΅s per LPC port read that is tens of seconds per I/O with preemption disabled and interrupts deferred on the issuing CPU. The wait loop in vpoio.c:533-538 (#if 0 /* broken */) shows an interruptible ppb_poll_device() variant was attempted and abandoned.

Threat model / reachability

  • Stated threat: malicious parallel-port ZIP emulator (device-side attacker). It only has to withhold nBUSY β€” no protocol violation needed. Every issued command (any user I/O to the vpo-backed da device β€” raw device nodes are group-readable by operator; plus every scheduled backup/scan as root) pays the full spin.
  • Weaker variant: a faulty cable/dongle or a wedged drive produces the same stall.
  • CAM serializes commands (VP0_OPENNINGS=1, devq maxopenings=1, vpo.c:151/156), so this is a per-I/O livelock, not a concurrency bug β€” it is the duration under crit_enter() that makes it a hard stall (interrupt blackout), not a race.

Why not higher severity

Requires physically-attached malicious hardware (or the same class of HW fault) β€” same reasoning as DF-4618 (Low) and the ig_hal stuck-semaphore family (DF-4306/DF-4508, Low). Availability impact only.

Fix

Short-term (this pack's fix.diff): cut the spin ceilings 10x as the family mitigation. Proper fix: remove the crit_enter() around the synchronous transaction in vpo_intr (vpo.c) β€” the ppbus ownership lock already serializes the bus β€” and/or port vpoio_wait() to the interruptible ppb_poll_device() path the author stubbed at vpoio.c:533-538.

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

not_testable: no parallel-port hardware on guest to baseline or patch-test; fix.diff is the family's short-term 10x ceiling cut, mirrors DF-4618 guidance, trivially reviewable

fix.diff
↓ fix.diffper-fix-DF-4637

Confirmed kernel references

Detail

Exploit chain

[]

Evidence (decisive lines)

VERDICT.md (line-by-line trace: vpo.c:215 crit_enter; vpoio.c:59-60 ceilings; vpoio.c:103-113 wait_microseq; ppc.c:1447-1451 DBRA + 1453-1458 BRSET = raw spin; call sites vpoio.c:716/733/774); trigger.sh documents the hardware trigger; env.txt documents guest hardware absence; fix.diff = 10x ceiling cut (short-term family mitigation)

PoC changes

n/a β€” hardware-gated; trigger.sh documents the setup, never executed on guest

Verified recommended fix

Short-term: cut spin ceilings 10x (fix.diff). Proper: remove crit_enter/crit_exit around the synchronous transaction in vpo_intr (vpo.c) β€” ppbus ownership already serializes β€” and/or make vpoio_wait() interruptible via ppb_poll_device() (stub at vpoio.c:533-538).

Verdict

not_run: no ppbus/ppc/lpt hardware on the audit guest (env.txt: dmesg/pciconf empty, vpo.ko present but unattachable); mechanism source-certain β€” vpoio_do_scsi's wait/select handshakes are MS_BRSET/MS_DBRA no-sleep port-I/O spins (ppc.c:1447-1458) with budgets of 500000/5000000 iterations (vpoio.c:59-60), executed at vpoio.c:705/716/733/774 entirely inside vpo_intr's crit_enter/crit_exit (vpo.c:215/323, autosense round vpo.c:278-284 included). Worst case ~13M status-register polls per command under one critical section. Follows family convention of DF-4618/DF-4508.