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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2222 Β· 6 files
FileTypeDescriptionSize
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
VERDICT.md verdict gate analysis + ERESTART/EINTR loop-spin trace
↓ download 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.

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_testable
baseline no→ patch + rebuild →patched clean

not_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).
↓ fix.diffn/a (no target HW/device on this guest)

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.