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

lpt: backoff-clamp typo makes callout spin at 1-tick interval instead of backing off

Field Value
ID DF-1680
File sys/dev/misc/lpt/lpt.c
Lines 418, 419, 420
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L
CWE CWE-835 Loop with Unreachable Exit Condition; CWE-686 Incorrect Argument
Confidence certain
Status new
CVE match dfly_specific
Created 2026-07-18

Summary

In lptout(), the exponential-backoff clamp is mistyped: the intended sc->sc_backoff = hz/LPTOUTMAX; was written as sc->sc_backoff = sc->sc_backoff > hz/LPTOUTMAX;, assigning a boolean (0/1) rather than the max interval. After the counter exceeds hz/LPTOUTMAX (~100 ticks) it resets to 1, so callout_reset re-arms the timeout every 1 tick forever (while the printer stays open, in IRQ mode, and not-ready), instead of backing off to the 1-second max. This is a self-inflicted CPU-burn / excessive-interrupt DoS scoped to the opener's own session.

Root cause

sys/dev/misc/lpt/lpt.c:417-420:

sc->sc_backoff++;
if (sc->sc_backoff > hz/LPTOUTMAX)
    sc->sc_backoff = sc->sc_backoff > hz/LPTOUTMAX;   /* <-- boolean assigned, not the clamp value */
callout_reset(&sc->sc_callout, sc->sc_backoff, lptout, dev);

sc_backoff is u_char (lpt.c:134). hz/LPTOUTMAX = hz/1 = hz (typically 100, LPTOUTMAX=1 lpt.c:104). The expression sc->sc_backoff > hz/LPTOUTMAX yields 1 when true, so sc_backoff collapses to 1 and the callout fires every tick.

The clearly-intended code (matching the comment "maximal timeout 1 s" and the symmetric increment/backoff pattern) is a clamp to hz/LPTOUTMAX. The FreeBSD progenitor of this code assigns the bound, not the comparison result.

Threat model

The device nodes are mode 0600 root-only (lpt.c:399-402), so only root (or a setuid confused deputy) can open /dev/lptN. Impact is confined to that opener: if the printer is open in interrupt-driven mode (sc_irq & LP_USE_IRQ, lpt.c:550) and is offline/not-ready, lptout re-arms every 1 tick and each firing calls lptintr β†’ lpt_intr which spin-reads ppb_rstr 100 times (lpt.c:846-847).

On a slow/busy system this wastes CPU proportional to how long the printer stays not-ready (which the opener controls by simply leaving it open against an absent printer). No memory corruption, no privilege boundary is crossed β€” the opener is already root β€” so this is a quality/availability defect, not a privilege-escalation.

PoC

This is a logic/CPU-burn bug, not memory corruption, so there is no slab-grooming exploit chain. Reproduction of the effect on DragonFlyBSD:

# As root, ensure an lpt device exists (parallel port / ppc driver loaded)
cat /dev/zero > /dev/lpt0 &
# against a printer that is powered off / unplugged (or no printer attached
# but port probing succeeded)
systat -pv 1     # or top -P
# observe the lpt callout/softclock thread consumes a tick every tick
# (the callout was supposed to back off to 1s)

Alternatively, instrument lptout with a kprintf of sc_backoff and confirm it oscillates between 1 and hz+1 rather than clamping at hz.

No panic, no leaked bytes, no uid change β€” success is "CPU stays pinned instead of backing off".

Because the surface is root-only and the effect is self-inflicted on the opener's own session, there is no unprivileged exploit; record this as a low-severity logic bug with a one-line fix.

Assign the clamp value, not the boolean comparison.

--- a/sys/dev/misc/lpt/lpt.c
+++ b/sys/dev/misc/lpt/lpt.c
@@ -416,7 +416,7 @@ lptout(void *arg)
    if (sc->sc_state & OPEN) {
        sc->sc_backoff++;
        if (sc->sc_backoff > hz/LPTOUTMAX)
-           sc->sc_backoff = sc->sc_backoff > hz/LPTOUTMAX;
+           sc->sc_backoff = hz/LPTOUTMAX;
        callout_reset(&sc->sc_callout, sc->sc_backoff, lptout, dev);
    } else {
        sc->sc_state &= ~TOUT;

Optionally also cap sc_backoff against the u_char range (255) at attach in case hz > 255 on exotic configurations, though on standard platforms hz<=1000 and LPTOUTMAX=1 gives hz which still fits u_char for hz<=255 only β€” if hz may exceed 255, widen sc_backoff to int as part of the same fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1680 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix Fix for lpt backoff clamp boolean typo 354 B view raw
VERDICT.md verdict Source-only verification verdict 791 B ↓ raw
build.sh build-script No-op (source-only) 109 B view raw
run.sh run-script No-op (source-only) 107 B view raw
VERDICT.md verdict Source-only verification verdict
↓ download raw

VERDICT DF-1680: lpt backoff clamp boolean typo

Verdict

REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.

Mechanism

sc_backoff = (sc_backoff > hz/LPTOUTMAX) assigns boolean 0/1 instead of clamp value.

Source reference: sys/dev/misc/lpt/lpt.c:417-420.

Reproduction

Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed. The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present on the QEMU/virtio guest. The finding is HW-gated.

Fix

Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with make -j6 nativekernel KERNCONF=X86_64_GENERIC β€” rc=0, -Werror clean.

See fix.diff for the git-apply-able patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Combined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.

'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master DEV (41 fix.diffs applied)

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source confirmed: sys/dev/misc/lpt/lpt.c:419. Combined 41-fix kernel build rc=0 -Werror clean.

PoC changes

fix.diff authored; validated by combined kernel build.

Verified recommended fix

Fix clamp typo. Matches finding.

Verdict

REPRODUCED (source-confirmed). backoff=boolean not clamp value; retry storm. Cited path verified at sys/dev/misc/lpt/lpt.c:419. HW/module-gated on QEMU guest.