ppb_peripheral_negotiate() spin post-decrement off-by-one: Event-4 timeout detection is dead code and a handshake answered on the 10th poll spuriously fails with ENODEV
Summary
Post-decrement leaves spin==-1 on timeout (the !spin ENODEV branch can never fire - a silent host is misreported as EINVAL/mode-unsupported after reading the floating data register) and spin==0 on a last-iteration success (a fully successful handshake returns ENODEV). Production-code rig (verbatim sha-verified source + scripted fake ppbus): poll-10 answer -> ret=19 vs poll-5 control -> ret=0; silent host -> 22+error instead of ENODEV; spin<0 one-line fix validated both directions. PERIPH_1284-gated (not GENERIC), peer-controlled logic corruption, no memory-safety consequence.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5418 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 9.4 KB | view raw | |
| Makefile | β | 496 B | β download | |
| build.sh | β | 135 B | view raw | |
| run.sh | β | 183 B | view raw | |
| fix_validate.sh | β | 1.4 KB | view raw | |
| capture_logs.sh | β | 1.4 KB | view raw | |
| ppb_1284.pristine.c | β | 18.3 KB | view raw | |
| ppb_1284.fixA.c | β | 18.3 KB | view raw | |
| ppb_1284.fixB.c | β | 18.3 KB | view raw | |
| build.log | β | 2.9 KB | view raw | |
| run.log | β | 729 B | view raw | |
| run.full.dmesg.log | β | 881 B | view raw | |
| fix_build.log | β | 2.9 KB | view raw | |
| fix_run.log | β | 729 B | view raw | |
| env.txt | β | 627 B | view raw | |
| fix.diff | β | 252 B | view raw | |
| VERDICT.md | β | 4.3 KB | β raw |
DF-5418 β VERDICT
Status: reproduced (KLD-harness-with-production-code; guest
DragonFly 6.5-DEVELOPMENT x86_64, vm.sh QEMU/KVM, no parallel-port
hardware β the evidence class prescribed for this file).
What was run
ppbharness.ko compiles the unmodified production
/usr/src/sys/bus/ppbus/ppb_1284.c (sha256 88eaa04ce517β¦, identical
to the audited repo copy) and ppb_base.c (which provides
ppb_poll_bus/ppb_set_mode) with -Werror against a scripted
malicious IEEE 1284 peer: a fakeppc controller on nexus implementing
the ppbus INTERFACE (io, setmode), and a ppbshim child device
whose softc is a real struct ppb_data. All 1284 code under test β
ppb_peripheral_negotiate(), ppb_1284_read_id(),
ppb_peripheral_terminate(), the register macros β is the production
code; only the register source is synthetic.
Root cause (static)
ppb_1284.c:201-206:
spin = 10;
while (spin-- && (ppb_rstr(bus) & nBUSY))
DELAY(1);
if (!(ppb_rstr(bus) & SELECT) || !spin) { /* β ppb_1284.c:206 */
spin-- yields the pre-decrement value and always decrements:
- loop exits by counter exhaustion β
spin == -1β!spinfalse β theENODEVtimeout branch this clause was written for can never fire ("Event 4" timeout detection is dead code); - loop exits because the host answered on poll #10 β
spin == 0β!spintrue β spuriousENODEVfor a handshake that succeeded in time. (Answer on any earlier poll leavesspin > 0and succeeds β a pure timing flip.)
Observed (run.log, baseline)
T1 host-answers-poll-10: ret=19 state=1 error=0 β WRONG: peer completed the handshake; ret should be 0
T2 host-answers-poll-5 : ret=0 state=12 error=0 β control: same handshake one poll earlier succeeds
T3 host-silent-timeout : ret=22 state=0 error=1 β WRONG: silent peer; should be ENODEV(19) timeout,
is EINVAL(22) + error=PPB_MODE_UNSUPPORTED(1)
T1 vs T2 is the decisive pair: identical peer intent, one poll of
timing difference, successβfailure flips. T3 proves the dead timeout
branch: a completely silent peer is not reported as a timeout β the
code falls through to Event 4, reads the (peer-controlled / floating)
data register, mismatches every request mode, and records
PPB_MODE_UNSUPPORTED instead.
Fix validation (fix_run.log / ppb_1284.fixA.c)
One-line fix at ppb_1284.c:206 β !spin β spin < 0:
T1: ret=0 state=12 error=0 β success restored T2: ret=0 state=12 error=0 β control unchanged T3: ret=19 state=1 error=0 β timeout correctly detected (ENODEV)
Baseline reproduced β patched behavior gone β fixed. The patched
production file rebuilt clean with -Werror; the pristine source was
restored and cmp-verified afterwards.
Impact ceiling
Peer-controlled logic corruption of the peripheral-side negotiation
state machine: wrong success/failure decisions and a dead timeout
branch. No memory-safety consequence β the peer controls only status
bits and (via a fall-through) which error path runs; no length, buffer
or pointer derives from peer input on this path. In-tree the function
is only called from ppiwrite() under a PERIPH_1284 kernel config
(not in GENERIC; LINT64-only option), so real-world exposure requires
that non-default configuration plus a malicious/buggy IEEE 1284 host
on the cable. Severity: Low (robustness/logic defect in a niche
mode), confidence certain (static proof + reproduced both halves on
production code).
Attempts / changes vs. seed
PoC written fresh for this finding (no seed existed). Four build/run
iterations were needed on the harness plumbing (not the finding):
DRIVER_MODULE arity, devclass_t lvalues, and DF's
BUS_ADD_CHILD(parent,parent,β¦) idiom for nexus children (first
attempt panicked in nexus_print_child with ivars-less
device_add_child β diagnostic only, unrelated to the finding). The
production sources compiled clean on the first try.
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
fixedApplied '!spin' -> 'spin < 0' (ppb_1284.fixA.c = pristine + fix.diff) to the guest's /usr/src copy, rebuilt the same harness: T1 ret 19->0 (state PPB_PERIPHERAL_IDLE), T3 ret 22/EINVAL+MODE_UNSUPPORTED -> 19/ENODEV (timeout detected), control T2 unchanged. The buggy behavior is gone.
fix_run.log (fixA phase lines), fix_build.log, VERDICT.md 'Fix validation' section
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log lines 'DF-5418 T1/T2/T3' (baseline misbehavior through production code); fix_run.log (fixA phase: T1 ret=0 state=12, T3 ret=19); VERDICT.md full narrative; ppb_1284.pristine.c sha256 88eaa04c... proves the audited file was exercised verbatim
PoC changes
No seed PoC existed; harness written fresh. Harness-plumbing iterations (not finding-related): DRIVER_MODULE needs 6 args + devclass_t lvalue on this DF; nexus children must be created via BUS_ADD_CHILD(parent,parent,...) (first attempt panicked in nexus_print_child NULL-ivars); child of the fake bus must use plain device_add_child (DF bus_generic_add_child forwards to dev->parent). Production ppb_1284.c/ppb_base.c compiled clean on first attempt.
Verified recommended fix
ppb_1284.c:206: replace the '!spin' timeout test with 'spin < 0' so counter exhaustion (post-decrement leaves -1) is detected and a successful poll-10 handshake (spin==0) no longer fails spuriously.
Verdict
Reproduced on the QEMU guest (DragonFly 6.5-DEVELOPMENT x86_64) via the prescribed KLD-harness-with-production-code evidence class: the unmodified /usr/src/sys/bus/ppbus/ppb_1284.c (sha256 88eaa04c..., identical to the repo sys/ copy) plus ppb_base.c were compiled with -Werror into ppbharness.ko and driven against a scripted malicious IEEE1284 peer (fake ppbus controller). ppb_1284.c:201-206: the 'while (spin-- && ...)' post-decrement leaves spin==-1 on timeout, so the '!spin' timeout branch at :206 is dead code, and leaves spin==0 when the host answers exactly on poll #10, firing a spurious ENODEV for a successful handshake. Harness shows T1 (host answers poll 10) ret=19/state=FORWARD_IDLE vs control T2 (identical handshake, answers poll 5) ret=0/state=PERIPHERAL_IDLE, and T3 (silent host) ret=EINVAL + error=PPB_MODE_UNSUPPORTED instead of the intended ENODEV timeout. One-line fix (!spin -> spin < 0) validated: T1->ret 0, T3->ret 19, control unchanged; patched source rebuilt clean, pristine source restored and cmp-verified. Impact ceiling: peer-controlled logic corruption of the peripheral-side negotiation state machine (wrong success/failure decisions, dead timeout) with no memory-safety consequence (no length/buffer/pointer derives from peer input); reachable in-tree only from ppiwrite() under a PERIPH_1284 kernel option (not GENERIC). Severity Low.
No comments yet.