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)
PoC verification
Evidence pack
findings/poc/1085 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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β verbatimcrom_parse_textlogic; expects>>> OVERALL: BUG CONFIRMED(exit 1).harness_fixedβ same code + thecrc_len < 2guard fromfix.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 |
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
-4or-8beforebuf. Realistic exploitation needs to (a) shapesdevallocation so the corrupted byte lands on a security-sensitive field, and (b) arrange for the subsequentSTAILQ_INSERT_TAIL/REMOVEto deref the now-wildtqh_last. Because the write is a single NUL on a kernel heap pointer (clearing the high byte of an address in0xffff800xxxxxxxxx-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 touid=0would require a much more elaborate grooming chain and a different victim field (the high byte oftqh_lastis 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.cviapatch -p1 --forward < /root/fix.diffβHunk #1 succeeded at 212. - Kernel built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC(forced by deleting the warmfwcrom.oso the patched TU actually recompiled), rc=0, full log saved asfix_build.log(35 855 lines). - Kernel installed by overwriting the bare loader name
(
cp kernel.stripped /boot/kernel/kernel), sha256cc74c4ec65d3fd187098a67a11ad6816dbcf2d37e4b08fb1c5a5004e92ec16db. - Kernel booted clean:
kern.version = DragonFly 6.5-DEVELOPMENT #1: Thu Jul 16 04:51:43 UTC 2026(the#1suffix 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_textat0xffffffff804bdf00, seecrom_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-2Thecmp $0x1; jbe strncpy_pathis the compiler's encoding ofif (textleaf->crc_len < 2)β when the guard fires the function tail-callsstrncpy(buf, "(null)", len)and returns, never reaching the underflow-pronesub $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
fixedVALIDATED: baseline underflow writes; patched guard fires, canary intact. Compile+boot+disasm.
BEFORE: BUG CONFIRMED. AFTER: canary intact. Disasm: cmp $0x1,%ax; jbe strncpy path.
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.
No comments yet.