wait_on_bit_timeout() passes Linux TASK_* state constants as tsleep(9) flags (TASK_INTERRUPTIBLE never maps to PCATCH: interruptible waits are silently unkillable) and truncates the unsigned long timeout to int (MAX_SCHEDULE_TIMEOUT β -1 β next-tick callout: 'infinite' waits are 1 Hz polling loops)
Summary
TASK_INTERRUPTIBLE=1 never maps to PCATCH=0x100 (unkillable 'interruptible' waits) and the low bits land in PWAKEUP_CPUMASK; MAX_SCHEDULE_TIMEOUT truncates to int -1 which the callout clamps to next-tick - 'infinite' waits poll at 1Hz. Deterministic guest repro (EWOULDBLOCK after 2 ticks, no waker; poll-driven exit at 396 ticks). Translate-mode + clamp-timeout fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5183 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| lwqdemo.c | β | 12.1 KB | view raw | |
| Makefile | β | 179 B | β download | |
| build.log | β | 6.6 KB | view raw | |
| run.log | β | 3.3 KB | view raw | |
| env.txt | β | 228 B | view raw | |
| fix.diff | β | 1008 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 3.1 KB | view raw |
DF-5183 β wait_on_bit_timeout() misuses the tsleep(9) API: Linux mode constants passed as sleep flags + unsigned-long timeout truncated to int
File: sys/dev/drm/linux_wait.c:104-131 (wait_on_bit_timeout), :141-144 (wait_on_bit)
Severity: Low β broken API contract; latent unkillable waits; 1 Hz polling instead of blocking
Verdict: REPRODUCED (deterministic, tsleep-level, no waker)
Build / Run
Same module and procedure as the DF-5182 pack (this experiment rides in
the same lwqdemo.ko; the relevant output lines are prefixed LWQ-F3:):
cd /root/lwqdemo && make SYSDIR=/usr/src/sys && kldload ./lwqdemo.ko
sleep 8 && dmesg | grep LWQ-F3
Expected output (decisive lines)
LWQ-F3: tsleep(timo=(int)MAX_SCHEDULE_TIMEOUT=-1) returned rv=35 after 2 ticks
with NO waker - 'infinite' timeout actually expires every tick
LWQ-F3: wait_on_bit_timeout(MAX_SCHEDULE_TIMEOUT) on a set bit returned rv=0
after 396 ticks (only exits because we clear the bit below; internally
it polls at 1 tick granularity)
rv=35 is EWOULDBLOCK: with no waker at all, a sleep whose timeout the
caller meant to be infinite (MAX_SCHEDULE_TIMEOUT == LONG_MAX) expires
after ~1 tick.
Why (source chain)
wait_on_bit()(linux_wait.c:141-144) passesMAX_SCHEDULE_TIMEOUT(LONG_MAX) intowait_on_bit_timeout().wait_on_bit_timeout()(linux_wait.c:118) passestimeoutstraight totsleep(word, mode, "lwobt", timeout);tsleep()'stimoparameter isint(kern_synch.c:512), soLONG_MAXtruncates to-1._callout_setup_quick()(kern_timeout.c:940-976) computesqtick = curticks - 1and clamps it tosoftticks + 1β a next-tick callout. Every tsleep iteration therefore returnsEWOULDBLOCKafter one tick: "infinite" waits are 1 Hz polling loops (timer fire + context switch per waiter per tick).mode(TASK_RUNNING=0/TASK_INTERRUPTIBLE=1/TASK_UNINTERRUPTIBLE=2, sched.h:72-74) is also passed as the tsleep flags.PCATCHis 0x100 (param.h:324), soTASK_INTERRUPTIBLEnever requests signal delivery: an interruptible wait is silently uninterruptible (unkillable thread). The remaining low bits land inPWAKEUP_CPUMASK(param.h:327), whichtsleepignores for the sleep itself (td_wdomain = flags & PDOMAIN_MASK, kern_synch.c:448) β inert today, but the values are wrong on their face.
In-tree callers currently pass TASK_UNINTERRUPTIBLE only (i915_irq.c:3200,
i915_irq.c:3341), which is why this is latent rather than an active
defect; the truncation half is active in every wait_on_bit() call.
Fix
fix.diff in this directory: translate mode β PCATCH, clamp the
timeout to 0 (= no timeout) instead of truncating.
Artifacts
Same as the DF-5182 pack (lwqdemo.c, Makefile, build.log,
run.log, env.txt); the LWQ-F3: lines in run.log are this
finding's evidence. Reproduced twice (two guest boots), identical
numbers.
DF-5183 β Verification Verdict
Status: reproduced (deterministic) Impact: dos (low-grade: 1 Hz poll loop per "infinite" waiter; latent unkillable-wait contract violation) Confidence: certain
What was reproduced
On the QEMU/KVM guest (DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC,
6 vCPU, cc 8.3), module lwqdemo.ko (which links the pristine
sys/dev/drm/linux_wait.c):
-
tsleep((void *)&f3_word, 0, "lwqf3", (int)MAX_SCHEDULE_TIMEOUT)with no waker returnedrv=35(EWOULDBLOCK) after 2 ticks (run.log, firstLWQ-F3:line; reproduced on two boots). An "infinite" timeout truncates(unsigned long)LONG_MAX β int β1;_callout_setup_quick()clamps the negative expiry to the next tick (kern_timeout.c:971-972), so the sleep expires immediately. Consequence insidewait_on_bit_timeout(): the do-while loop (linux_wait.c:117-123) re-enters tsleep every tick β a 1 Hz poll loop for everywait_on_bit()caller (i915_irq.c:3341, i915_irq.c:3199) instead of a blocking sleep. -
wait_on_bit_timeout(&f3_word, 0, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT)on a never-cleared bit exited only when the harness cleared the bit 5 s later (rv=0, 396 ticks) β i.e. the function's exit is driven by the bit test, never by the "infinite" timeout bookkeeping, consistent with the polling behavior above.
The modeβflags half of the finding is source-certain rather than
runtime-demonstrated (kernel threads cannot receive signals, so PCATCH
inertness cannot be shown from a KLD): tsleep(word, mode, ...) at
linux_wait.c:118 with mode β {0,1,2,3} never sets PCATCH (0x100,
sys/sys/param.h:324) and puts the low bits into PWAKEUP_CPUMASK,
which tsleep ignores apart from td_wdomain = flags & PDOMAIN_MASK
(kern_synch.c:448). A future caller passing TASK_INTERRUPTIBLE
gets an unkillable uninterruptible sleep β the opposite of the Linux
contract (wait.h/wait_bit.h export this API tree-wide).
Reachability
Kernel-internal API used by i915 reset/handoff paths
(i915_irq.c:3199, i915_irq.c:3341 β active with
TASK_UNINTERRUPTIBLE) and exported to every drm driver via
<linux/wait_bit.h>. No unprivileged direct entry on the QEMU guest
(no drm device); the PoC drives the primitive from a root-loaded KLD.
Fix
fix.diff: translate mode & TASK_INTERRUPTIBLE β PCATCH and clamp
timeout >= INT_MAX || timeout == MAX_SCHEDULE_TIMEOUT to timo = 0
(tsleep's "no timeout"). Mechanical, not kernel-rebuild-validated
(Low-severity logic finding).
Relationship to known findings
DF-2181 owns the broken time_after_eq(start_time, timeout) expiry
test; DF-2182 owns the missing tsleep_interlock in this same
function. This finding is the two other parameter-handling defects at
the same call site (modeβflags mapping and unsigned-longβint timeout
truncation) β distinct root causes, distinct fix lines. The
reproduction also corrects the record: a wait_on_bit() waiter does
not literally "sleep forever" on a lost wake β it polls at 1 Hz due to
this truncation.
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
not_testablefix.diff authored (mode->PCATCH translation + timeout clamp); not kernel-rebuild-validated (Low-severity mechanical change).
fix.diff in this pack
Confirmed kernel references
Detail
Exploit chain
kernel-internal (drm drivers via linux/wait_bit.h; i915 reset paths are active callers). Consequences: unkillable TASK_INTERRUPTIBLE waits for any future caller; 1 Hz timer-fire + context-switch per 'infinite' waiter (CPU cost, and it masks stranded waiters); no unprivileged direct trigger on this guest (no drm device).
Evidence (decisive lines)
run.log LWQ-F3 lines (two boots, identical); VERDICT.md source chain; kern_timeout.c:971-972 clamp; kern_synch.c:512 int timo.
PoC changes
No seed; experiment added to the DF-5182 module (deterministic tsleep-level probe + a wait_on_bit_timeout-level confirmation).
Verified recommended fix
Translate mode to PCATCH and clamp timeout >= INT_MAX / == MAX_SCHEDULE_TIMEOUT to timo=0 before calling tsleep() in wait_on_bit_timeout().
Verdict
REPRODUCED deterministically on the QEMU guest (same lwqdemo.ko as DF-5182): tsleep(timo=(int)MAX_SCHEDULE_TIMEOUT) with NO waker returned EWOULDBLOCK (rv=35) after 2 ticks - the 'infinite' timeout that wait_on_bit() (linux_wait.c:141-144) passes truncates from (unsigned long)LONG_MAX to int -1, and _callout_setup_quick() clamps the negative expiry to a next-tick callout, so every 'infinite' wait_on_bit() is actually a 1 Hz polling loop. wait_on_bit_timeout(MAX_SCHEDULE_TIMEOUT) on a set bit exited only when the bit was cleared 5s later (rv=0, 396 ticks), confirming poll-driven exit. The mode-as-flags half (TASK_INTERRUPTIBLE=1 never maps to PCATCH=0x100, so interruptible waits are silently unkillable; low bits land in PWAKEUP_CPUMASK) is source-certain but not runtime-demonstrable from a KLD (kernel threads take no signals); latent because in-tree callers pass TASK_UNINTERRUPTIBLE only.
No comments yet.