# DF-2222 — wait_for_completion_interruptible spins forever on signal (drm/linux_completion)

## Verdict: NOT REPRODUCED (HW-gated); REAL BUG IN SOURCE (defense-in-depth fix warranted)

## Hardware gate (why the PoC cannot run on this guest)

The triggering ioctls (`DRM_IOCTL_MODE_ATOMIC`, i915 PSR) require a DRM/KMS GPU
with an attached `/dev/dri/card0`. The audit QEMU/KVM guest has only the
framebuffer-style `vgapci0` (class 0x030000, Bochs/QEMU std VGA chip 0x11111234)
and **no DRM driver attaches**:

```
$ pciconf -l | grep vga          # vgapci0@pci0:0:2:0: class=0x030000 ... chip=0x11111234
$ ls /dev/dri* 2>&1              # /dev/drm* /dev/dri*: No such file or directory
$ ls /dev/agpgart 2>&1           # No such file or directory
$ kldstat                        # only kernel + ehci.ko + xhci.ko (no drm/i915/amdgpu)
```

With no `/dev/dri/card0`, the `DRM_IOCTL_MODE_ATOMIC` and i915 PSR paths that
reach `wait_for_completion_interruptible()` are unreachable by any user. The
unprivileged `maxx` user cannot open a non-existent device. The bug is also
exercisable in principle from any in-tree DRM driver (the function is generic
Linux-completion glue), but none of those drivers have a device to attach on
this guest.

## Source trace — the bug is REAL (sys/dev/drm/include/linux/completion.h)

`__wait_for_completion_generic()` (`completion.h:78-116`):
```c
lockmgr(&c->wait.lock, LK_EXCLUSIVE);
while (c->done == 0 && !timeout_expired) {
    ret = lksleep(&c->wait, &c->wait.lock, flags, "lwfcg", timeout);
    switch(ret) {
    case EWOULDBLOCK:
        timeout_expired = true;
        ret = 0;
        break;
    case ERESTART:                  /* signal pending (PCATCH) */
        ret = -ERESTARTSYS;
        break;                      /* breaks the SWITCH only, NOT the while */
    case 0:
        awakened = true;
        break;
    /* EINTR: no case at all -> ret stays stale, loop continues */
    }
}
```

On `ERESTART` (signal arrived during a `PCATCH` sleep) the code sets
`ret = -ERESTARTSYS` but the `break` exits only the `switch`; the `while`
predicate (`c->done == 0 && !timeout_expired`) is unchanged, so the loop
re-enters `lksleep`. Because `tsleep()` with `PCATCH` returns *immediately*
without blocking whenever a signal is pending (`kern_synch.c` `CURSIG` path does
not consume the signal), the loop becomes a tight, CPU-bound kernel spin. The
process cannot be interrupted by SIGTERM/SIGINT/SIGKILL (deferred to a user
boundary that is never reached). `EINTR` has no case at all, so `ret` is stale
and the loop likewise spins. (The wrapper at `linux_completion.c:38-46` only
maps the final `ret`; it cannot break the inner loop.)

Secondary issue: `awakened` is set `true` on a `case 0` and never reset, so a
spurious-wakeup-then-real-timeout iteration falsely reports `remaining_jiffies`
as success.

The claim's reachability via `DRM_IOCTL_MODE_ATOMIC`
(`drm_atomic_helper.c:2627/2638/2649`) and i915 PSR (`intel_psr.c:869`) is
accurate for any system with a DRM driver attached.

## Exploit chain status

Not pursuable — primitive (uninterruptible CPU-bound spin / unkillable thread,
a local DoS holding DRM/GEM resources) is behind absent DRM hardware (valid
Phase-6 hard blocker: dead path at runtime, no harness on this guest). On a
DRM-equipped host the unprivileged-video-group trigger is real.

## PoC changes

None. `/dev/dri/card0` absent; verified by source trace only.

## Recommended fix

On `ERESTART`/`EINTR`, break out of the `while` loop (not just the `switch`),
and handle `EINTR`. See `fix.diff` (supersedes finding proposal by also handling
the missing `EINTR` case and the sticky-`awakened` false-success).
