# 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`:

```c
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):

```c
/* 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:

```c
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:

```diff
--- 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).
