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)
PoC verification
Evidence pack
findings/poc/DF-5565 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedWith 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)
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).
No comments yet.