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

crom_parse_text bounds check missing sizeof(u_int32_t) multiplier allows OOB heap read past ROM buffer

Summary

crom_parse_text checks (vm_offset_t)textleaf + textleaf->crc_len > CROM_END(cc) but crc_len is quadlet count not byte count - check missing * sizeof(u_int32_t) factor and is 4x too permissive. Text leaf near buffer end passes check but text data reads up to hundreds of bytes past ROM buffer leaking kernel heap into user-visible SCSI inquiry strings. For leaf at buffer+1000 with crc_len=10: check passes (1000+10=1010<=1023) but textleaf->text[0..7] reads buffer+1012 through buffer+1043 up to 19 bytes past buffer. Works even with default info_len=4.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2291 Β· 12 files
FileTypeDescriptionSize
fwcrom_rig.c β€” 11.6 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
run.log β€” 2.6 KB view raw
run.2.log β€” 2.6 KB view raw
run.3.log β€” 2.6 KB view raw
fix.diff β€” 1.1 KB view raw
env.txt β€” 2.0 KB view raw
VERDICT.md β€” 4.9 KB ↓ raw
README.md β€” 1.9 KB ↓ raw
manifest.json β€” 1.7 KB view raw
verdict.json β€” 5.8 KB view raw

DF-2291 β€” crom_parse_text bounds check missing sizeof(u_int32_t) β†’ OOB read past ROM buffer

Status: REPRODUCED (verbatim-code rig) β€” impact class: kernel info leak (OOB read), independent of DF-2290 (triggers with the default info_len = 4). Fix: A/B-validated in the same rig (fix.diff hunk 2).

What this pack contains

Same layout as the DF-2290 pack (fwcrom_rig.c, build.sh, run.sh, full logs, fix.diff, env.txt). The rig and build scripts are identical; run.sh drives the df2291_* modes.

How to reproduce

./build.sh
./run.sh

Expected output (baseline = unmodified production code)

RIG mode=df2291_guard result=OOB_FAULT_CONFIRMED
  ... fault address is 0 bytes past the buffer end        # SIGSEGV reading textleaf->text[3] at byte 1024
RIG mode=df2291_canary result=OOB_LEAK_CONFIRMED
  crom_parse_text output (the buffer that becomes sdev->vendor/product, sbp.c:606,621):
    ab0de000 ab0de001 ab0de002 c0ffee00 c0ffee01 c0ffee02 c0ffee03 c0ffee04
  words 3..7 = canary 0xC0FFEE00..04 read from bytes 1024..1043, PAST the 1024-byte buffer
RIG mode=control result=PARSED_INBOUNDS                   # rig sanity

With fwcrom_rig_fixed: NULLSTR_NO_FAULT (leaf rejected by bounds check: out="(null)") for both attack modes; control still PARSED_INBOUNDS (no regression).

Attacker input (the crafted ROM)

Default bus-info block (info_len = 4), root directory at byte 20 with one entry {key=CROM_TEXTLEAF(0x81), val=244} pointing at a text leaf placed at byte 1000 whose crc_len = 10 quadlets. The buggy check at fwcrom.c:207 tests 1000 + 10 <= 1023 (passes) but the read loop then consumes text[0..7] = bytes 1012..1043 β€” 20 bytes past the 1024-byte buffer. The 5 canary words that land in the output string are the proof that adjacent-kernel-heap contents flow into the buffer that becomes the SCSI INQUIRY vendor/product string in production (sbp.c:606,621).

VERDICT.md
↓ download raw

VERDICT β€” DF-2291 (verify mode)

status: reproduced Β· reproduced=1 Β· impact=leak Β· confidence=certain rig: verbatim-code (guest has no FireWire hardware/KLD β€” see env.txt)

Claim under test

crom_parse_text() (sys/bus/firewire/fwcrom.c:187-225) validates the text leaf with

if ((vm_offset_t)textleaf + textleaf->crc_len > CROM_END(cc))   /* fwcrom.c:207 */

but crc_len is a QUADLET count (see crom_add_simple_text, fwcrom.c:459: tl->crc_len = howmany(sizeof(struct csrtext) + len, sizeof(u_int32_t))), while the read loop consumes (crc_len - 2) quadlets of textleaf->text[] (fwcrom.c:215-219), i.e. a byte extent of 12 + (crc_len-2)*4 = 4*crc_len + 4. The check is therefore ~4x too permissive; a leaf near the buffer end passes validation and the loop reads past the 1024-byte ROM with the default info_len = 4 β€” no dependency on DF-2290.

How it was verified

Same verbatim rig as DF-2290 (production parser linked unmodified; sha256 in build.log).

  1. df2291_guard β€” ROM: info_len=4, root dir @20 with one entry {key=0x81 CROM_TEXTLEAF, val=244} β†’ leaf at byte 1000, crc_len=10. Buggy check: 1000 + 10 <= CROM_END(1023) passes. Loop reads text[0..2] (bytes 1012..1023, in-bounds) then faults reading text[3] at exactly buffer_end+0 (SIGSEGV, si_addr == rom+1024). Run 3/3 identical. β†’ the read crosses the object boundary by up to 20 bytes (last byte at 1043).
  2. df2291_canary β€” same ROM, canary page after the buffer. Output of crom_parse_text (the exact buffer that in production is sdev->vendor / sdev->product, sbp.c:606,621): ab0de000 ab0de001 ab0de002 c0ffee00 c0ffee01 c0ffee02 c0ffee03 c0ffee04 β€” words 0..2 are the in-buffer markers, words 3..7 are canary read from bytes 1024..1043, i.e. adjacent-memory contents copied into the user-visible string. The leak primitive is demonstrated end-to-end at parser level.
  3. control β€” in-bounds leaf (byte 512, crc_len=6) parses correctly, guard page untouched β†’ rig sanity.

Production context: hostile FireWire device's config ROM β†’ fw_device.csrrom[1024/4] (firewirereg.h:52-54, filled at firewire.c:1500 host-endian) β†’ sbp target attach calls crom_parse_text(cc, sdev->vendor, sizeof(sdev->vendor)) and ..._product (sbp.c:602-621) β†’ SCSI INQUIRY data readable by any local user with access to the sbp LUN. Ceiling: the copy is clamped by the destination (len), and in the kernel path len = sizeof(sdev->vendor) = 32 (sbp.c:187,606) β†’ up to 32 bytes of adjacent kernel heap per text leaf reach the INQUIRY string (a leaf near CROM_END makes ALL 32 bytes out-of-bounds-sourced; the read fault extent reaches ~43 bytes past the buffer). crom_parse_text is called for both vendor and product (and the revision loop walks more leaves), and a hostile ROM may contain many text leaves β€” each swept with the broken bound. The userspace fwcontrol/crom_desc path passes larger len, reading further. Read-only bug β†’ leak class; Medium/kernleak as filed stands.

Fix A/B (same rig, fix.diff hunk 2)

Baseline reproduces (above). With hunk 2 applied (bound the leaf's full byte extent: textleaf + (crc_len + 1) * sizeof(u_int32_t) > CROM_END): both attack modes return the safe "(null)" string, no fault, no canary; control still parses (PARSED_INBOUNDS) β€” the stricter check accepts every legitimately-placed leaf because crom_load() emits leaves whose quadlet extent is crc_len (fwcrom.c:459,521-523). The +1 term covers the loop's 4*crc_len + 4 reach exactly (one byte conservative in the boundary case, which is unreachable for well-formed leaves).

Honesty notes

  • Same userspace-rig caveat as DF-2290: kernel adjacency + INQUIRY leg is source-cited, not executed (no FireWire hardware on the audit guest).
  • The finding's arithmetic ("reads buffer+1012 through buffer+1043, up to 19 bytes past") is confirmed modulo the exact struct size: struct csrtext header is 12 bytes (BIT16x2 + spec + lang, iec13213.h:148-159), so text starts at leaf+12; for a leaf at 1000 the read extent is 1012..1043 β†’ 20 bytes past the 1024-byte buffer end. Immaterial to the verdict.
  • Interaction: DF-2290's fix is NOT required for this one (default info_len), but with both unfixed a hostile ROM can chain them (DF-2290 to widen CROM_END, DF-2291 to sweep larger windows past the buffer).

Bottom line

Confirmed as filed: the crc_len-as-bytes check at fwcrom.c:207 lets a hostile text leaf read past fw_device.csrrom (with DF-2290 chained, far past it), with the leaked bytes landing in SCSI INQUIRY strings. One-line fix, A/B validated, no regression.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/B in the same verbatim rig: baseline reproduces both the guard-page fault (SIGSEGV at buffer_end+0) and the canary leak into the output string; with fix.diff hunk 2 applied, crom_parse_text returns '(null)' for the hostile leaf in BOTH attack modes with zero out-of-bounds access, and the in-bounds control leaf still parses identically (PARSED_INBOUNDS, no regression -- the +1 term conservatively covers the loop's 4*crc_len+4 reach). Kernel rebuild not meaningful: parser unreachable on the guest (no FireWire hardware/KLD); the rig compiles the identical production source byte-for-byte.

run.log PATCHED section: 'RIG mode=df2291_guard result=NULLSTR_NO_FAULT (leaf rejected by bounds check: out="(null)")' + same for df2291_canary + 'RIG mode=control result=PARSED_INBOUNDS'; build.log: 'Hunk #2 succeeded at 217.'
↓ fix.diffper-fix-DF-2291

Confirmed kernel references

Detail

Exploit chain

hostile FireWire device -> config ROM stored host-endian in fw_device.csrrom[1024/4] (firewire.c:1500, firewirereg.h:52-54) -> sbp target attach: crom_search_key finds the CROM_TEXTLEAF entry (sbp.c:602-605) -> crom_parse_text(cc, sdev->vendor, 32) passes the quadlet-count-as-bytes check at fwcrom.c:207 and copies up to 32 bytes starting at leaf+12 (fwcrom.c:214-219) -> a leaf placed at bytes ~1000-1023 sources those bytes from past the buffer (kernel heap adjacent to the kmalloc'd fw_device, firewire.c:1311) -> bcopy into the SCSI INQUIRY response (sbp.c:1527-1528) readable by a local user. Rig proves the parser-level read past the object and the content flow into the string buffer; kernel adjacency and INQUIRY legs are source-cited (no FireWire hardware on the audit guest).

Evidence (decisive lines)

run.log (all 3 runs identical): 'RIG mode=df2291_guard result=OOB_FAULT_CONFIRMED ... fault address is 0 bytes past the buffer end' and 'RIG mode=df2291_canary result=OOB_LEAK_CONFIRMED ... ab0de000 ab0de001 ab0de002 c0ffee00 c0ffee01 c0ffee02 c0ffee03 c0ffee04 ... words 3..7 = canary read from bytes 1024..1043, PAST the 1024-byte buffer'; PATCHED cells: 'NULLSTR_NO_FAULT (leaf rejected by bounds check: out="(null)")'; control cells PARSED_INBOUNDS on both binaries.

PoC changes

No seed pack existed (poc_path was empty); rig authored from scratch in verify mode. Iterations: (1) added for ptrdiff_t under gcc 8.3; (2) fix.diff regenerated mechanically after the guest's GNU patch 2.0 rejected hand-counted hunks; (3) '(null)' outcome classified as NULLSTR_NO_FAULT in both guard and canary modes so the fixed binary's behavior reports cleanly. Trigger matches the finding (leaf at 1000, crc_len=10, default info_len=4); struct math corrected: struct csrtext header is 12 bytes, so text spans 1012..1043 (20 bytes past), vs 19 stated in the finding -- immaterial.

Verified recommended fix

crom_parse_text: compare the leaf's full byte extent -- (vm_offset_t)textleaf + (textleaf->crc_len + 1) * sizeof(u_int32_t) > CROM_END(cc) -- instead of adding the raw quadlet count; see fix.diff hunk 2.

Verdict

REPRODUCED via verbatim-code rig on the QEMU guest (no FireWire hardware/KLD; verbatim-rig evidence class). The rig links the UNMODIFIED production parser /usr/src/sys/bus/firewire/fwcrom.c (sha256 b38ce9b5..., byte-identical to the audited tree) and parses a hostile ROM with the DEFAULT info_len=4 (independent of DF-2290): root directory at byte 20 with one text-leaf entry pointing at byte 1000, crc_len=10 quadlets. The check at fwcrom.c:207 compares 1000+10<=1023 (passes) because crc_len is treated as bytes although it is a quadlet count (crom_add_simple_text, fwcrom.c:459); the loop at fwcrom.c:215-219 then reads text[0..7] = bytes 1012..1043. Guard-page run: SIGSEGV at exactly buffer_end+0 (text[3] read at byte 1024). Canary run: the crom_parse_text output -- the exact buffer that becomes sdev->vendor/product (sbp.c:606,621) -- contains words c0ffee00..c0ffee04 read from bytes 1024..1043 past the 1024-byte object, i.e. adjacent-memory contents are copied into the user-visible string. In production the copy is clamped by sizeof(sdev->vendor)=32 (sbp.c:187) and lands in SCSI INQUIRY output (sbp.c:1527-1528); fwcontrol's userspace crom_desc path passes larger len and reads further. Read-only primitive: leak ceiling, consistent with Medium/kernleak. fix.diff hunk 2 (bound (crc_len+1)*4 bytes) A/B-validated: leaf rejected to '(null)', zero OOB, no regression.