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

uvisor VISOR GET_CONNECTION_INFORMATION: actlen never validated after a SHORT_XFER_OK control-IN into an 18-byte stack struct; the USB_DEBUG connection loop consumes uninitialized kernel stack when an evil Visor short-transfers (Info: no production consumer)

Summary

The VISOR branch never checks actlen (contrast the PALM4 branch's actlen<12 guard) and bypasses the wrapper zeroization via the raw call: a zero-length/2-byte data stage leaves num_ports/connections[] uninit - only the USB_DEBUG loop consumes it (production hardwires nports=1). Same pattern became a real leak in DF-5455 where a consumer existed. actlen>=sizeof guard fix compile-validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5489 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
fix.diff β€” 564 B view raw
fix_compile.log β€” 7.2 KB view raw
env.txt β€” 443 B view raw
verdict.json β€” 4.2 KB view raw

DF-5489 β€” uvisor VISOR GET_CONNECTION_INFORMATION: actlen never validated;

the USB_DEBUG connection loop consumes uninitialized kernel stack when an

evil Visor short-transfers (Info β€” no production consumer)

The code (production)

The VISOR branch of uvisor_init() (uvisor.c:380-428) reads an 18-byte struct uvisor_connection_info coninfo from the device with USB_SHORT_XFER_OK:

err = usbd_do_request_flags(udev, NULL,
    &req, &coninfo, USB_SHORT_XFER_OK,
    &actlen, USB_DEFAULT_TIMEOUT);          /* uvisor.c:387-389 */

if (err) {
        goto done;
}
/* NO actlen CHECK β€” contrast the PALM4 branch's
   "if (actlen < 12)" at uvisor.c:447-451 */

usbd_do_request_flags() is the raw entry point, not usbd_do_request_proc() β€” the zeroize-on-error / zero-pad-on-short logic (usb_request.c:782-787) does NOT apply, and the controller DMAs only the bytes actually transferred. A device that answers with a zero-length or 2-byte data stage (both legal under USB_SHORT_XFER_OK; err == 0) leaves connections[] / part of num_ports as uninitialized stack.

The only consumer is the #ifdef USB_DEBUG block (uvisor.c:395-428): np = UGETW(coninfo.num_ports) (clamped to UVISOR_MAX_CONN=8 at uvisor.c:401-403 β€” no OOB), then per-port coninfo.connections[i].port_function_id (selects a string) and coninfo.connections[i].port (printed as %d). With a USB_DEBUG kernel and hw.usb.uvisor.debug >= 1, up to ~18 stale stack bytes are disclosed to the console/msgbuf as numbers/string-selections.

Production kernels never read coninfo β€” dead store, no userspace path β€” hence Info, same treatment as the DF-5448 (uvscom) and DF-5438 (ulpt) siblings. The bug class is real (this is exactly how DF-5455 umoscom leaked into TIOCMGET, where the buffer had a consumer), and the PALM4 sibling branch shows the intended hygiene (actlen < 12 check, uvisor.c:447-451).

Threat model

Evil/wedged Handspring VISOR-VID:PID adapter (082d:0100 etc., uvisor.c:254); requires a USB_DEBUG kernel build for any observable effect.

Evidence class

Static, code-certain β€” NOT executed (no Visor device model; USB_DEBUG is not enabled on the guest's X86_64_GENERIC kernel, so the consumer does not even compile there). Same treatment as DF-5448/DF-5438.

Fix

fix.diff mirrors the PALM4 check: require actlen >= UVISOR_CONNECTION_INFO_SIZE (18) before the (debug) consumer, failing with USB_ERR_INVAL on a short transfer. The patched production uvisor.ko compiles -Werror clean on the guest (fix_compile.log).

VERDICT.md
↓ download raw

DF-5489 VERDICT β€” static, code-certain, NOT executed (Info class)

Finding: the VISOR branch of uvisor_init() (uvisor.c:380-428) never validates actlen after usbd_do_request_flags(..., &coninfo, USB_SHORT_XFER_OK, &actlen, USB_DEFAULT_TIMEOUT) (uvisor.c:387-389). That is the raw request entry point β€” the zeroize-on-error / zero-pad-on-short protection in usbd_do_request_proc() (usb_request.c:782-787) does not apply β€” so a short or zero-length (both legal under SHORT_XFER_OK) vendor response leaves the 18-byte stack buffer partially uninitialized. The #ifdef USB_DEBUG consumer (uvisor.c:400-426) then reads UGETW(coninfo.num_ports) and connections[i].port / .port_function_id and prints them (DPRINTF), disclosing up to ~18 stale kernel-stack bytes to console/msgbuf on a USB_DEBUG kernel with hw.usb.uvisor.debug >= 1.

Why not executed

  • The guest's X86_64_GENERIC kernel is not built with USB_DEBUG, so the consumer does not exist in the running kernel.
  • No QEMU Visor VID:PID device model exists (-device usb-serial is a Netchip 0525:7701 that binds uftdi, not uvisor).
  • Even with both, the observable is a weak debug-log disclosure of number-formatted stack bytes β€” no memory-safety consequence (the loop count is clamped, uvisor.c:401-403; there is no OOB).

Production impact: none (dead store)

Outside #ifdef USB_DEBUG, coninfo is never read after the request; num_ports never feeds ucom_attach() (uvisor hardwires nports=1, uvisor.c:341). So this is a hardening/hygiene finding in the exact shape of the family's DF-5448 (uvscom) / DF-5438 (ulpt) / DF-5455 (umoscom, where the buffer DID have a consumer β†’ Low) entries. It is filed so the next refactor that starts consuming connection info (e.g. a multi-port uvisor like FreeBSD's) does not inherit an unvalidated device buffer.

Line-accurate confirmation (guest tree == audited tree,

uvisor.c sha256 b742032f…d703844438)

  • Request: uvisor.c:382-389 (wLength 18 = sizeof(coninfo) β€” buffer size itself is correct; only completeness is unchecked).
  • Missing check: immediately after uvisor.c:391-393's error check; the PALM4 branch demonstrates the intended pattern at uvisor.c:447-451 (if (actlen < 12) β€” which correctly covers the offsets its code later reads: 1, 8, 9).
  • Uninit consumers (USB_DEBUG only): uvisor.c:400 (UGETW(coninfo.num_ports)), uvisor.c:407 (port_function_id), uvisor.c:424-425 (port printed).
  • No zeroization: call goes to usbd_do_request_flags directly; the _proc wrapper that would zero-pad (usb_request.c:780-787) is bypassed.

Fix validation

fix.diff adds the missing actlen < UVISOR_CONNECTION_INFO_SIZE β†’ USB_ERR_INVAL guard (mirroring uvisor.c:447-451). The patched production uvisor.c compiled -Werror clean as a kmod on the guest (fix_compile.log, RC=0). Not runtime-tested (see above) β€” fix_status build-validated only.

Guest hygiene

Guest untouched by this finding's verification (build-only scratch dirs removed; production uvisor.c md5 unchanged 28d0d390d06dd00f9dbfc03106b453a7).

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

not_testable
baseline no→ patch + rebuild →patched clean

Compile-dimension validated (patched production uvisor.ko builds -Werror-clean RC=0 on the guest, fix_compile.log); runtime dimension not testable without a Visor device model and a USB_DEBUG kernel -- the guard is the same shape as the proven PALM4 check at uvisor.c:447-451.

fix.diff (sha256 30c63c71...); fix_compile.log (RC=0).
↓ fix.diffper-fix-DF-5489

Confirmed kernel references

Detail

Evidence (decisive lines)

Line-accurate static confirmation in VERDICT.md: request uvisor.c:382-389 (wLength 18 == sizeof(coninfo) -- buffer size correct, only completeness unchecked); missing check after the error test at uvisor.c:391-393; intended pattern demonstrated by the PALM4 sibling at uvisor.c:447-451; USB_DEBUG-only consumers at uvisor.c:400, 407, 424-425; no zeroization (raw usbd_do_request_flags bypasses usb_request.c:782-787); guest tree == audited tree (uvisor.c sha256 b742032f...). fix_compile.log: patched production uvisor.ko builds -Werror-clean RC=0.

PoC changes

No seed PoC existed; no runtime PoC authored (static Info-class finding, DF-5448/DF-5438 precedent). fix.diff authored and compile-validated.

Verified recommended fix

Require actlen >= UVISOR_CONNECTION_INFO_SIZE (18) before consuming coninfo, mirroring the PALM4 actlen check (full diff in fix.diff).

Verdict

Static, code-certain finding NOT executed (Info-severity hardening class; same treatment as the DF-5448 uvscom and DF-5438 ulpt siblings): the VISOR branch of uvisor_init() reads an 18-byte struct uvisor_connection_info from the device with USB_SHORT_XFER_OK (uvisor.c:387-389) and never validates actlen -- in contrast to the PALM4 branch's 'if (actlen < 12)' check at uvisor.c:447-451 which correctly covers the offsets that branch consumes (1, 8, 9). The call goes to the raw usbd_do_request_flags(), not usbd_do_request_proc(), so the zeroize-on-error / zero-pad-on-short protection (usb_request.c:782-787) does not apply: an evil Visor-VID:PID device answering with a zero-length or 2-byte data stage (both legal under SHORT_XFER_OK, err==0) leaves connections[]/num_ports as uninitialized kernel stack. The ONLY consumer is the #ifdef USB_DEBUG block (uvisor.c:395-428: UGETW(num_ports) at :400, port_function_id at :407, port printed at :424-425) which discloses up to ~18 stale stack bytes to console/msgbuf on a USB_DEBUG kernel with hw.usb.uvisor.debug >= 1; the loop count is clamped (uvisor.c:401-403), so there is no OOB. Production kernels never read coninfo (dead store; nports is hardwired to 1 at uvisor.c:341), so there is no userspace path and no memory-safety impact -- filed so a future multi-port refactor does not inherit an unvalidated device buffer (this is exactly how DF-5455 umoscom became a real leak: same unguarded pattern, but with a TIOCMGET consumer). Not runtime-testable: no QEMU Visor VID:PID device model exists and the guest's X86_64_GENERIC kernel is not built with USB_DEBUG, so the consumer does not exist in the running kernel.