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

crom_next() dereferences the peer-derived nested-directory pointer (reg + 24-bit val, forward up to +64 MB) before any bounds check β€” OOB kernel read and deterministic kernel panic from a malicious FireWire device's Config ROM

Summary

On CSRTYPE_D entries the sub-directory pointer is computed from the entry's 24-bit value and immediately loaded (&& evaluates left-first) BEFORE the CROM_END bound check - never validated at all: a peer controls the whole 1024-byte ROM and can aim the load at csrrom+24+4*V for any V up to 0xffffff (+64 MB). Mapped target = silent foreign-memory read + a 1-bit oracle per probe (the bound-check console line iff the foreign word is nonzero); unmapped = Fatal trap 12. Distinct from all 6 knowns (root-ptr ~1KB / text-leaf / stack-write families - none covers the nested-directory 64MB reach). Production-code KLD + guard-page userspace model: V=300 faults at the exact predicted load window, V=0xffffff at +64MB, slab-groom oracle lines 4x, in-kernel +4..64MB sweep -> panic at dfh_crom_next+0x53; bounds-check-before-assign fix validated (no fault, oracle 0, no panic). Read-only primitive - no OOB write arises.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5497 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 5.8 KB ↓ raw
verdict.json β€” 6.6 KB view raw
df5497.c β€” 6.5 KB view raw
Makefile β€” 702 B ↓ download
c5497_user.c β€” 4.1 KB view raw
build.sh β€” 144 B view raw
run.sh β€” 222 B view raw
build.log β€” 5.9 KB view raw
fix_build.log β€” 5.9 KB view raw
run.log β€” 4.4 KB view raw
panic.txt β€” 1.0 KB view raw
fix_run.log β€” 3.5 KB view raw
env.txt β€” 280 B view raw
fix.diff β€” 643 B view raw

DF-5497 β€” crom_next() dereferences the device-derived nested-directory pointer before any bounds check (OOB kernel read up to +64 MB; kernel panic)

File: sys/bus/firewire/fwcrom.c β€” pass 2 (GLM 5.3 re-audit).

Build (guest, root; requires /usr/src, i.e. vm.sh reset with-src)

sh build.sh          # KLD df5497.ko (production fwcrom.c #included, symbols
                      # renamed via -D) + userspace guard-page model c5497_user
sh /root/df5498/build.sh   # (sibling model, see DF-5498 pack)

Run

./c5497_user                 # userspace model: production parser vs guard-page ROM
kldload ./df5497.ko
sysctl kern.df5497_mode=1    # oracle: OOB reads +1.2..16 KB past a kmalloc(1024) ROM
sysctl kern.df5497_fire=1
sysctl kern.df5497_mode=2    # sweep: descents +4..64 MB -> EXPECT KERNEL PANIC
sysctl kern.df5497_fire=1

Expected (baseline, unpatched /usr/src)

  • userspace model: subtest A (V=300) SIGSEGV at the predicted rom+1224..1227 load (200 bytes into the guard page past the 1024-byte ROM β€” the load happens BEFORE the CROM_END comparison that would have rejected the directory); subtest B (V=0xffffff) SIGSEGV at rom+0x4000014 (the full 24-bit, +64 MB reach); control C (V=100, in-ROM directory) walks cleanly.
  • KLD mode 1: crom_next: bound check failed oracle lines β€” the production parser read crc_len out of groomed heap neighbours outside the ROM.
  • KLD mode 2: Fatal trap 12 / panic with RIP inside crom_next (observed: dfh_crom_next+0x53), guest goes down β€” see panic.txt.

Fix validation

fix.diff applied inside the guest /usr/src, module and model rebuilt: subtests A/B walk cleanly (no fault), oracle lines 4 -> 0, mode 2 completes with no panic and the guest stays up (fix_run.log).

Hardware-gated entry (no FireWire controller in QEMU): the harness feeds the production parser a device-controlled 1024-byte Config ROM exactly as fw_bus_explore() fills fwdev->csrrom[] and sbp(4) walks it.

VERDICT.md
↓ download raw

DF-5497 VERDICT β€” REPRODUCED (kernel panic via production crom_next(); OOB read up to +64 MB from a device-controlled ROM)

Root cause

crom_next() walks Config-ROM directories supplied by the peer FireWire device (buffered byte-swapped in fwdev->csrrom[CSRROMSIZE/4], a 1024-byte heap array β€” firewirereg.h:52-54, filled quadlet-by-quadlet from the wire at firewire.c:1500). When an entry has key type CSRTYPE_D (every unit directory, key 0xd1, qualifies), it derives the sub-directory pointer straight from the entry's 24-bit value:

reg->val is u_int32_t val:24 (iec13213.h:124-132), so reg + reg->val is an unvalidated forward pointer up to 0xffffff quadlets = +64 MB from the entry. The very next statement dereferences it:

  • sys/bus/firewire/fwcrom.c:130 β€” if (ptr->index < ptr->dir->crc_len && (vm_offset_t)crom_get(cc) <= CROM_END(cc)) β€” the left operand loads the 16-bit crc_len bitfield before the CROM_END bound check on the right operand is even evaluated (C && evaluates left first);
  • sys/bus/firewire/fwcrom.c:134 β€” second unguarded ptr->dir->crc_len load.

Contrast: the root directory pointer is (incorrectly, DF-1084/DF-2290) derived in crom_init_context() and text-leaf pointers get a start-of-leaf check in crom_parse_text() (fwcrom.c:200-201); nested directory pointers get no check at all before the load β€” this is a distinct defect from the six known findings (DF-1083/1084/1085/1086, DF-2289/2290/2291), which bound the damage to ~1 KB past the buffer and to the root/text paths.

Reachability (in-kernel, production call chain)

A peer node's Config ROM is fetched on bus reset and every sbp(4) probe walks it with this exact code: sbp.c:412/468/567/602 (crom_init_context(&cc, target->fwdev->csrrom)) β†’ crom_search_key() (fwcrom.c:146-158) β†’ crom_next(). A malicious device puts one CSRTYPE_D entry with a large value in its root directory; nothing else is required. FireWire is unauthenticated β€” any device on the bus (physical/prox access, or a hostile SBP-2 target) is the attacker. Hardware-gated for the entry; the QEMU guest has no FireWire controller, so verification used the established production-code KLD harness + userspace-model pattern (DF-5302/DF-5321/DF-5413 precedents), driving the unmodified production parser.

Reproduction

  1. Userspace model (c5497_user.c, production fwcrom.c compiled for userland as fwcontrol(8) links it, ROM placed against a PROT_NONE guard page): * V=300 β†’ SIGSEGV at rom+1226 (predicted load window rom+1224..1227; GCC emits a halfword load for the crc_len bitfield): the load is 200 bytes past the 1024-byte ROM and happens before the CROM_END check that would have rejected the directory (run.log). * V=0xffffff β†’ SIGSEGV at rom+0x4000014 β€” the full +64 MB reach. * V=100 (in-ROM) β†’ clean walk (control).
  2. KLD oracle mode (production fwcrom.c in df5497.ko; slab groom: 64 same-class 1024-byte blocks patterned with crc_len=1, every other one freed, model ROM allocated into a hole): four crom_next: bound check failed lines β€” the production parser read crc_len from kernel heap memory outside the ROM and that foreign content observably steers the walk (1-bit oracle per crafted value; attacker-selected forward offset, 4-byte granularity).
  3. KLD panic mode (16 descents, +4 MB..+64 MB steps): mapped probes print oracle lines; the first unmapped one panics the kernel β€” panic: assertion "obj != NULL" failed in vm_object_hold_shared, trap 000000000000000c, RIP dfh_crom_next+0x53 (the crc_len load), guest down (panic.txt, serial console).

Impact

  • Kernel panic / local-physical DoS (deterministic: probe an unmapped kernel_map offset) β€” demonstrated.
  • OOB kernel-memory read: 4 bytes at a device-chosen forward offset up to +64 MB from a heap object, with a 1-bit content oracle (the console line) per probe β€” repeatable at attacker pace across walks.
  • Read-only primitive: a wild directory can never be entered (entering requires dir+entry[0] <= CROM_END, i.e. near-ROM), so no OOB write arises from this defect alone.

Severity: Medium (hardware-gated physical-bus attacker; family calibration with DF-2290/DF-2291 = Medium kernleak; this primitive is strictly stronger: +64 MB reach and panic-capable). CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H.

Fix validation

fix.diff (validate the derived directory pointer against CROM_END before descending/dereferencing) applied to the guest's /usr/src, module and model rebuilt:

  • userspace model: A/B walk cleanly, no fault (exit 0);
  • KLD mode 1: oracle lines 4 β†’ 0 (no OOB neighbour reads at all);
  • KLD mode 2: no panic, walk finishes, guest stays up.

The single remaining bound check failed line in patched mode 2 comes from the 16th probe whose value wraps to 0 (160x100000 & 0xffffff == 0), descending into* the ROM and walking entries against the pre-existing, separately-tracked CROM_END unit slack (DF-2290's territory) β€” reads stay within that known ~1 KB window; the +64 MB primitive is gone.

Artifacts

run.log (all baseline manifestations), panic.txt (serial-console panic), build.log / fix_build.log, fix_run.log, env.txt, df5497.c, Makefile, c5497_user.c, build.sh, run.sh, fix.diff.

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

Baseline: userspace faults at the two predicted OOB addresses, 4 in-kernel oracle lines, mode-2 panic (guest down). Patched (fix.diff on /usr/src, module + model rebuilt, identical triggers): userspace subtests A/B walk cleanly, oracle lines 0, mode-2 sweep completes with no panic and the guest stays up.

fix_run.log, fix_build.log, fix.diff, /tmp transcript of the patched userspace model (clean walks)
↓ fix.diffper-fix-DF-5497

Confirmed kernel references

Detail

Exploit chain

attacker: malicious/uncompromised FireWire peer device on the bus (unauthenticated, physical/prox access) -> bus reset: fw_bus_explore() fetches its Config ROM into fwdev->csrrom[256] (firewire.c:1500, firewirereg.h:52-54) -> sbp(4) probe walks it: crom_init_context + crom_search_key -> crom_next (sbp.c:412-425) -> ROM root directory contains a CSRTYPE_D entry (key 0xd1) with val=V -> kernel loads the 16-bit crc_len at csrrom+24+4*V without any prior bound -> mapped: silent foreign-memory read + 1-bit console oracle ('bound check failed') per crafted V; unmapped: Fatal trap 12 in crom_next -> kernel panic (deterministic DoS). uid=0 chain not applicable: read-only primitive (wild directories are never entered), hardware-gated trigger.

Evidence (decisive lines)

panic.txt (serial console: trap 000000000000000c, RIP dfh_crom_next+0x53, four oracle lines before the fault); run.log (userspace guard-page model: predicted-address SIGSEGVs at rom+1226 and rom+0x4000014, clean control; KLD mode 1 groomed oracle 4x; mode 2 never returns); fix_run.log (patched: subtests clean, oracle 4->0, mode 2 survives, guest up); fix.diff; build.log/fix_build.log; VERDICT.md for the full path:line narrative

PoC changes

Harness written fresh for this file (DF-5302/DF-5321 KLD pattern: production fwcrom.c #included with -D-renamed globals; the only synthetic state is the device-controlled ROM buffer and its slab neighbourhood). Bring-up fixes: (1) re-added the fire-flag variable removed with a duplicate sysctl; (2) sys/queue.h include for the -DTEST userspace build; (3) userspace model fault prediction widened to a 4-byte window because GCC loads the crc_len bitfield as a halfword at dir+2; (4) mode-1 oracle made observable with a slab groom (64 same-class blocks patterned crc_len=1, every other freed, ROM allocated into a hole) after the first run read all-zero neighbours; (5) mode-2 probe 16 wraps to val=0 (16*0x100000 & 0xffffff), which descends into the ROM itself and produces one bound-check line against the known CROM_END slack (DF-2290) -- the +64MB probes are entries 0..15 steps 1..15.

Verified recommended fix

In crom_next(), bounds-check the derived directory pointer ((vm_offset_t)dir <= CROM_END(cc)+1-sizeof(*dir)) before assigning ptr->dir and dereferencing crc_len; skip the entry as a leaf otherwise (fix.diff).

Verdict

Reproduced on the QEMU guest with the production sys/bus/firewire/fwcrom.c compiled unmodified into a KLD harness (DF-5302/5321/5413 pattern; FireWire is hardware-gated in QEMU, so the harness feeds the production parser a device-controlled 1024-byte Config ROM exactly as fw_bus_explore() fills fwdev->csrrom[] and sbp(4) walks it). crom_next() derives the nested-directory pointer from the unvalidated 24-bit entry value (fwcrom.c:122, reg+reg->val, up to +64 MB forward) and loads ptr->dir->crc_len (fwcrom.c:130/134) BEFORE the CROM_END bound check on the next operand of the &&. Demonstrated: (1) userspace guard-page model -- the production parser faults at rom+1226 (200 bytes past the 1024-byte ROM, predicted window 1224..1227 for the crc_len bitfield load) and at rom+0x4000014 (full 24-bit reach), while the in-ROM control walks cleanly -- proving the load precedes the check that would have rejected the directory; (2) in-kernel, with the slab around the model ROM groomed with a crc_len=1 pattern, four 'crom_next: bound check failed' oracle lines fired -- production reads of kernel heap memory outside the ROM that observably steer the walk; (3) a +4..+64 MB descent sweep panics the kernel: 'panic: assertion obj != NULL failed in vm_object_hold_shared', trap 000000000000000c, RIP dfh_crom_next+0x53 (the crc_len load), guest down (panic.txt). Read-only primitive (a wild directory can never be entered, so no OOB write from this defect); attacker is any device on the FireWire bus via sbp.c:412/468/567/602 crom_init_context/crom_search_key walks. Distinct from the six known findings (DF-1083/1084/1085/1086, DF-2289/2290/2291): those bound the damage to ~1 KB past the buffer or the root/text-leaf paths; this is the nested-directory dereference-before-validation with a 64 MB reach. Fix validated: with fix.diff (bounds-check the derived directory pointer before descending) applied to guest /usr/src, userspace subtests walk cleanly, oracle lines 4->0, the +4..64 MB sweep completes with no panic and the guest stays up.