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

orm_identify() scan loop lacks a MAX_ROMS bound β€” latent OOB write past the 196-byte orm_softc (rid[]/res[]), currently unreachable because the ISA bus caps SYS_RES_MEMORY rids at ISA_NMEM=8

Summary

rnum is unbounded while the arrays hold 16 - with >=17 signatures the writes go past the softc (and rid[16] aliases res[0]). GUEST-VERIFIED containment: an injected 47-signature option ROM produced exactly 8 regions (the ISA_NMEM=8 clamp holds, headers proven present-but-skipped at 0xcf800/0xe2800); the coupling is two constants in different files - one raise makes it a live boot-time heap overflow. Explicit bound + hole-clamp fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5156 Β· 13 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
gen_orm_rom.py β€” 900 B view raw
orm-evil.rom β€” 95.0 KB ↓ download
build.sh β€” 77 B view raw
run.sh β€” 153 B view raw
run.log β€” 657 B view raw
run.2.log β€” 271 B view raw
seabios-attempts.txt β€” 1.0 KB view raw
seabios-attempt2-debugcon.log β€” 5.1 KB view raw
env.txt β€” 268 B view raw
fix.diff β€” 927 B view raw
verdict.json β€” 3.8 KB view raw

DF-5156 β€” orm(4) identify loop missing MAX_ROMS bound β€” PoC evidence pack

What this tests

orm_identify() (sys/dev/misc/orm/orm.c:104-150) fills sc->rid[]/sc->res[] (MAX_ROMS=16 entries, orm.c:56-62) once per valid option-ROM signature (55 AA, size byte multiple of 4) found while scanning the ISA hole (0xa0000-0xfffff, 2KB steps). The loop condition is only chunk < IOMEM_END (orm.c:104) β€” there is no sc->rnum < MAX_ROMS check before the writes at orm.c:146-147. If more than 16 signatures exist, the kernel writes kernel pointers/ints past the 196-byte orm_softc (heap overflow at boot), and &sc->rid[16] aliases &sc->res[0], corrupting the already-stored resource pointers.

Threat model: attacker-controlled firmware / option-ROM content delivered into the ISA hole (flashed PCI/ISA option ROM, VM firmware image).

How to reproduce (what was done here)

  1. python3 gen_orm_rom.py β€” builds orm-evil.rom (97,280 bytes): outer header 55 AA BE (0xBE&3==2 β†’ the orm scan itself skips the base but SeaBIOS copies the full file), an immediate RETF (0xCB) at offset 3 of every 2KB block (SeaBIOS enters ISA ROMs at seg:0003 β€” without this the zero body gets executed and hangs the BIOS), and 55 AA 04 (2KB "ROM") at each internal 2KB boundary β†’ 47 fake ROM signatures.
  2. Boot the audit guest with the ROM injected (QEMU 7.2, SeaBIOS):

    cd dfbsd-qemu QEMU_EXTRA="-option-rom /home/maxx/dfbsd/dfbsd/findings/poc/DF-5156/orm-evil.rom" ./vm.sh up

  3. Observe dmesg | grep orm / devinfo -r / memory dump.

Expected (per the unbounded code) vs actual

  • Expected if the bound were reachable: rnum reaches 47(+1) β†’ 31 pairs of OOB writes past the softc (sc->rid[16..47], sc->res[16..47]), likely panic during autoconf; &rid[16+i] corrupting res[i] low halves.
  • Actual (2 runs): orm0: <ISA Option ROMs> at iomem 0xcb800-...-0xcf7ff β€” exactly 8 regions, guest boots and runs normally, no panic. Headers are verifiably present in RAM at 0xcb800, 0xcf800 and 0xe2800 (run.log) β€” the scan saw them and silently stopped recording at 8.

Why it does not manifest (root cause of the containment)

The ISA bus layer caps memory rids below orm's array size:

  • isa_set_resource() (sys/bus/isa/isa_common.c:897-899) returns EINVAL for SYS_RES_MEMORY with rid >= ISA_NMEM (8) β€” and orm.c:105-106 ignores the return value of bus_set_resource().
  • With no resource-list entry, bus_alloc_resource_any() β†’ resource_list_alloc() (sys/kern/subr_bus.c:2693-2695) returns NULL β†’ orm.c:141-144 silently skips the region (chunk += IOMEM_STEP).
  • Hence sc->rnum ≀ 8 < MAX_ROMS=16 on every boot. The only ISA implementation in the tree is sys/bus/isa/x86_64/isa.c (platforms: pc64, vkernel64; no i386), so this holds on all supported DragonFly platforms.

Files

  • gen_orm_rom.py β€” option-ROM generator (the PoC source)
  • orm-evil.rom β€” generated image (sha256 ff6aa6fe…324bd)
  • run.log, run.2.log β€” decisive guest outputs (2 independent boots)
  • seabios-attempts.txt β€” SeaBIOS debug-port captures of the failed attempts
  • env.txt β€” guest/QEMU identity
  • fix.diff β€” defense-in-depth bound check (not applied; bug latent)
  • verdict.json, manifest.json
VERDICT.md
↓ download raw

DF-5156 VERDICT β€” NOT REPRODUCED (containment proven at runtime)

Bottom line: the missing sc->rnum < MAX_ROMS bound in orm_identify() is real as written (orm.c:104-150 writes sc->rid[sc->rnum]/sc->res[sc->rnum] at orm.c:146-147 with no bound), but the overflow cannot be reached on any supported DragonFly platform: the only ISA bus implementation (sys/bus/isa/x86_64/isa.c, platforms pc64+vkernel64) silently refuses to create SYS_RES_MEMORY resource-list entries with rid β‰₯ ISA_NMEM(8), which is below MAX_ROMS(16). The PoC injected 48 valid option-ROM signatures into the ISA hole via QEMU -option-rom; the driver recorded exactly 8 regions and the guest stayed healthy across two independent boots.

Reproduction narrative (attempts)

  1. 121,856-byte ROM β€” SeaBIOS did not copy it (no space left in the option ROM area after the VGA BIOS/kvmvapic; nothing appeared in the hole).
  2. 97,280-byte ROM, no entry stub β€” SeaBIOS enters ISA ROMs at seg:0003 (the init vector at offset 3 is ignored on this path). The zero body was executed, IP walked off the end of the ROM and the machine hung pre-boot (empty serial log; SeaBIOS debug stream ends right after "Running option rom at cb00:0003"). Fixed by placing 0xCB (RETF) at offset 3 of every 2KB block β€” see seabios-attempts.txt.
  3. 97,280-byte ROM with RETF stubs (the final PoC) β€” boots cleanly:

    orm0: at iomem 0xcb800-0xcbfff,0xcc000-0xcc7ff, 0xcc800-0xccfff,0xcd000-0xcd7ff,0xcd800-0xcdfff,0xce000-0xce7ff, 0xce800-0xcefff,0xcf000-0xcf7ff on isa1

  • devinfo -r shows the same 8 memory ranges (devinfo does NOT truncate; the dmesg printer caps at ISA_NMEM=8 too β€” isa_common.c:575-577 β€” but devinfo reads the resource list directly, so 8 is the true count).
  • /dev/mem dump proves the signatures were present and unscanned past the 8th: 55 aa 04 cb at 0xcb800 (recorded), 0xcf800 (9th, NOT recorded), 0xe2800 (47th, NOT recorded).
  • Repeat boot: identical result.

Why rnum cannot exceed 8 (line-by-line)

  • orm.c:105 bus_set_resource(child, SYS_RES_MEMORY, sc->rnum=8, …) β†’ BUS_SET_RESOURCE(isa) β†’ isa_set_resource() isa_common.c:897-899: if (type == SYS_RES_MEMORY && rid >= ISA_NMEM) return EINVAL; β†’ no resource-list entry is created (and orm ignores the return value).
  • orm.c:108/140 bus_alloc_resource_any(child, SYS_RES_MEMORY, &rid=8, …) β†’ isa_alloc_resource() x86_64/isa.c:91 (isdefault=1 skips the rid-cap branch) β†’ resource_list_alloc() subr_bus.c:2693-2695: rle = resource_list_find(rl, type, *rid); if (!rle) return 0; β†’ NULL.
  • orm.c:141-144: res==NULL β†’ delete (no-op) β†’ chunk += IOMEM_STEP β†’ the 2KB region is silently skipped; rnum stays 8. Every later signature fails identically (rid is pinned at 8).

Secondary observations (killed / downgraded)

  • Zero-size header (55 AA 00) β€” rom_size=0 would make chunk += 0 (orm.c:149) and, if a 0-length alloc succeeded, loop forever while writing OOB. Killed: resource_list_alloc() clamps count = ulmax(count, rle->count) = max(1,0) = 1 (subr_bus.c:2690-2691), so a 1-byte region is allocated once and the next 2KB probe at the same chunk fails (overlap) β†’ the scan advances. No infinite loop.
  • Resource range overreach β€” a signature near the top of the hole (e.g. 55 AA FC at 0xff800) reserves [0xff800, 0x11d7ff), past IOMEM_END into physical RAM above 1MB. Harmless resource overreach at boot (no OOB access); folded into the finding as a hardening note.
  • No copyin/uiomove, no privilege surface, no runtime entry point (identify runs once at boot; rescans no-op via the DS_ATTACHED check at orm.c:92).

Impact

None demonstrated and none reachable on supported platforms. The finding is filed as Info (defense-in-depth): the missing bound is a latent kernel heap overflow that becomes live the moment ISA_NMEM is raised above MAX_ROMS (two constants in different files, no compile-time or runtime tie between them). fix.diff adds the explicit bound.

Fix validation

Not performed β€” memory corruption did not reproduce (containment proven), so the mandatory rebuild-and-test cycle does not apply. fix.diff is a defense-in-depth one-liner authored against the read-only sys/ tree.

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 validation not performed: memory corruption did not reproduce (containment proven at runtime), so the mandatory rebuild-and-test cycle does not apply. fix.diff is a defense-in-depth bound check authored against the read-only sys/ tree and verified to apply cleanly with patch(1).

n/a
↓ fix.diffper-fix-DF-5156

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log/run.2.log: dmesg 'orm0: <ISA Option ROMs> at iomem 0xcb800-...-0xcf7ff' = exactly 8 regions + /dev/mem dump proving 55 aa 04 cb headers exist at 0xcb800, 0xcf800 (9th, unrecorded) and 0xe2800 (47th, unrecorded); VERDICT.md: line-by-line containment trace (isa_set_resource isa_common.c:897-899 -> resource_list_alloc subr_bus.c:2693-2695 returns 0 -> orm.c:141-144 silent skip); seabios-attempts.txt: the two failed PoC iterations and why.

PoC changes

Authored the PoC from scratch (pass 1 filed nothing): learned (1) QEMU >=7.2 spells it -option-rom, (2) SeaBIOS enters ISA ROMs at seg:0003 regardless of the init vector at offset 3 - every 2KB block needs an immediate RETF (0xCB) or the zero body gets executed and the machine hangs pre-boot, (3) the outer size byte must have granularity != 0 (mod 4) so the orm scan itself skips the base while SeaBIOS still copies the whole file, (4) the option ROM must fit the ~120KB SeaBIOS window or it is silently not copied.

Verified recommended fix

Add explicit 'if (sc->rnum >= MAX_ROMS) break;' at the top of the orm_identify scan loop and clamp rom_size to keep the reserved range inside the ISA hole.

Verdict

The missing sc->rnum < MAX_ROMS bound in orm_identify() (orm.c:104-150, writes at orm.c:146-147) is real code but unreachable on every supported DragonFly platform: the only ISA implementation (sys/bus/isa/x86_64/isa.c; platforms pc64+vkernel64) silently rejects SYS_RES_MEMORY rids >= ISA_NMEM(8) in isa_set_resource (isa_common.c:897-899), and orm ignores bus_set_resource's return value, so rnum can never exceed 8 < 16. Verified empirically: a crafted option ROM injected via QEMU -option-rom placed 48 valid 55 AA signatures in the ISA hole (verified present in RAM at 0xcb800/0xcf800/0xe2800); the driver recorded exactly 8 regions (devinfo + dmesg, 2 boots) and the guest stayed healthy. No OOB write, no panic. Filed as Info defense-in-depth: the latent heap overflow becomes live if ISA_NMEM is ever raised above MAX_ROMS (two constants in different files with no tie between them).