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

iicbus_request_bus's first do-while spins forever for IIC_DONTWAIT when the bridge callback persistently reports busy β€” unkillable tight kernel loop with no signal checkpoint (livelock)

Summary

The first do-while re-invokes IICBUS_CALLBACK whenever poll returns EWOULDBLOCK, and for DONTWAIT the poll default returns EWOULDBLOCK immediately without sleeping: a busy bridge gives an unbounded kernel-mode loop (no sleep/yield/signal checkpoint). In-tree via lpbb ppbus contention with the iicwrite/iicread DONTWAIT callers; the SECOND loop of request_bus handles the same condition correctly - only the first spins. Rig: CPU time accrues 1:1 with wall clock, SIGKILL ignored; single-callback-attempt fix validated (immediate EWOULDBLOCK errno 35, sanity round trip passes).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5565 Β· 14 files
FileTypeDescriptionSize
df5563_rig.c β€” 12.7 KB view raw
iicbb_reg.c β€” 491 B view raw
park.c β€” 3.2 KB view raw
Makefile.rig β€” 202 B ↓ download
Makefile.reg β€” 189 B ↓ download
build.sh β€” 111 B view raw
build.log β€” 13.5 KB view raw
run.sh β€” 169 B view raw
run.log β€” 481 B view raw
fix_run.log β€” 1.1 KB view raw
env.txt β€” 400 B view raw
fix.diff β€” 5.2 KB view raw
README.md β€” 2.7 KB ↓ raw
verdict.json β€” 3.3 KB view raw

DF-5565 β€” IIC_DONTWAIT + busy bridge callback = unkillable tight kernel loop (livelock)

  • File under audit: sys/bus/iicbus/iiconf.c (pass 2)
  • Class: local DoS (CPU livelock, unkillable)
  • Severity: Low (root-gated; in-tree reachable via lpbb/ppbus contention)
  • Verdict: REPRODUCED (non-destructive), fix validated

The bug

The first loop of iicbus_request_bus() (iiconf.c:92-97):

do {
        error = IICBUS_CALLBACK(device_get_parent(bus),
                        IIC_REQUEST_BUS, (caddr_t)&how);
        if (error)
                error = iicbus_poll(sc, how);
} while (error == EWOULDBLOCK);

With how == IIC_DONTWAIT (0), iicbus_poll() takes the default: case and returns EWOULDBLOCK immediately without sleeping (iiconf.c:70-72). If the bridge callback persistently reports busy, the loop retries forever: no sleep, no yield, no signal checkpoint β€” a tight kernel-mode loop that burns one CPU and cannot be interrupted by SIGKILL.

In-tree reachability: lpbb is the bridge whose callback can report busy β€” lpbb_callback (sys/dev/misc/lpbb/lpbb.c:90-113) forwards to ppb_request_bus, which returns EWOULDBLOCK immediately for PPB_DONTWAIT when another ppbus client (e.g. lpt printing, or another lpbb user) holds the parallel port (sys/bus/ppbus/ppbconf.c:458-493). DONTWAIT callers: iicwrite/iicread on /dev/iicN (sys/bus/iicbus/iic.c:212-213, 253-254). Requires lpbb-class hardware (parallel-port I2C) or any KLD bridge with the same contract β€” the rig's hw.df5563.busy=1 emulates it faithfully.

Contrast: the second loop (owner contention) handles the DONTWAIT EWOULDBLOCK correctly (releases and returns, iiconf.c:110-113); only the first loop spins.

Reproduce (non-destructive)

sysctl hw.df5563.busy=1
./park nowait &          # enters the loop; never returns
ps -o pid,stat,time -p $!   # STAT R+ (running, never sleeps), TIME += wall clock
kill -9 $!               # IGNORED
sysctl hw.df5563.busy=0  # next callback succeeds -> loop exits, process dies with pending SIGKILL

Expected output (baseline)

--- ps t0 ---                --- ps t1 (+3s) ---
  2007 R1       0:03.01       2007 R1       0:06.01   <-- 100% kernel CPU
SIGKILL_IGNORED=YES
GONE_AFTER_UNBUSY=YES

CPU time accrues 1:1 with wall clock (3 s CPU per 3 s wall) while the thread is in kernel mode the whole time.

Fix

Covered by DF-5563's fix.diff: the restructured loop does exactly one callback attempt for DONTWAIT β€” iicbus_poll()'s EWOULDBLOCK exits the loop instead of retrying. Validation (fix_run.log): with the patched iicbus.ko, ./park nowait under busy returns errno=35 (EWOULDBLOCK) immediately, twice, with no spin; the sanity round trip still passes.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

With fix.diff the identical DONTWAIT+busy call returns EWOULDBLOCK (errno 35) immediately with no spin (fix_run.log F3v section), and the attach sanity round trip still passes.

findings/poc/DF-5565/fix_run.log (F3v section); fix.diff (shared with DF-5563)
↓ fix.diffper-fix-DF-5565

Confirmed kernel references

Detail

Exploit chain

root on an lpbb system: open /dev/lpt0 (or otherwise hold the ppbus), then write()/read() /dev/iicN -> iicwrite/iicread -> iicbus_request_bus(IIC_DONTWAIT) -> infinite kernel loop: one CPU pegged at 100% in kernel mode, the calling process unkillable (kill -9 ignored), the fd held open. Only freeing the ppbus ends it. Any KLD bridge with a busy callback reproduces it identically (rig-proven).

Evidence (decisive lines)

run.log: ps lines '2007 R1 0:03.01' -> '2007 R1 0:06.01' (TIME grows with wall clock, STAT never leaves R), 'SIGKILL_IGNORED=YES', 'GONE_AFTER_UNBUSY=YES'. fix_run.log: with fix.diff the same call returns 'errno=35: Resource temporarily unavailable' immediately, twice, no spin.

PoC changes

Shared rig with DF-5563; ./park nowait drives iicbus_request_bus(bus, dev, IIC_DONTWAIT), the iicwrite/iicread contract; hw.df5563.busy=1 emulates the lpbb ppbus-held callback.

Verified recommended fix

Restructured request loop performs exactly one callback attempt for DONTWAIT (DF-5563 fix.diff).

Verdict

The first do-while of iicbus_request_bus (iiconf.c:92-97) re-invokes the bridge callback whenever iicbus_poll returns EWOULDBLOCK; for how == IIC_DONTWAIT the poll's default case (iiconf.c:70-72) returns EWOULDBLOCK immediately without sleeping, so a persistently-busy bridge callback produces a tight unbounded kernel loop with no signal checkpoint. Demonstrated on the guest: ./park nowait under hw.df5563.busy=1 stayed in STAT R with CPU TIME accruing exactly 1:1 with wall clock (3s->6s->9s CPU over 3s->6s->9s wall), SIGKILL was IGNORED (no signal checkpoint in the loop), and the ioctl only completed after the bridge was unbusied. The busy-callback contract is real in-tree: lpbb forwards IIC_REQUEST_BUS to ppb_request_bus (lpbb.c:97-101), which returns EWOULDBLOCK immediately for DONTWAIT when the ppbus is held (ppbconf.c:458-493); iicwrite/iicread on /dev/iicN are the DONTWAIT callers (iic.c:212-213, 253-254).