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

ohci_controller_init latches unvalidated device-reported NDP (0-255) into sc_noport: kernel-driven OOB MMIO reads AND writes past the 0x100-byte OHCI register file on every roothub event (DF-5268/5282/5296 register-geometry family, OHCI variant)

Summary

The 8-bit NDP register field is latched with only a retry-on-zero guard - no clip to the OHCI 1.0a max of 15 - and re-latched on every resume: with NDP in [16,255] the roothub status loop OREAD4s ports 1..noport (offsets to 0x474), roothub_exec OREAD4/OWRITE4s 0x50+4*index including UPS_RESET fixed patterns, and bNbrPorts makes the in-kernel uhub thread itself walk all bogus ports - kernel-driven OOB MMIO R/W up to 0x374 past the register file on minimal-BAR implementations. QEMU hard-caps at 15 and models the register read-only - honestly untested (same posture as DF-5296). The isoc 3-page ITD mis-mapping found en route is stream-data corruption only (writes stay inside the transfer's own DMA allocation) - documented not filed. clip-at-latch fix git-apply-clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5767 Β· 8 files
FileTypeDescriptionSize
README.md β€” 5.0 KB ↓ raw
VERDICT.md β€” 6.8 KB ↓ raw
fix.diff β€” 743 B view raw
itd_offset_sim.py β€” 2.1 KB view raw
qemu-hcd-ohci-reference.c β€” 60.1 KB view raw
verdict.json β€” 4.0 KB view raw
manifest.json β€” 1.4 KB view raw
env.txt β€” 294 B view raw

DF-5767 β€” OHCI sc_noport latched from unvalidated device-reported NDP (0–255) β€” OOB MMIO R/W past the 0x100-byte OHCI register file

File: sys/bus/u4b/controller/ohci.c (pass 2, GLM 5.3) Family: register-geometry trust β€” OHCI variant of DF-5268 (xhci), DF-5282 (ehci), DF-5296 (uhci). Severity: Low. Bucket: kernleak (hardware-trust class). Confidence: certain (code facts).

What the code does

ohci_controller_init() reads the number of downstream ports directly from device MMIO and latches it into sc->sc_noport with no upper-bound check (sys/bus/u4b/controller/ohci.c:282-287):

sc->sc_noport = 0;
for (i = 0; (i < 10) && (sc->sc_noport == 0); i++) {
        usb_pause_mtx(NULL, USB_MS_TO_TICKS(OHCI_READ_DESC_DELAY));
        sc->sc_noport = OHCI_GET_NDP(OREAD4(sc, OHCI_RH_DESCRIPTOR_A));
}
  • The retry loop only guards against NDP == 0 (the documented AMD756 erratum, comment at ohci.c:279-281). A flaky or hostile controller returning 0xFF (or any value > 15) is latched verbatim.
  • OHCI_GET_NDP(s) = (s) & 0xff (ohcireg.h:97) β€” the full 8-bit register field, while the OHCI 1.0a spec caps NDP at 15 and defines the operational register file as 0x00-0xFF with per-port registers at 0x54 + 4*(n-1) (ohcireg.h:113).
  • sc->sc_noport is re-latched on every resume: ohci_resume() (ohci.c:461-476) β†’ ohci_controller_init(sc, 0) β†’ ohci.c:286. A transient bogus read at resume time replaces the boot-time value.

Propagation (all in ohci.c)

  1. ohci_root_intr() (ohci.c:1590-1620), runs on every RHSC interrupt / roothub status poll: * m = sc->sc_noport + 1 clamped to 8*sizeof(sc_hub_idata) = 256 β€” the clamp is vacuous (noport ≀ 255 as uint8_t, ohci.h:248,251). * Loop (ohci.c:1610-1616) does OREAD4(sc, OHCI_RH_PORT_STATUS(i)) for i = 1..noport β†’ MMIO reads at 0x54..0x50+4noport = 0x474 max*, i.e. up to 0x374 bytes past the 0x100-byte register file.
  2. ohci_roothub_exec() port requests (index bounded only by sc_noport): * GET port status β€” bounds check ohci.c:2306-2310, read at 2311. * SET port feature β€” bounds check ohci.c:2324-2328, writes at 2332-2358 (incl. UPS_RESET at 2339) β†’ OOB MMIO writes of fixed UPS_ bit patterns at 0x54+4(noport-1). * CLEAR port feature β€” bounds check ohci.c:2221-2225, writes at 2229-2252, re-read at 2264.
  3. Hub descriptor (ohci.c:2276-2296): bNbrPorts = sc_noport (2279) and bDescLength = 8 + ((noport+7)/8) (2294). The in-kernel uhub driver then enumerates noport ports and itself issues the GET/SET status requests of (2) for every bogus port β€” the OOB accesses are kernel-driven, no userspace request needed beyond plugging/unplugging a device (or any roothub change event) once noport is poisoned. (The DeviceRemovable[l/8] writes at 2290 stay in-bounds: array is [32] and l/8 ≀ 31.)

Why impact is Low

  • Requires a hostile or misbehaving controller (PCI device / emulation): the register read the driver performs is hardware-controlled. Not triggerable by unprivileged local input on healthy hardware.
  • The OOB MMIO writes carry only fixed feature-bitmask constants (UPS_PORT_POWER, UPS_RESET, change-acknowledge bits <<16), not attacker data; reads contribute at most one change-bit per bogus port into the hub interrupt bitmap (weak info side channel into uhub state).
  • On implementations whose OHCI BAR is 4 KiB (common), ports 16..255 map inside the BAR and nothing happens; on minimal (0x100) BARs the accesses land on adjacent PCI MMIO regions (cross-device register corruption, same ceiling as DF-5296/uhci).

Why not live-verified on the audit guest (status: untested)

  • The guest's QEMU caps the OHCI port count at OHCI_MAX_PORTS = 15 at device init (hw/usb/hcd-ohci.c:1953-1955 in QEMU master, fetched 2026-10-11) and models HcRhDescriptorA as fully read-only (OHCI_RHA_RW_MASK = 0, qemu hcd-ohci.c:189, 1791-1792) β€” so no value

    15 can ever be observed by the driver on this guest, not even with a kernel module rewriting the register.

  • The default audit-guest QEMU config does not instantiate any USB controller at all (pciconf -l shows none), which additionally forecloses device-driven triggering.
  • Same verification posture as the accepted family sibling DF-5296 (uhci BAR-size trust), filed untested for the identical reason.

Clamp the latched port count to the spec maximum at both latch sites:

--- a/sys/bus/u4b/controller/ohci.c
+++ b/sys/bus/u4b/controller/ohci.c
@@ -283,6 +283,12 @@
    sc->sc_noport = 0;
    for (i = 0; (i < 10) && (sc->sc_noport == 0); i++) {
        usb_pause_mtx(NULL,
            USB_MS_TO_TICKS(OHCI_READ_DESC_DELAY));
        sc->sc_noport = OHCI_GET_NDP(OREAD4(sc, OHCI_RH_DESCRIPTOR_A));
+       if (sc->sc_noport > 15) {
+           device_printf(sc->sc_bus.bdev,
+               "invalid NDP %d, clipping to 15\n",
+               sc->sc_noport);
+           sc->sc_noport = 15;
+       }
    }

(Defense-in-depth: additionally clamp in ohci_root_intr / ohci_roothub_exec index checks, index > 15.)

VERDICT.md
↓ download raw

DF-5767 β€” VERDICT

Status: untested (hardware-precondition finding β€” not reproducible on the audit guest; code path proven by trace). Reproduced: no. Impact: none demonstrated (device-trust class). Confidence in the defect: certain.

Narrative

sys/bus/u4b/controller/ohci.c:286 latches the number of root-hub ports straight from device MMIO (OHCI_GET_NDP(OREAD4(sc, OHCI_RH_DESCRIPTOR_A)), mask 0xff at ohcireg.h:97) with only a zero-retry guard (ohci.c:283, the AMD756 "reports 0 ports" erratum workaround). The OHCI 1.0a spec limits NDP to 15 and defines the operational register file as 0x00-0xFF with port registers at 0x50 + 4*n (ohcireg.h:113); the driver never clips, and re-latches the value on every resume (ohci_resume ohci.c:461-476 β†’ ohci_controller_init β†’ ohci.c:282-287).

With sc->sc_noport in [16,255]:

  • ohci_root_intr() (ohci.c:1610-1616) reads port-status registers 1..noport on every RHSC interrupt β€” MMIO reads at 0x54..0x474, past the 0x100-byte register file for any noport β‰₯ 44. The idata clamp at ohci.c:1607-1609 is vacuous (noport+1 ≀ 256 = 8*sizeof(sc_hub_idata), ohci.h:251).
  • ohci_roothub_exec() bounds port indices only by sc_noport (ohci.c:2221-2225 CLEAR, 2306-2310 GET, 2324-2328 SET) and then OREAD4/OWRITE4 at OHCI_RH_PORT_STATUS(index) β€” OOB MMIO reads and writes (fixed UPS_* bit patterns, incl. UPS_RESET) up to offset 0x474.
  • The hub descriptor reports bNbrPorts = sc_noport (ohci.c:2279), so the in-kernel uhub thread walks all bogus ports and performs those accesses itself during normal exploration β€” no crafted userspace request needed once the register lies.

Sibling family findings: DF-5268 (xhci CAPLENGTH/DBOFF/RTSOFF, Medium), DF-5282 (ehci CAPLENGTH/N_PORTS, Medium), DF-5296 (uhci BAR size vs 0x14 register file, Low) β€” this is the OHCI instance of the same idiom.

Why it is not reproduced on the guest

  1. QEMU initializes OHCI NDP from the num-ports property and hard-errors above OHCI_MAX_PORTS = 15 (QEMU hw/usb/hcd-ohci.c:1953-1955).
  2. QEMU models HcRhDescriptorA as fully read-only (OHCI_RHA_RW_MASK = 0x00000000, qemu hcd-ohci.c:189; write handler 1791-1792 applies a zero mask), so a guest KLD cannot poison the register either.
  3. The default audit-guest QEMU command line has no USB controller at all (pciconf -l on guest shows none), and NDP > 15 could not occur even with -device usb-ohci added.

Precondition class: malicious/buggy PCI hardware or emulation β€” identical to DF-5296, which was likewise filed untested. A patched QEMU with num-ports > 15 (or a hardware OHCI whose NDP reads > 15) would exhibit the OOB accesses immediately on the first roothub status change; on QEMU's 0x1000-byte OHCI MMIO region the reads return 0 (no fault), so even a patched QEMU shows no crash β€” the class is cross-device MMIO corruption, visible only with a minimal-BAR implementation.

Fix validation

Not performable on this guest (bug cannot be manifested β€” see above); fix.diff provided, fix_status: not_testable. The patch is a two-line clip at the latch site plus optional clamp at the consumers; build-tested mentally against the file (no other consumer of sc_noport permits > 15: sc_devices[] is OHCI_MAX_DEVICES=128 β€” unrelated; hubd.bNbrPorts is uByte β€” 255 fits; DeviceRemovable[32] β€” l/8 ≀ 31 in-bounds, so no secondary overflow exists to fix).

Negative results worth recording (pass-2 classes hunted and killed)

  • Isoc ITD page arithmetic (ohci.c:1900-1965): the flush condition (length & 0xF000) allows ITDs spanning 3 pages (start page offset + accumulated length up to ~0x23FC). OHCI PSW encodes only {page(bp0), page(be)} (bit 12 page-select; confirmed in QEMU ohci_service_iso_td, qemu hcd-ohci.c:694-713), so middle-page frames are mis-mapped into page(be) β€” verified numerically (sim in pack: frames intended for the middle page DMA to be's page, up to ~3 KB past be). All such writes stay inside the transfer's own DMA allocation (page(be) is the buffer's last page; usbd_transfer_setup_sub validates Ξ£frlengths ≀ max_data_length via usb_generic.c:1166-1170, so be never leaves the map; isoc data buffers β‰₯ 8 KB are page-granular bus_dmamem allocations, so past-be is the allocation's own slack). Net: silent frame-data corruption (integrity), NOT kernel memory corruption. Upstream FreeBSD carries identical code (diffed 2026-10-11); not filed as a security finding.
  • ITD field encodings correct: OHCI_ITD_SET_FC(x)=((x)-1)<<24 3-bit count-1 (ohci.h:152), SET_SF 16-bit (ohci.h:147).
  • ntd/nitd allocation formulas (ohci.c:2404-2435) cover worst-case TD consumption (per frame: floor(len/4096)+1 TDs +1 ZLP ≀ 2*nframes + max_data/4096); panic safety nets at ohci.c:1297/1901.
  • ohci_non_isoc_done_sub cbp/be arithmetic (ohci.c:824-838): underflow and overrun guarded by temp > td->len β†’ cc=STALL.
  • ohci_isoc_done plen walk (ohci.c:712-764): nframes>8 clamp at 736, len > *plen β†’ 0 clamp at 748, walk bounded by the ITD chain built at enter time.
  • isoc nframes==0 NULL-deref in ohci_device_isoc_enter (td_last NULL, ohci.c:1968): unreachable β€” usbd_transfer_submit rejects nframes==0 (usb_transfer.c:1748) and the stall_pipe special case completes via usbd_pipe_start without touching HCD methods (usb_transfer.c:2779-2785); usbd_transfer_setup_sub rejects ZERO_NFRAMES (usb_transfer.c:547).
  • ohci_device_done double-invocation: guarded by prev==0 no-op in _ohci_remove_qh (ohci.c:691) + shared-stack did_close/did_transfer serialization; ED (qh_start) non-NULL whenever methods assigned (setup success is a precondition for pipe open).
  • Done-list reaping: this driver does not walk hcca_done_head at all (completion detected by ED head/tail polling, ohci.c:1037-1038); intr_q TAILQ mutation during polling handled by goto repeat (ohci.c:1092-1095). HW-drain protocol = two DMA sets + 1125 us dma delay (ohci.c:2586).
  • Roothub descriptor writes: DeviceRemovable[32] β‰₯ max index 31 β€” no overflow even at NDP=255 (ohci.c:2288-2293, usb.h:610).
  • Port reset poll bounded (ohci.c:2340-2352); SET_ADDRESS bounded to OHCI_MAX_DEVICES (ohci.c:2190); SET_CONFIG ∈ {0,1} (ohci.c:2197).
  • Toggle-carry latch (ohci.c:1050-1055) reads HC-maintained ED headp β€” trusted-by-design, matches upstream.
  • Detach-veto family: lives in ohci_pci.c (DF-5562, already filed); this file's ohci_detach (ohci.c:429-445) is post-children teardown only.

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

fix.diff (git-apply clean) clips sc_noport to 15 inside the re-read loop, covering boot and resume latch paths; validation kernel build/boot not performed because the bug cannot be manifested on this guest (QEMU NDP hard cap 15 + read-only HcRhDescriptorA), so baseline==patched==no-observable-change by construction.

findings/poc/DF-5767/fix.diff
↓ fix.diffper-fix-DF-5767

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5767/README.md (full trace), VERDICT.md (narrative incl. killed pass-2 candidates), fix.diff (applies cleanly, NDP clip to 15), qemu-hcd-ohci-reference.c:189/1791/1953 (QEMU caps+RO register proving untestability), env.txt (guest has no USB controller), itd_offset_sim.py (negative-result proof for the isoc ITD page-math class)

PoC changes

no PoC seed existed; no trigger written because the precondition (controller NDP>15) cannot be instantiated on the audit guest's QEMU (num-ports capped at 15, HcRhDescriptorA read-only, no USB controller on default cmdline)

Verified recommended fix

Clip latched NDP to the OHCI 1.0a maximum of 15 at the latch site (ohci.c:286, both boot and resume paths) with a device_printf warning; defense-in-depth: clamp port indices to 15 in ohci_root_intr/ohci_roothub_exec checks.

Verdict

ohci_controller_init (sys/bus/u4b/controller/ohci.c:282-287) latches the device-reported root-hub port count OHCI_GET_NDP(OREAD4(OHCI_RH_DESCRIPTOR_A)) with an 8-bit mask (ohcireg.h:97) and no upper-bound check β€” the retry loop only retries on zero (AMD756 erratum) β€” and re-latches it on every resume (ohci.c:462-476). The OHCI 1.0a spec caps NDP at 15 and the operational register file at 0x100 bytes (port regs at 0x50+4n, ohcireg.h:113). With NDP in [16,255]: ohci_root_intr (ohci.c:1610-1616) OREAD4s port status 1..noport (up to 0x474) on every RHSC interrupt; ohci_roothub_exec bounds port indices only by sc_noport (ohci.c:2221/2306/2324) and issues OOB MMIO reads AND writes (fixed UPS_* bit patterns incl. UPS_RESET) up to 0x474; the hub descriptor propagates bNbrPorts=sc_noport (ohci.c:2279) so the kernel uhub thread drives these OOB accesses itself during port exploration. Same register-geometry-trust family as DF-5268/DF-5282/DF-5296. Not live-testable on the audit guest: QEMU hard-caps OHCI num-ports at 15 (hw/usb/hcd-ohci.c:1953-1955) and models HcRhDescriptorA fully read-only (OHCI_RHA_RW_MASK=0, hcd-ohci.c:189/1791-1792), and the default guest QEMU config has no USB controller at all β€” identical posture to family sibling DF-5296 (filed untested).