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

Device-controlled sc->port_id indexes rxf.port[2]/port[4] unchecked in copy_stats_to_sc_be2/_be3 β€” OOB read past the stats DMA buffer into world-readable sysctls

Summary

port_id is a raw firmware dword stored unvalidated and used as the port[] array index against 2/4-entry arrays. ~66 dwords per refresh are copied into hw.oceN.stats.* sysctls readable by any user; port_id beyond the struct runs off the exactly-sized stats DMA allocation into adjacent heap. Exact-geometry replica (port_id 4/5 already past both array and buffer). NELEM-clamp fix (2 hunks).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4387 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.3 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
trigger.c β€” 1.5 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
run.log β€” 654 B view raw
fix.diff β€” 881 B view raw
env.txt β€” 478 B view raw
manifest.json β€” 793 B view raw
verdict.json β€” 2.0 KB view raw

DF-4387 β€” Device-controlled sc->port_id indexes rxf.port[] unchecked in the BE2/BE3 stats copy (OOB read β†’ world-readable sysctls)

~66 dwords are then copied from port_stats into sc->oce_stats_info, whose fields are published world-readable under hw.oceN.stats.rx.* / hw.oceN.stats.tx.*. port_id β‰₯ 2 (BE2) / β‰₯ 4 (BE3) reads past the port array; a large enough port_id reads past the entire mbx_get_nic_stats{,_v0} DMA allocation (oce_stats_init sizes stats_mem to exactly the struct) into adjacent kernel heap β€” and those bytes land in world-readable sysctls.

Build / run

./run.sh β€” hardware gate + replica printing the read offsets for port_id 0..5 against the real array/struct geometry.

Expected

port_id=4: read at rxf+1056 [PAST port[] ARRAY] [PAST mbx_get_nic_stats DMA BUFFER]-style lines. Kernel confirmation needs an Emulex adapter whose QUERY_FW_CONFIG returns port_id β‰₯ array bound.

VERDICT.md
↓ download raw

DF-4387 VERDICT

Trace

  1. oce_mbox_get_fw_config() stores the raw firmware dword: sc->port_id = fwcmd->params.rsp.port_id (sys/dev/netif/oce/oce_mbox.c:493) β€” no mask, no range check anywhere in the driver (other uses pass it back to the device at oce_mbox.c:1063,1879).
  2. oce_refresh_nic_stats() β†’ copy_stats_to_sc_be2() / copy_stats_to_sc_be3() index with it: - sys/dev/netif/oce/oce_sysctl.c:1151 β€” rxf.port[port], oce_rxf_stats_v0.port[2] (oce_hw.h:2943) - sys/dev/netif/oce/oce_sysctl.c:1216 β€” rxf.port[port], oce_rxf_stats_v1.port[4] (oce_hw.h:3004) Both then copy ~26 port fields plus rxf_stats/pmem fields into sc->oce_stats_info.u0.be.
  3. Exposure: every copied field is a CTLFLAG_RD sysctl node created at attach (oce_add_stats_sysctls_be3, oce_sysctl.c:538-752) β€” readable by any user (sysctl hw.oceN.stats.rx.err.crc_errs etc.).
  4. Allocation: oce_stats_init() (oce_sysctl.c:1268-1284) sizes the DMA block to exactly sizeof(struct mbx_get_nic_stats_v0) / sizeof(struct mbx_get_nic_stats), so the OOB read runs off the end of the oce_dma_alloc mapping into adjacent kernel heap once port_id outruns the struct (replica arithmetic in run.log: port_id 4 already clears both the port[] array and the rxf-sized buffer model).

Threat model: device-controlled index (malicious/compromised NIC or DMA), unprivileged reader harvests the leaked dwords. Missing-validation class, same family as the DF-1576 device-trust pattern.

Verification (Phase V)

Kernel path hardware-gated (no 19a2: device, no hw.oce tree β€” run.log). Demonstrated: exact-geometry replica (trigger.c, guest run in run.log) showing which port_id values cross which boundary. fix.diff clamps port_id to NELEM(rxf.port) at both copy functions; git apply --check OK; patched file syntax-validated on the guest kernel toolchain (0 errors).

Bottom line

Certain code-wise, device-gated exploit-wise: Low. Two-line defense-in-depth clamp; worth fixing alongside any broader "validate fw_config" hardening.

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

not_testable at runtime (no oce NIC); fix.diff (2 hunks) git-apply OK, patched oce_sysctl.c syntax-validated with guest kernel toolchain (0 errors).

fix.diff; guest cc -fsyntax-only of patched oce_sysctl.c: 0 errors.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (guest replica: port_id=4/5 past array+buffer), VERDICT.md trace, fix.diff (git apply --check OK, syntax-validated)

PoC changes

n/a - pass-2 finding authored fresh (added stddef.h after first guest compile)

Verified recommended fix

clamp port to NELEM(rxf.port) in copy_stats_to_sc_be2/_be3 (fix.diff)

Verdict

sc->port_id is a raw firmware dword (oce_mbox.c:493, unvalidated) used to index rxf.port[2] (be2, oce_sysctl.c:1151) / rxf.port[4] (be3, oce_sysctl.c:1216); the ~66 dwords read per refresh land in world-readable hw.oceN.stats.* sysctls. port_id beyond the array reads wrong/past fields, and beyond the struct runs off the stats_mem DMA allocation (sized to exactly the mbx struct) into adjacent kernel heap -> unprivileged-readable device-shaped leak. Hardware-gated on guest; replica arithmetic in run.log. Low (malicious-peripheral class); fix is a NELEM clamp at both sites.