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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5418 Β· 17 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 β†’ !spin false β†’ the ENODEV timeout 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 β†’ !spin true β†’ spurious ENODEV for a handshake that succeeded in time. (Answer on any earlier poll leaves spin > 0 and 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied '!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
↓ fix.diffper-fix-DF-5418

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.