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

ppb_MS_microseq() discards the return values of its recursive self-calls β€” a mode-transfer loop that aborts on error is skipped and the whole MS_OP_PUT/GET reports success (initxfer's MS_RET(0) zeroes the error channel first)

Summary

The recursive calls' return values are thrown away and only the *ret side-channel (MS_RET) is observed - which the initxfer run actively zeroes before the transfer loop executes: any abort inside xfer->loop (adapter failure, C_CALL timeout hook like vpio's nibble timeout, or the DF-5574 guard) propagates nowhere and vpo/imm see a completed transfer over stale buffer contents. Found empirically while validating the DF-5574 fix (the guard's EINVAL vanished, rc=0). Capture-and-fold fix validated (rc=5 propagated, ret untouched).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5577 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
msqhx.c β€” 19.3 KB view raw
Makefile β€” 311 B ↓ download
build.sh β€” 126 B view raw
run.sh β€” 175 B view raw
build.log β€” 7.7 KB view raw
run.log β€” 166 B view raw
fix_run.log β€” 263 B view raw
fix.diff β€” 3.2 KB view raw
env.txt β€” 670 B view raw

DF-5577 β€” ppb_MS_microseq() discards the return value of its recursive self-calls: a mode-transfer loop that aborts on error is skipped and the whole MS_OP_PUT/GET reports success (error channel zeroed by initxfer's MS_RET(0))

Severity: Low Β· Confidence: certain Β· File: sys/bus/ppbus/ppb_msq.c (pass 2)

Reproduce

  1. sh build.sh β€” compiles the verbatim production ppb_msq.c into msqhx.ko
  2. sh run.sh β€” case 6 (non-fatal)
  3. Observe dmesg (dmesg | grep MSQHX) / the serial console.

Expected

BASELINE (unfixed): 'DF-5577 hook_failed=1 (rc=0 ret=0x0): FAILURE SWALLOWED - reported success (bug)' β€” the loop aborted on the hook's error 5, yet the top-level reports success and even executes the following MS_RET (ret overwritten to 0). PATCHED: 'DF-5577 hook_failed=1 (rc=5 ret=0x77): failure propagated (fixed)'.

Harness (shared by DF-5573..DF-5577)

msqhx.c + Makefile compile the verbatim production /usr/src/sys/bus/ppbus/ppb_msq.c (sha256 223e7f9184c81ab83f49228ae7d1f99a 6e3936b9a452b01c6bb6176b150dda67, identical to the repo sys/ copy) into a KLD, together with a fake ppbus controller on nexus whose ppbus_exec_microseq method implements the exact dispatch semantics of ppc_exec_microseq() (sys/dev/misc/ppc/ppc.c:1332-1542) for the exercised opcodes. The interpreter target device's softc is a real struct ppb_data and its child's ivars are a real struct ppb_device, so mode2xfer(), ppb_MS_init(), ppb_MS_exec(), ppb_MS_microseq(), ppb_MS_loop() and ppb_MS_init_msq() all run their production code paths. Evidence class: "KLD-harness-with-production-code" (no parallel port in the QEMU rig) β€” family standard, cf. DF-5418.

Build

# inside the guest, from this directory
sh build.sh          # -> /tmp/msqhx-build/msqhx.ko

Run

sh run.sh            # runs this pack's case via sysctl hw.msqhx_case
sh run.sh <case>     # 0=suite(5575+5573) 2=5574 4=5576a 5=5576b 6=5577

Cases 2/4/5 PANIC the guest on unfixed production code (that is the finding); the panics land on the serial console (dfbsd-qemu/vm.sh log).

Fix validation

fix.diff (sha256 8e3dfc90e6049ce1704c3cef833cba0904b6404f11360bdd299e37980e9327bb) patches sys/bus/ppbus/ppb_msq.c + sys/bus/ppbus/ppbconf.h (all five findings; never applied to the repo's read-only sys/ tree). Validation procedure: copy the fixed files into the guest's /usr/src/sys/bus/ppbus/, sh build.sh again (recompiles the production source), re-run the same case. Baseline vs patched outputs are in run.log / fix_run.log (plus panic*.txt / fix_run.log for the fatal cases).

VERDICT.md
↓ download raw

DF-5577 β€” verdict narrative

Finding

ppb_MS_microseq() discards the return value of its recursive self-calls: a mode-transfer loop that aborts on error is skipped and the whole MS_OP_PUT/GET reports success (error channel zeroed by initxfer's MS_RET(0))

Bottom line (status: reproduced, impact: none)

Reproduced on the QEMU guest (DragonFly 6.5-DEVELOPMENT x86_64), discovered during DF-5574 fix validation when the guard's EINVAL vanished. At ppb_msq.c:301 and :309 the two recursive ppb_MS_microseq() calls have their return values discarded; the caller checks only its local error, which is the *ret side-channel written by MS_RET β€” and the initxfer run at :301 ends with MS_RET(0), zeroing error immediately before the transfer loop executes. Any abort inside the mode transfer loop (adapter exec_microseq failure β€” e.g. vpio's nibble_inbyte_hook timeout, an MS_OP_C_CALL error β€” or the DF-5574 guard) therefore propagates nowhere: the aborted transfer is skipped and the sequence continues to the next instruction, reporting success (harness: hook returns 5, top-level rc=0 and the following MS_RET overwrites ret with 0). For vpo/imm this means a timed-out or failed ZIP read/write mid-transfer is presented to the SCSI layer as a completed transfer over stale buffer contents β€” a silent data-integrity failure, not memory corruption. The top-level ppb_MS_exec() does propagate its own return value, which is why single-opcode failures surface but loop failures do not. Fix validated: capturing the recursive return value (xerr) and folding in the side-channel makes the same case return rc=5 with ret untouched (0x77) β€” 'failure propagated (fixed)' β€” and is the same change that makes the DF-5574 guard observable (rc=22).

Exploit chain

root-gated ppbus device (vpo/imm) mid-transfer timeout -> error discarded at ppb_msq.c:309 -> vpio_instr() returns success -> SCSI/CAM consumes stale buffer data as a completed transfer (silent data corruption at the I/O layer)

Evidence

run.log ('DF-5577 hook_failed=1 (rc=0 ret=0x0): FAILURE SWALLOWED - reported success (bug)') on pristine production code; fix_run.log ('DF-5577 hook_failed=1 (rc=5 ret=0x77): failure propagated (fixed)') on the fix.diff-patched source; the DF-5574 pack's v1-fix run ('rc=0' despite the guard tripping) documents the discovery

Kernel references

Fix validation (fixed)

Return values of the recursive ppb_MS_microseq() calls are captured (xerr) and folded with the *ret side-channel: the failing-loop case returns rc=5 (hook's error) instead of 0, and ret remains unwritten (0x77), proving the following MS_RET no longer runs after an aborted transfer.

fix.diff (sha256 8e3dfc90e6049ce1704c3cef833cba0904b6404f11360bdd299e37980e9327bb) carries all five ppb_msq.c fixes for this pass; the hunks relevant to this finding are marked with its DF id in the comments. Fix validation followed the family standard (cf. DF-5418): the fixed sources were installed into the guest's /usr/src/sys/bus/ppbus/, the harness was rebuilt (recompiling the production file), and the exact same case was re-run β€” baseline vs patched outputs are in run.log / fix_run.log (+ panic*.txt). The repo's sys/ tree was never modified; the guest was vm.sh reset with-src afterwards (sources restored+verified by snapshot).

PoC changes vs seed

Found while validating the DF-5574 fix: the guard's EINVAL was swallowed (rc printed 0), which pinpointed the discarded return values at ppb_msq.c:301/:309. Case 6 added to demonstrate the pristine-code swallow directly (failing MS_C_CALL hook inside a registered loop).

Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 Harness sha256: 4b728315b40157934f62c99e847f386b30202eef77083dc1a5c065b7b0448ded

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

Return values of the recursive ppb_MS_microseq() calls are captured (xerr) and folded with the *ret side-channel: the failing-loop case returns rc=5 (hook's error) instead of 0, and ret remains unwritten (0x77), proving the following MS_RET no longer runs after an aborted transfer.

fix_run.log (patched production source, same case); run.log (baseline) vs fix_run.log (patched); fix.diff sha256 8e3dfc90e6049ce1704c3cef833cba0904b6404f11360bdd299e37980e9327bb
↓ fix.diffper-fix-DF-5577

Confirmed kernel references

Detail

Exploit chain

root-gated ppbus device (vpo/imm) mid-transfer timeout -> error discarded at ppb_msq.c:309 -> vpio_instr() returns success -> SCSI/CAM consumes stale buffer data as a completed transfer (silent data corruption at the I/O layer)

Evidence (decisive lines)

run.log ('DF-5577 hook_failed=1 (rc=0 ret=0x0): FAILURE SWALLOWED - reported success (bug)') on pristine production code; fix_run.log ('DF-5577 hook_failed=1 (rc=5 ret=0x77): failure propagated (fixed)') on the fix.diff-patched source; the DF-5574 pack's v1-fix run ('rc=0' despite the guard tripping) documents the discovery

PoC changes

Found while validating the DF-5574 fix: the guard's EINVAL was swallowed (rc printed 0), which pinpointed the discarded return values at ppb_msq.c:301/:309. Case 6 added to demonstrate the pristine-code swallow directly (failing MS_C_CALL hook inside a registered loop).

Verified recommended fix

Capture the return values of the recursive ppb_MS_microseq() calls at ppb_msq.c:301/:309 instead of checking only the *ret side-channel that initxfer's MS_RET(0) zeroes

Verdict

Reproduced on the QEMU guest (DragonFly 6.5-DEVELOPMENT x86_64), discovered during DF-5574 fix validation when the guard's EINVAL vanished. At ppb_msq.c:301 and :309 the two recursive ppb_MS_microseq() calls have their return values discarded; the caller checks only its local error, which is the *ret side-channel written by MS_RET β€” and the initxfer run at :301 ends with MS_RET(0), zeroing error immediately before the transfer loop executes. Any abort inside the mode transfer loop (adapter exec_microseq failure β€” e.g. vpio's nibble_inbyte_hook timeout, an MS_OP_C_CALL error β€” or the DF-5574 guard) therefore propagates nowhere: the aborted transfer is skipped and the sequence continues to the next instruction, reporting success (harness: hook returns 5, top-level rc=0 and the following MS_RET overwrites ret with 0). For vpo/imm this means a timed-out or failed ZIP read/write mid-transfer is presented to the SCSI layer as a completed transfer over stale buffer contents β€” a silent data-integrity failure, not memory corruption. The top-level ppb_MS_exec() does propagate its own return value, which is why single-opcode failures surface but loop failures do not. Fix validated: capturing the recursive return value (xerr) and folding in the side-channel makes the same case return rc=5 with ret untouched (0x77) β€” 'failure propagated (fixed)' β€” and is the same change that makes the DF-5574 guard observable (rc=22).