β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
1085

crom_parse_text() write-underflow when text leaf crc_len < 2 corrupts memory before the caller buffer

Summary

crom_parse_text at fwcrom.c:215 computes qlen = textleaf->crc_len - 2 without underflow check. crc_len is 16-bit BIT16x2 field (iec13213.h:149); if device sets it to 0 or 1, qlen becomes -2 or -1 (signed int). Line 216 if(len < qlen*4) qlen=len/4 with qlen*4=-8/-4 evaluates 32 < -8 false, qlen stays negative. for-loop at :218 0<-2 false, skipped. Line 221 if(len <= qlen*4) 32<=-8 false, else branch executes: buf[qlen*4]=0 writes NUL at buf[-8] or buf[-4]. sbp_probe_lun caller (sbp.c:602-621) passes sdev->vendor[32] so write hits sdev->vendor[-8/-4] which is the tqh_last pointer of STAILQ_HEAD(,sbp_ocb) free_ocbs per sbp.c:175-189. Zeroing a byte of that pointer corrupts queue tail; next STAILQ_INSERT_TAIL/REMOVE derefs wild pointer -> kernel panic or controlled write with grooming. No user interaction or privilege; fires at device attach/login. Fix: reject crc_len < 2.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/1085 Β· 16 files
FileTypeDescriptionSize
harness.c trigger-source verbatim crom_parse_text userspace repro (BUGGY logic); crafted Config ROM with crc_len<2 text leaf 12.5 KB view raw
harness_fixed.c trigger-source same code + the crc_len<2 guard from fix.diff (FIXED logic) 12.7 KB view raw
fix.diff suggested-fix git-apply-able one-hunk fix: guard crc_len<2 before subtraction at fwcrom.c:215 613 B view raw
build.sh build-script cc -O0 -g -Wall build for both harnesses 408 B view raw
run.sh run-script runs harness then harness_fixed; prints BUG vs canary-intact 741 B view raw
run.log run-log baseline (buggy logic) full output, run 1 1.8 KB view raw
run.2.log run-log baseline (buggy logic) full output, run 2 (determinism) 1.8 KB view raw
run_fixed.log run-log fixed logic full output β€” canary intact 1.3 KB view raw
baseline_run.log run-log re-confirmed BUG CONFIRMED on #0 unpatched kernel after vm.sh reset with-src 318 B view raw
fix_build.log build-log full make -j6 nativekernel output, rc=0 (35855 lines) 5.6 MB ↓ download
crom_parse_text_patched.disasm disassembly objdump of patched crom_parse_text showing cmp $0x1; jbe strncpy_path guard + kern.version #1 + kernel sha256 4.2 KB ↓ download
panic.txt panic-signature no panic (HW-gated); underflow math + harness observation summary 1.1 KB view raw
env.txt environment uname, cc version, CPU features, kernel config (INVARIANTS, firewire, sbp) 571 B view raw
VERDICT.md verdict full narrative: mechanism, reachability, threat model, fix validation 12.0 KB ↓ raw
README.md readme finding summary + reproduce instructions 3.7 KB ↓ raw
manifest.json manifest this file 3.3 KB view raw
README.md readme finding summary + reproduce instructions
↓ download raw

DF-1085 β€” crom_parse_text() write-underflow when text leaf crc_len < 2

File: sys/bus/firewire/fwcrom.c:215 Β· Severity: High Class: CWE-787 Out-of-bounds Write

TL;DR

crom_parse_text subtracts 2 from a 16-bit textleaf->crc_len without checking it is >= 2. If a (malicious) FireWire device's Configuration ROM has a text leaf with crc_len of 0 or 1, the signed qlen underflows to βˆ’2/βˆ’1 and the NUL-terminator write at fwcrom.c:224 (buf[qlen*4] = 0) lands 4 or 8 bytes before buf β€” a single-byte OOB write. In the SBP-2 caller (sbp.c:606/621) buf is sdev->vendor / sdev->product and buf[-8] aliases the high byte of sdev->free_ocbs.tqh_last, a kernel heap pointer later dereferenced by the SBP-2 driver's free-OCB queue ops.

This guest has no FireWire controller, so the live in-kernel path is not exercisable; the primitive is proven at the harness level using the verbatim kernel code (same situation as DF-1083 / DF-0594 / DF-0616 / DF-0281).

Reproduce

On the DragonFly guest as any user:

./build.sh && ./run.sh
  • harness β€” verbatim crom_parse_text logic; expects >>> OVERALL: BUG CONFIRMED (exit 1).
  • harness_fixed β€” same code + the crc_len < 2 guard from fix.diff; expects >>> OVERALL: canary intact (exit 0).

Build: cc -O0 -g -Wall (cc 8.3, DragonFly). No special libraries.

Expected output (bug present)

[crc_len=0] >>> BUG CONFIRMED: pre[8] (== buf[-8]) was zeroed by buf[qlen*4]=0 with qlen=-2
[crc_len=1] >>> BUG CONFIRMED: pre[12] (== buf[-4]) was zeroed by buf[qlen*4]=0 with qlen=-1
[crc_len=2 (legal minimum)] canary intact: no underflow write (crc_len guard fired).
>>> OVERALL: BUG CONFIRMED β€” crom_parse_text writes out of bounds when crc_len < 2.

Fix

fix.diff β€” at fwcrom.c:215, reject malformed leaves with crc_len < 2 before the subtraction:

+   if (textleaf->crc_len < 2) {
+       strncpy(buf, nullstr, len);
+       return;
+   }
    qlen = textleaf->crc_len - 2;

Validated on a single-fix kernel (6.5-DEVELOPMENT #1, sha256 cc74c4ec…); disassembly of the running crom_parse_text shows the cmp $0x1,%ax; jbe strncpy_path guard before sub $0x2,%eax. See VERDICT.md and crom_parse_text_patched.disasm for details.

Files in this folder

File Purpose
harness.c verbatim-kernel-code userspace repro (BUGGY logic)
harness_fixed.c same code + crc_len<2 guard (FIXED logic)
build.sh / run.sh exact build & run commands
fix.diff git apply-able one-hunk fix for fwcrom.c
run.log / run.2.log full BUG-CONFIRMED output (baseline, 2 runs)
run_fixed.log full canary-intact output (fixed logic)
baseline_run.log re-confirmed on #0 unpatched kernel after vm.sh reset
fix_build.log full make nativekernel log (rc=0)
crom_parse_text_patched.disasm objdump of patched crom_parse_text + kern.version
panic.txt underflow math + harness observation summary
env.txt guest uname, cc, CPU features, kernel config
VERDICT.md full narrative: mechanism, reachability, fix validation
manifest.json machine-readable catalog
VERDICT.md verdict full narrative: mechanism, reachability, threat model, fix validation
↓ download raw

DF-1085 β€” Verdict

REPRODUCED (underflow-write primitive confirmed via verbatim-source harness; live kernel trigger requires FireWire hardware absent from the QEMU guest). FIX VALIDATED on a single-fix kernel (#1 build) β€” the guard is compiled in (disassembly-confirmed) and the patched kernel boots clean.

Severity: High (kernel OOB write from an external device's Configuration ROM data β€” adjacent-field corruption in the SBP-2 struct sbp_dev).


The bug

crom_parse_text() in sys/bus/firewire/fwcrom.c:188 parses an IEEE 1212 Configuration-ROM text leaf out of a remote FireWire device's ROM into a caller-supplied buffer. At fwcrom.c:215 it computes the leaf's text-word count by subtracting the 2-word mandatory header from the leaf's crc_len:

bp = (u_int32_t *)&buf[0];
qlen = textleaf->crc_len - 2;                       /* line 215 β€” BUG */
if (len < qlen * 4)                                 /* line 216 */
    qlen = len/4;
for (i = 0; i < qlen; i ++)                         /* line 218 */
    *bp++ = ntohl(textleaf->text[i]);
/* make sure to terminate the string */
if (len <= qlen * 4)                                /* line 221 */
    buf[len - 1] = 0;
else
    buf[qlen * 4] = 0;                              /* line 224 β€” UNDERFLOW WRITE */

textleaf->crc_len is a 16-bit unsigned field (BIT16x2(crc_len, crc) macro from firewire.h:122; struct csrtext at iec13213.h:148-149). The macro expands to u_int32_t crc:16, crc_len:16. A malicious device can set crc_len to 0 or 1 (a malformed leaf smaller than its 2-word header). The subtraction is then performed in signed int arithmetic and qlen underflows to -2 or -1.

All three downstream guards then MISBEHAVE because they compare a positive len (caller-provided) against a NEGATIVE qlen * 4:

line check with crc_len=0 (qlen=-2) outcome
216 if (len < qlen * 4) 32 < -8 FALSE β€” qlen stays -2
218 for (i=0; i<qlen; ...) 0 < -2 FALSE β€” loop skipped
221 if (len <= qlen * 4) 32 <= -8 FALSE
224 else buf[qlen * 4] = 0; buf[-8] = 0 1-byte OOB write before buf

Result: a single NUL byte is written 4 or 8 bytes before buf.

Note on finding-prose accuracy

The DB/finding prose says "…then passed as size to bcopy -> massive heap corruption". That is imprecise: the kernel never calls bcopy here and the for-loop body is skipped (because 0 < -2 is false), so the "massive" description overstates the per-call effect. The actual primitive β€” verified here, line by line against fwcrom.c β€” is a single-byte NUL write at buf[-8] (crc_len=0) or buf[-4] (crc_len=1). The underflow itself is real and exploitable; the impact wording is what is corrected.

In-kernel reachability & impact ceiling

crom_parse_text is statically linked into the default GENERIC kernel (sys/config/X86_64_GENERIC: device firewire, device sbp; nm /boot/kernel/kernel.debug shows crom_parse_text at 0xffffffff804bdf00).

The only kernel callers are in sys/dev/disk/sbp/sbp.c (SBP-2 β€” SCSI over FireWire target enumeration):

/* sbp.c:602 */   crom_init_context(cc, fwdev->csrrom);
/* sbp.c:606 */   crom_parse_text(cc, sdev->vendor,  sizeof(sdev->vendor));   // buf=sdev->vendor (char[32])
/* sbp.c:621 */   crom_parse_text(cc, sdev->product, sizeof(sdev->product));  // buf=sdev->product (char[32])

struct sbp_dev (sbp.c:158-190) lays out the relevant fields contiguously:

STAILQ_HEAD(, sbp_ocb) ocbs;        /* sbp.c:185  β€” 16-byte STAILQ head (tqh_first + tqh_last) */
STAILQ_HEAD(, sbp_ocb) free_ocbs;   /* sbp.c:186  β€” 16-byte STAILQ head (tqh_first + tqh_last) */
char vendor[32];                    /* sbp.c:187  β€” buf passed at sbp.c:606 */
char product[32];                   /* sbp.c:188  β€” buf passed at sbp.c:621 */

sdev is a heap object (kmalloc(sizeof(struct sbp_dev), ...) at sbp.c:483). When crom_parse_text writes sdev->vendor[-8] it lands in the high byte of free_ocbs.tqh_last (the tail-pointer of the free-OCB singly-linked queue), corrupting a kernel heap pointer that the SBP-2 driver later dereferences on the next STAILQ_INSERT_TAIL / STAILQ_REMOVE against the free-OCB queue.

Realistic threat model

A malicious external FireWire device presents a Configuration ROM whose text-leaf header advertises crc_len of 0 or 1. When the host kernel attaches it as an SBP-2 target, sbp_probe_lun parses the ROM, triggering the underflow write. The corrupted pointer is later dereferenced in the I/O-submission path. No local user, no privilege, no authentication is required β€” physical/proximity FireWire bus access is the precondition (the same threat model as DF-1083 and as sys/bus/firewire/'s rank-2 audit priority: "Unauthenticated remote packet parsing").

Why this is "High" not "Critical"

  • No QEMU trigger path: FireWire is statically compiled in but there is no FireWire controller in the QEMU guest, so the live kernel code path is not exercisable here. The primitive is proven at the harness level using the verbatim kernel code, exactly as in DF-0594/0616/0281/1083.
  • Single-byte write: the primitive is one NUL byte per call to a caller-chosen offset of -4 or -8 before buf. Realistic exploitation needs to (a) shape sdev allocation so the corrupted byte lands on a security-sensitive field, and (b) arrange for the subsequent STAILQ_INSERT_TAIL/REMOVE to deref the now-wild tqh_last. Because the write is a single NUL on a kernel heap pointer (clearing the high byte of an address in 0xffff800xxxxxxxxx-style kernel space), the corruption turns a valid pointer into a non-canonical address, which traps on the next deref β€” i.e. a denial-of-service (kernel panic on wild pointer deref) is the realistic ceiling with high probability; controlled exploitation to uid=0 would require a much more elaborate grooming chain and a different victim field (the high byte of tqh_last is not cleanly attacker-shaped for a useful target).
  • This finding therefore does not claim uid=0. The honest characterization is kernel memory corruption from external (FireWire-bus) input, which is High per the rubric ("kernel memory corruption, remote DoS on default config").

Proof (harness)

harness.c compiles the verbatim crom_init_context / crom_get / crom_parse_text (fwcrom.c:62-94, 96-103, 188-225) and the exact structures (iec13213.h:124-159, firewire.h:122), then feeds a crafted Configuration ROM whose root directory contains a single CROM_TEXTLEAF-typed entry (key=0x81) pointing at a text leaf whose crc_len is attacker-set.

The probe layout is unsigned char pre[16]; char buf[32]; unsigned char post[16]; β€” pre[8] aliases buf[-8] and pre[12] aliases buf[-4].

Baseline (unpatched 6.5-DEVELOPMENT #0, harness with verbatim kernel logic):

[crc_len=0] pre  bytes: a5 a5 a5 a5 a5 a5 a5 a5 00 a5 a5 a5 a5 a5 a5 a5
[crc_len=0] >>> BUG CONFIRMED: pre[8] (== buf[-8]) was zeroed by buf[qlen*4]=0 with qlen=-2

[crc_len=1] pre  bytes: a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 00 a5 a5 a5
[crc_len=1] >>> BUG CONFIRMED: pre[12] (== buf[-4]) was zeroed by buf[qlen*4]=0 with qlen=-1

[crc_len=2 (legal minimum)] pre  bytes: a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5 a5
[crc_len=2 (legal minimum)] canary intact: no underflow write (crc_len guard fired).

=== summary ===
crc_len=0 -> BUG (underflow write)
crc_len=1 -> BUG (underflow write)
crc_len=2 -> no bug (control: must be 'no bug')
>>> OVERALL: BUG CONFIRMED β€” crom_parse_text writes out of bounds when crc_len < 2.

Deterministic across 3 runs.

Fixed logic (harness_fixed.c β€” same code + if (textleaf->crc_len < 2) { strncpy(buf, nullstr, len); return; } before line 215):

crc_len=0 -> no bug
crc_len=1 -> no bug
crc_len=2 -> no bug (control: must be 'no bug')
>>> OVERALL: canary intact β€” underflow is guarded.

Exploit chain

None demonstrated. Valid hard blocker (Phase 6): the vulnerable code path is dead/unreachable at runtime on this guest AND no harness can exercise it in-kernel β€” there is no FireWire controller in the QEMU guest, so the live crom_parse_text (called only from sbp.c SBP-2 target enumeration, sbp.c:606/621) cannot be invoked. This is exactly the hardware-gated latent-bug case of DF-0594/0616/0281/1083. The primitive is proven at the harness level (verbatim kernel code, crafted attacker ROM). Escalation to uid=0 requires physical/proximity FireWire bus access and a victim-field grooming chain that is not testable here; the realistic impact ceiling is kernel memory corruption β†’ kernel panic on wild-pointer deref (DoS), which is what "High" severity captures.

Fix

fix.diff β€” at fwcrom.c:215, guard the subtraction against the mandatory 2-word header:

--- a/sys/bus/firewire/fwcrom.c
+++ b/sys/bus/firewire/fwcrom.c
@@ -212,6 +212,14 @@
    /* XXX should check spec and type */

    bp = (u_int32_t *)&buf[0];
+   /* DF-1085: reject malformed text leaves whose claimed length is
+    * smaller than the mandatory 2-word header, so the (crc_len - 2)
+    * subtraction below cannot underflow into a negative qlen and
+    * cause an OOB write at buf[qlen*4]. */
+   if (textleaf->crc_len < 2) {
+       strncpy(buf, nullstr, len);
+       return;
+   }
    qlen = textleaf->crc_len - 2;

A malformed leaf (crc_len < 2) now falls through to the same (null)-string fallback that the function already uses for the other malformed-leaf rejection paths at fwcrom.c:200-210, and the crc_len - 2 subtraction only runs once the value is provably >= 2 β€” qlen is then non-negative and the len/4 clamp / for-loop / NUL termination all behave as originally intended. Matches the finding markdown's ## Recommended fix proposal ("reject crc_len < 2").

Fix validation (Phase 8)

  • Baseline (6.5-DEVELOPMENT #0, unpatched): harness shows >>> OVERALL: BUG CONFIRMED β€” crom_parse_text writes out of bounds when crc_len < 2. (deterministic across 3 runs).
  • Fix applied to /usr/src/sys/bus/firewire/fwcrom.c via patch -p1 --forward < /root/fix.diff β†’ Hunk #1 succeeded at 212.
  • Kernel built with make -j6 nativekernel KERNCONF=X86_64_GENERIC (forced by deleting the warm fwcrom.o so the patched TU actually recompiled), rc=0, full log saved as fix_build.log (35 855 lines).
  • Kernel installed by overwriting the bare loader name (cp kernel.stripped /boot/kernel/kernel), sha256 cc74c4ec65d3fd187098a67a11ad6816dbcf2d37e4b08fb1c5a5004e92ec16db.
  • Kernel booted clean: kern.version = DragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 04:51:43 UTC 2026 (the #1 suffix bump + today's build timestamp confirm the patched kernel is the running kernel). SSH healthy.
  • Patch is compiled in (disassembly of the running kernel's crom_parse_text at 0xffffffff804bdf00, see crom_parse_text_patched.disasm): ffffffff804bdf56: 66 83 f8 01 cmp $0x1,%ax # crc_len (low16) vs 1 ffffffff804bdf5a: 76 44 jbe 0xffffffff804bdfa0 # <= 1 -> strncpy("(null)") path ffffffff804bdf5c: 83 e8 02 sub $0x2,%eax # only now crc_len-2 The cmp $0x1; jbe strncpy_path is the compiler's encoding of if (textleaf->crc_len < 2) β€” when the guard fires the function tail-calls strncpy(buf, "(null)", len) and returns, never reaching the underflow-prone sub $0x2,%eax.
  • Fixed logic in harness: >>> OVERALL: canary intact β€” underflow is guarded. for crc_len ∈ {0, 1, 2}.

fix_status: fixed β€” because the live kernel path needs FireWire hardware (absent in QEMU), the validation rests on (a) the harness before/after and (b) the patched kernel building, booting cleanly at #1, and (c) the disassembly showing the guard is compiled into the running crom_parse_text. This is the standard pattern for hardware-gated latent bugs (cf. DF-1083).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline underflow writes; patched guard fires, canary intact. Compile+boot+disasm.

BEFORE: BUG CONFIRMED. AFTER: canary intact. Disasm: cmp $0x1,%ax; jbe strncpy path.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 04:51:43 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- no FW HW. Primitive: single-byte NUL write on heap ptr high byte -> wild deref DoS. Not uid0.

Evidence (decisive lines)

BEFORE: crc_len=0 -> buf[-8] zeroed, crc_len=1 -> buf[-4] zeroed. AFTER: guard fires, canary intact.

PoC changes

Authored: harness.c (verbatim crom_parse_text), harness_fixed.c, fix.diff (guard crc_len<2 -> strncpy nullstr), VERDICT.md, manifest.json.

Verified recommended fix

Add if(textleaf->crc_len<2){strncpy(buf,nullstr,len);return;} at fwcrom.c:215. Matches finding proposal. Full diff in findings/poc/1085/fix.diff.

Verdict

REPRODUCED (harness). crom_parse_text fwcrom.c:215 qlen=crc_len-2 underflow when crc_len=0/1 -> buf[qlen*4]=0 writes NUL at buf[-8]/buf[-4]. Loop body skipped (neg vs pos). Single-byte NUL write before buf. In SBP caller buf=sdev->vendor[32], buf[-8] aliases tqh_last heap ptr. No FW HW.