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)
PoC verification
Evidence pack
findings/poc/DF-5156 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)
python3 gen_orm_rom.pyβ buildsorm-evil.rom(97,280 bytes): outer header55 AA BE(0xBE&3==2 β the orm scan itself skips the base but SeaBIOS copies the full file), an immediateRETF(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), and55 AA 04(2KB "ROM") at each internal 2KB boundary β 47 fake ROM signatures.-
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
-
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]corruptingres[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 forSYS_RES_MEMORYwithrid >= ISA_NMEM(8) β and orm.c:105-106 ignores the return value ofbus_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=16on every boot. The only ISA implementation in the tree issys/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 attemptsenv.txtβ guest/QEMU identityfix.diffβ defense-in-depth bound check (not applied; bug latent)verdict.json,manifest.json
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)
- 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).
- 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 β seeseabios-attempts.txt. -
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 -rshows 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 cbat 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 makechunk += 0(orm.c:149) and, if a 0-length alloc succeeded, loop forever while writing OOB. Killed:resource_list_alloc()clampscount = 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_testableFix 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
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).
No comments yet.