wait_for_completion_interruptible spins forever on signal (no break on ERESTART/EINTR) unkillable CPU-bound local DoS
Summary
wait_for_completion_interruptible() documented to return -ERESTARTSYS when interrupted by signal but __wait_for_completion_generic() (completion.h:78-116) never breaks out of wait loop on signal. On ERESTART sets ret=-ERESTARTSYS but break only breaks switch not while loop control returns to while predicate c->done still 0 timeout_expired still false loop re-enters lksleep. tsleep() with PCATCH returns immediately without blocking whenever signal pending (kern_synch.c:627-628 goto resume CURSIG does not consume signal) so loop becomes tight CPU-bound kernel spin. Process cannot be interrupted by signals including SIGTERM/SIGINT/SIGKILL deferred to user boundary never reached. On EINTR no case in switch at all ret left stale loop also spins. Reachable via DRM_IOCTL_MODE_ATOMIC (drm_atomic_helper.c:2627/2638/2649 wait_for_completion_interruptible on every atomic commit/page-flip/modeset) and i915/intel_psr.c:869. Attacker: any local user with /dev/dri/card0 access (group video or render node user). Trigger: issue atomic commit then immediately raise signal (kill/INT/setitimer SIGALRM). Impact: calling thread enters uninterruptible CPU-bound kernel spin signals including SIGKILL cannot be delivered process unkillable repeating across N CPUs pins all DRM/GEM resources held cannot be reclaimed. Also: awakened flag sticky across iterations not reset on later EWOULDBLOCK wait experiencing spurious wakeup then real timeout falsely reports success.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2222 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | gate analysis + ERESTART/EINTR loop-spin trace | 3.6 KB | β raw |
| fix.diff | suggested-fix | break out of while on ERESTART/EINTR | 416 B | view raw |
| build.sh | build-script | documents HW gate | 149 B | view raw |
| run.sh | run-script | prints gate proof | 372 B | view raw |
| env.txt | environment | guest env | 1.1 KB | view raw |
| gate_proof.txt | gate-proof | no /dev/dri*, only std vgapci | 219 B | view raw |
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):
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).
Fix verification
not_testablenot_testable: PoC cannot run on this guest (HW-gated, no target device). fix.diff validated by git apply --check (clean) + line-accurate source trace confirming it closes the cited path.
git apply --check findings/poc/DF-2222/fix.diff -> OK (clean apply). No runtime test possible (HW-gated).
Confirmed kernel references
Detail
Exploit chain
none β valid hard blocker (driver/device path dead at runtime on this guest: no target HW / no attached device). No unprivileged->root path.
Evidence (decisive lines)
usbconfig list -> No device match or lack of permissions.; pciconf -l -> no target controller HW; ifconfig -l -> vtnet0 lo0; kldstat -> kernel/ehci/xhci only; ls /dev/<target> -> No such file or directory; id maxx -> uid=1001 groups=1001 (not operator). Source confirmed at cited lines.
PoC changes
Created findings/poc/DF-2222/{VERDICT.md,fix.diff,build.sh,run.sh,env.txt,gate_proof.txt,manifest.json}. No PoC source (HW-gated).
Verified recommended fix
on ERESTART/EINTR set timeout_expired=true to exit while. Full git-apply-able diff in findings/poc/DF-2222/fix.diff (git apply --check OK).
Verdict
NOT REPRODUCED (HW-gated). The bug is REAL in source (traced line-by-line). drm/linux_completion wait_for_completion_interruptible infinite spin (ERESTART break exits switch not while); no /dev/dri. Gate confirmed via usbconfig list (No device match / no /dev/ugen*), pciconf -l (no target controller HW), ifconfig (vtnet0 lo0 only), kldstat (no target module), and ls /dev (no target nodes). maxx (uid 1001, not in operator) cannot reach any /dev/usbctl write path.
No comments yet.