wait_for_completion()/wait_for_completion_interruptible(): MAX_SCHEDULE_TIMEOUT (LONG_MAX) truncates to int -1 in lksleep() β every 'infinite' completion wait expires after one tick with spurious success
Summary
The no-timeout entry points pass LONG_MAX through unsigned long into lksleep's int timo -> -1 -> _callout_setup_quick clamps to next tick -> EWOULDBLOCK after ~10ms -> loop maps to ret=0 SUCCESS before the completion fires. Guest-verified on the exact drm_atomic_helper.c:2627 entry point (ret=0 after 1 tick, completer 500 ticks out, 2 runs; verbatim linux_completion.c linked into KLD); fix A/B validated (499-tick block restored; schedule_timeout's INT_MAX->0 conversion is the in-tree precedent). In-tree consumers drm_atomic_helper.c:2627/2638/2649 + i915 intel_psr.c:869 are hw-gated - latent trap, driver acts on unquiesced hw state. DISTINCT from DF-2222 (signal-spin; this is the truncation at completion.h:90 - opposite failure mode, independent fix).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5235 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| compbug.c | β | 4.7 KB | view raw | |
| Makefile | β | 669 B | β download | |
| fix.diff | β | 1011 B | view raw | |
| capture.sh | β | 848 B | view raw | |
| build.log | β | 7.5 KB | view raw | |
| build-fixed.log | β | 7.5 KB | view raw | |
| run.log | β | 657 B | view raw | |
| run.2.log | β | 768 B | view raw | |
| run-fixed.log | β | 488 B | view raw | |
| console.txt | β | 3.1 KB | view raw | |
| env.txt | β | 495 B | view raw | |
| SHA256SUMS | β | 357 B | β download | |
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-5235 β MAX_SCHEDULE_TIMEOUT truncates to -1: "infinite" completion waits expire after one tick
What this is
PoC + fix validation for the LinuxKPI completion shim
(sys/dev/drm/linux_completion.c +
sys/dev/drm/include/linux/completion.h).
__wait_for_completion_generic() passes its unsigned long timeout
argument straight into lksleep()'s int timo parameter. The
no-timeout entry points (wait_for_completion(),
wait_for_completion_interruptible()) pass MAX_SCHEDULE_TIMEOUT
(LONG_MAX), which on x86_64 truncates to timo = -1.
tsleep() treats any nonzero timo as "arm a callout", and
_callout_setup_quick() clamps a non-positive tick delta to the
next tick β so every "wait forever" completion wait returns
after ~1 tick (10 ms at hz=100), with success, before the
completion ever fired.
Distinct from DF-2222 (same file): DF-2222 is the missing loop break on ERESTART (signal β busy-spin, unkillable). This finding is the timeout truncation (no signal β premature EWOULDBLOCK β spurious success). Opposite failure mode, different code, different fix.
Files
compbug.cβ kernel-module PoC (self-contained, MOD_LOAD runs the test)Makefileβ kmod build against the guest's live /usr/srclinux_completion.cβ NOT in the pack; copied verbatim at build time from/usr/src/sys/dev/drm/linux_completion.c(sha256477bdd88e916bf95466e92fad95bff4355cfd0e719efa2b8c742f6aa23fd8a6e, identical to the audit tree) β the real upstream entry points are tested.linux_wait.cβ likewise copied verbatim (only for__init_waitqueue_head; its wait_event-family helpers are dead code here, never executed on the completion path)fix.diffβ git-apply-able fix (validated below)build.sh,run.sh,capture.shβ exact commandsbuild.logβ baseline (unpatched) build, fullbuild-fixed.logβ build after applying fix.diff to the guest's /usr/srcrun.log,run.2.logβ two baseline runs (unpatched): BUG both timesrun-fixed.logβ run after fix: waits block until the real completionconsole.txtβ serial console history of every load (A/B/A story)env.txtβ guest uname/hz/compiler/kldstatSHA256SUMSβ guest-side hashes of sources + .ko
Build
On the guest (as root), with kernel source at /usr/src:
cd /root/df5235 && make
Run
kldload ./compbug.ko && sleep 13 && dmesg | grep compbug | tail
Expected output
Baseline (unpatched kernel, verified 2 independent loads):
compbug[1] interruptible: ret=0 elapsed=1 ticks (completer due at 500): BUG: returned before completion compbug[2] wait_for_completion: elapsed=1 ticks (completer due at 500): BUG: returned before completion compbug[3] control timeout=10: ret=0 elapsed=9|10 ticks: OK compbug: hz=100, LONG_MAX=9223372036854775807 -> int timo=-1 (LKSLEEP SEES -1)
Case 1 is wait_for_completion_interruptible() β the exact entry point used
by drm_atomic_helper.c:2627,2638,2649 and i915/intel_psr.c:869 β returning
0 (== success in the Linux contract) after one tick although the
completer was scheduled 500 ticks (5 s) out.
Case 3 is the control: a small in-range timeout (10 ticks) behaves exactly
per contract (ret=0 after ~10 ticks), proving the generic timeout machinery
is fine and only the MAX_SCHEDULE_TIMEOUT conversion is broken.
With fix.diff applied to /usr/src (run-fixed.log):
compbug[1] interruptible: ret=0 elapsed=499 ticks (completer due at 500): OK compbug[2] wait_for_completion: elapsed=499 ticks (completer due at 500): OK compbug[3] control timeout=10: ret=0 elapsed=9 ticks: OK
Notes
run.log's first three lines are stale ring-buffer lines from the preceding patched-module load (the load order was patched β baseline); the six decisive baseline lines are the tail.run.2.logandconsole.txtshow the clean sequence.- No panic, no corruption: the demonstrated impact is the broken synchronization primitive itself (spurious early success). In-tree consumers are DRM (i915/amdgpu/radeon), i.e. hardware-gated; on this QEMU guest none probe, hence impact is recorded as "none" with the race consequences described in the finding.
DF-5235 β VERDICT
Status: reproduced (on-guest, two independent baseline loads; fix validated A/B) Impact: none demonstrated on this guest (primitive = broken synchronization: "infinite" kernel completion waits return spurious success after 1 tick; in-tree consumers are DRM drivers, hardware-gated, absent on this guest) Confidence: certain (code defect proven by execution; consumers traced)
Root cause (path:line)
sys/dev/drm/include/linux/completion.h:79-116β__wait_for_completion_generic(struct completion *c, unsigned long timeout, int flags)passestimeoutunchanged tolksleep()at completion.h:90.sys/sys/systm.h:414βint lksleep(..., const char *, int timo): the timeout parameter is an int.sys/dev/drm/include/linux/sched.h:78β#define MAX_SCHEDULE_TIMEOUT LONG_MAX=0x7FFFFFFFFFFFFFFFon x86_64 β truncates totimo = -1(implementation-defined C conversion; gcc/clang modulo β 0xFFFFFFFF).sys/dev/drm/linux_completion.c:33and:40β the two no-timeout entry points passMAX_SCHEDULE_TIMEOUT.sys/kern/kern_synch.c:691-693βif (timo)is TRUE for -1 β_callout_setup_quick(..., timo=-1, endtsleep, td).sys/kern/kern_timeout.c:971-972βc->qtick = sc->curticks + ticks;βcurticks-1;if (c->qtick - sc->softticks <= 0) c->qtick = sc->softticks + 1;β the callout fires on the next tick.endtsleep(kern_synch.c:935) sets TDF_TIMEOUT βtsleepreturnsEWOULDBLOCKβ completion.h:92-95 setstimeout_expired=true, ret=0β loop exits βwait_for_completion_interruptiblemaps that to 0 = success (linux_completion.c:45).
Contrast with the tree's own correct conversion in
sys/dev/drm/include/linux/sched.h:130-133 (schedule_timeout):
timo = timeout >= INT_MAX || timeout == MAX_SCHEDULE_TIMEOUT ? 0 : timeout.
What was executed
Kernel module compbug.ko built against the guest's live /usr/src,
linking the verbatim upstream linux_completion.c
(sha256 477bdd88e916bf95466e92fad95bff4355cfd0e719efa2b8c742f6aa23fd8a6e,
byte-identical to the audit tree β see SHA256SUMS) and the verbatim
linux_wait.c (needed only for __init_waitqueue_head; its wait_event
helpers are never executed on the completion path β linkage stubs satisfy
the current-macro symbols). See compbug.c header comment.
Three cases per load (completer scheduled 500 ticks = 5 s out):
wait_for_completion_interruptible()β elapsed=1 tick, ret=0 β spurious success. This is the exact entry point used bysys/dev/drm/drm_atomic_helper.c:2627,2638,2649(drm_atomic_helper_wait_for_dependencies / wait_for_vblanks family) andsys/dev/drm/i915/intel_psr.c:869(psr_wait_for_idle).wait_for_completion()β elapsed=1 tick (no in-tree caller today; latent trap, e.g. any ported driver that waits indefinitely).- Control
wait_for_completion_timeout(c, 10)β ret=0 after 9-10 ticks: the timeout machinery is correct for in-range values; only the MAX_SCHEDULE_TIMEOUT conversion is broken.
Reproduced twice from independent loads (run.log, run.2.log, full story
in console.txt). hz=100 confirmed (env.txt), so 1 tick = 10 ms.
Why it matters (threat)
Every DRM commit that waits for hw_done/cleanup_done "indefinitely"
proceeds after 10 ms whether or not the hardware/atomic-commit worker has
signaled completion. Linux semantics: block until complete(); return
-ERESTARTSYS on signal. DragonFly actual: return success after 10 ms.
Downstream consequence class: use of an object whose hardware state is not
yet quiesced (plane update on an unflipped CRTC, PSR exit before idle,
cleanup before the last vblank) β racy malfunction to potential UAF on
real GPU hardware. Not triggerable on this guest (QEMU, no i915/amdgpu
device; DRM modules not loaded β env.txt kldstat), hence impact recorded
as "none" rather than dos/corruption. No unprivileged trigger path exists
without the hardware.
Relationship to DF-2222 (do not merge)
DF-2222 = completion.h:91-102 switch never breaks out of the loop on ERESTART β with PCATCH and a pending signal, busy-spin forever. DF-5235 (this) = completion.h:90 passes LONG_MAX into an int β without any signal, premature EWOULDBLOCK after 1 tick β spurious success. With a signal pending, tsleep's CURSIG short-circuit (kern_synch.c:622-630) returns before the callout is armed, so DF-2222 dominates; without a signal, the truncation dominates. Opposite failure modes, independent lines, independent fixes; fixing one does not fix the other.
Fix validation (A/B, same guest, same module)
- Baseline: pristine
/usr/src,makeβkldloadβrun.log/run.2.log: elapsed=1, BUG (both loads). - Apply
fix.diffto the guest's/usr/srccopy (sys/dev/drm/include/linux/completion.h: converttimeout >= INT_MAX β timo 0exactly likeschedule_timeout),make clean && make(build-fixed.log) βkldloadβrun-fixed.log: elapsed=499 ticks, waits block until the real completer fires, ret=0 (true success); control case unchanged (9 ticks). - Reverted the guest's
/usr/srcheader to pristine afterwards (verified:lksleep(&c->wait... timeout)at line 90 again).
fix_status: fixed β baseline behavior (premature return) gone; semantics now match Linux for both entry points; explicit-timeout path untouched.
Kernel references
- sys/dev/drm/include/linux/completion.h:79-116 (helper), :90 (lksleep call)
- sys/dev/drm/linux_completion.c:30-46 (both entry points)
- sys/dev/drm/include/linux/sched.h:78, 130-133 (MAX_SCHEDULE_TIMEOUT, correct conversion)
- sys/sys/systm.h:414 (lksleep int timo)
- sys/kern/kern_synch.c:691-693 (timo!=0 arms callout), :935 (endtsleep)
- sys/kern/kern_timeout.c:940-975 (_callout_setup_quick; clamp at 971-972)
- Consumers: sys/dev/drm/drm_atomic_helper.c:2627,2638,2649; sys/dev/drm/i915/intel_psr.c:869
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
fixedApplied fix.diff to the guest's /usr/src copy of completion.h, rebuilt the same module (build-fixed.log), re-ran the identical PoC: baseline elapsed=1 tick BUG (both loads) -> patched elapsed=499 ticks, waits block until the real completer fires, control case unchanged (9-10 ticks). Premature-return behavior gone; Linux return semantics restored. fix_patched_reproduced=0 means the BUG behavior did NOT reproduce on the patched build (desired outcome).
fix.diff; run.log (baseline BUG); run.2.log (baseline BUG, 2nd load); run-fixed.log (patched OK elapsed=499); build.log vs build-fixed.log; console.txt full history
Confirmed kernel references
- sys/dev/drm/include/linux/completion.h:90
- sys/dev/drm/linux_completion.c:33
- sys/dev/drm/linux_completion.c:40
- sys/dev/drm/include/linux/sched.h:78
- sys/dev/drm/include/linux/sched.h:130
- sys/sys/systm.h:414
- sys/kern/kern_synch.c:692
- sys/kern/kern_timeout.c:971
- sys/dev/drm/drm_atomic_helper.c:2627
- sys/dev/drm/i915/intel_psr.c:869
Detail
Evidence (decisive lines)
run.log + run.2.log: 'compbug[1] interruptible: ret=0 elapsed=1 ticks ... BUG' (baseline, 2 loads); run-fixed.log: 'elapsed=499 ticks ... OK' (after fix.diff); console.txt: full A/B/A load history; env.txt: hz=100, no drm modules loaded; SHA256SUMS: tested linux_completion.c byte-identical (477bdd88...) to the audit tree; compbug.c:34-47 stubs are linkage-only, never executed on the completion path
PoC changes
PoC authored from scratch (no seed): kmod compiling the VERBATIM sys/dev/drm/linux_completion.c (+ linux_wait.c for __init_waitqueue_head, with linkage-only stubs for the never-executed wait_event/current path); needed KCFLAGS + -include kconfig.h per sys/dev/drm/drm/Makefile conventions after plain CFLAGS+= was overridden by bsd.kmod.mk
Verified recommended fix
In __wait_for_completion_generic(), convert the timeout to lksleep's int timo exactly like schedule_timeout(): timo = (timeout >= INT_MAX) ? 0 : (int)timeout; and pass timo to lksleep (see fix.diff).
Verdict
wait_for_completion()/wait_for_completion_interruptible() pass MAX_SCHEDULE_TIMEOUT (LONG_MAX) into lksleep()'s int timo, truncating to -1 on x86_64; _callout_setup_quick() clamps the negative delta to the next tick, so every 'infinite' completion wait returns spurious success after 1 tick (10 ms at hz=100) before the completion fires. Proven on-guest: drm_atomic_helper.c:2627/i915 intel_psr.c:869 entry point returned ret=0 after 1 tick with the completer scheduled 500 ticks out (two independent loads); control with an in-range 10-tick timeout behaves per contract; fix.diff (INT_MAX->timo 0 conversion, same as schedule_timeout) makes both waits block the full 499-500 ticks until the real completer. Demonstrated impact is the broken synchronization primitive itself; in-tree consumers are DRM drivers (hw-gated, absent on this QEMU guest), so no dos/corruption is observable here.
No comments yet.