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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5183 Β· 10 files
FileTypeDescriptionSize
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) passes MAX_SCHEDULE_TIMEOUT (LONG_MAX) into wait_on_bit_timeout().
  • wait_on_bit_timeout() (linux_wait.c:118) passes timeout straight to tsleep(word, mode, "lwobt", timeout); tsleep()'s timo parameter is int (kern_synch.c:512), so LONG_MAX truncates to -1.
  • _callout_setup_quick() (kern_timeout.c:940-976) computes qtick = curticks - 1 and clamps it to softticks + 1 β€” a next-tick callout. Every tsleep iteration therefore returns EWOULDBLOCK after 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. PCATCH is 0x100 (param.h:324), so TASK_INTERRUPTIBLE never requests signal delivery: an interruptible wait is silently uninterruptible (unkillable thread). The remaining low bits land in PWAKEUP_CPUMASK (param.h:327), which tsleep ignores 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.

VERDICT.md
↓ download raw

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):

  1. tsleep((void *)&f3_word, 0, "lwqf3", (int)MAX_SCHEDULE_TIMEOUT) with no waker returned rv=35 (EWOULDBLOCK) after 2 ticks (run.log, first LWQ-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 inside wait_on_bit_timeout(): the do-while loop (linux_wait.c:117-123) re-enters tsleep every tick β€” a 1 Hz poll loop for every wait_on_bit() caller (i915_irq.c:3341, i915_irq.c:3199) instead of a blocking sleep.

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

fix.diff authored (mode->PCATCH translation + timeout clamp); not kernel-rebuild-validated (Low-severity mechanical change).

fix.diff in this pack
↓ fix.diffper-fix-DF-5183

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.