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

asr_intr maps the adapter-posted FromFIFO reply offset into a kernel pointer with no validation β€” forged reply frame yields a forged ccb with LIST_REMOVE/callout_stop writes; FAIL path uses device-controlled MMIO offsets unbounded

Summary

Reply = (frame *)(ReplyOffset - ha_Msgs_Phys + ha_Msgs) with no check that ReplyOffset lies in the pool window; the InitiatorContext64 loaded through the forged Reply becomes a union asr_ccb* that ASR_ccbRemove writes through (callout_stop + LIST_REMOVE unlink), followed by status/resid/sense writes and xpt_done dispatch; the FAIL branch reads/writes bus-space at the device-supplied PreservedMFA offsets. Malicious/faulty IOP firmware -> arbitrary kernel read/write engine (forged ccb unlink writes) and out-of-BAR MMIO accesses; FreeBSD ancestor shares the defect. Validate the offset against the pool window.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3823 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
fix.diff β€” 944 B view raw
env.txt β€” 323 B view raw
verdict.json β€” 2.6 KB view raw

DF-3823 β€” asr(4) maps the adapter-supplied FromFIFO reply offset into a kernel pointer with no validation (device-controlled arbitrary-kernel deref/write), and reads/writes MMIO at device-controlled frame offsets

  • File: sys/dev/raid/asr/asr.c
  • Lines: 2886-2898 (reply mapping), 2906-2953 (FAIL path MMIO offsets)
  • Class: CWE-129/CWE-20 β€” unchecked device index β†’ wild kernel pointer β†’ arbitrary read/write (device-trust)
  • Reach: adapter interrupt only (I2O outbound FIFO). No software path produces the value.

Analysis

asr_intr():

2886:       if (((ReplyOffset = asr_get_FromFIFO(sc)) == EMPTY_QUEUE)
2887:        && ((ReplyOffset = asr_get_FromFIFO(sc)) == EMPTY_QUEUE)) {
2888:           break;
2889:       }
2890:       Reply = (PI2O_SCSI_ERROR_REPLY_MESSAGE_FRAME)(ReplyOffset
2891:         - sc->ha_Msgs_Phys + (char *)(sc->ha_Msgs));

ReplyOffset is whatever the IOP posts in the outbound FIFO. It is assumed to lie inside the ha_Msgs reply-frame pool (contigmalloc'd at asr.c:1910-1914, ha_Msgs_Count frames), but nothing checks:

ReplyOffset >= sc->ha_Msgs_Phys
 && ReplyOffset <  sc->ha_Msgs_Phys + ha_Msgs_Count * sizeof(frame)

An offset outside that window makes Reply an arbitrary offset from the pool base β€” including negative (below the pool). The handler then:

  • reads the 64-bit InitiatorContext64 from Reply->…StdMessageFrame (asr.c:2896-2898) and treats it as a union asr_ccb *;
  • passes that pointer to ASR_ccbRemove() (asr.c:2971) which does callout_stop(&ccb->ccb_h.timeout_ch) and LIST_REMOVE β€” two write primitives through the forged pointer;
  • writes completion status into ccb->ccb_h.status, csio.scsi_status, resid, and bcopys sense data from Reply->SenseData (asr.c:2975-3052);
  • finally xpt_done(ccb) / wakeup(ccb).

In the message-failure branch (asr.c:2899-2954) it additionally uses the device-supplied PreservedMFA (MessageOffset, asr.c:2906-2908) as a bus-space offset for a 4-byte MMIO read (asr.c:2917-2921) and an MMIO write of a whole I2O_UTIL_NOP_MESSAGE (asr.c:2948-2949) β€” unbounded offsets into the frame BAR (device-side corruption / potential host bus faults).

Impact ceiling

With a compromised or faulty IOP, this is a full arbitrary kernel-read/write engine: choose ReplyOffset so Reply lands on controlled DMA'd memory (or anywhere in physmap), return a crafted InitiatorContext64, and ASR_ccbRemove performs unlink-style writes at the chosen address before an indirect dispatch through the CAM layer. Severity capped Low for the hardware precondition.

Verdict

inconclusive β€” the audit guest has no DPT/Adaptec I2O HBA (see ../DF-3825/run.log and ../DF-3819/env.txt), so asr_intr never runs. Source-level: certain.

VERDICT.md
↓ download raw

VERDICT β€” DF-3823

Status: inconclusive (device-gated; the offending value is produced by the IOP's outbound FIFO. The audit guest has no asr-compatible HBA β€” /dev/asr0 ENOENT, zero PCI vendor-0x1044 devices β€” so asr_intr() never executes). Source-level confidence: certain.

Trace

  1. asr_intr() reads ReplyOffset from the FromFIFO register (asr.c:2886-2888). The only value rejected is EMPTY_QUEUE (0xffffffff).
  2. Reply = (frame *)(ReplyOffset - sc->ha_Msgs_Phys + (char *)sc->ha_Msgs) (asr.c:2890-2891) β€” pointer arithmetic on an unvalidated u32. The reply pool spans [ha_Msgs_Phys, ha_Msgs_Phys + ha_Msgs_Count * sizeof(I2O_SCSI_ERROR_REPLY_MESSAGE_FRAME)) (allocation at asr.c:1900-1915, FIFO population at asr.c:1918-1925 β€” every posted frame is one of these addresses, so any other value is by construction a firmware error or attack).
  3. ccb = (union asr_ccb *)(long) I2O_MESSAGE_FRAME_getInitiatorContext64(&Reply->…) (asr.c:2896-2898) β€” 8 bytes read from wherever Reply points (the macro is a raw u_int64_t load, i2odep.h:552-555; on this driver I2O_64BIT_CONTEXT == 0 so the 64-bit context is split across InitiatorContext and the following dword β€” both under device control through the forged Reply).
  4. ASR_ccbRemove(sc, ccb) (asr.c:2971 β†’ 785-792): callout_stop(ccb->ccb_h.timeout_ch) β€” writes to the callout through the forged pointer's field; LIST_REMOVE(&ccb->ccb_h, sim_links.le) β€” the classic unlink arbitrary-write (fwd->back = back; back->fwd = fwd).
  5. Status/resid/sense writes into the forged ccb (asr.c:2975-3052), then xpt_done(ccb) (indirect dispatch) or wakeup(ccb).
  6. FAIL branch: MessageOffset = PreservedMFA from the reply (asr.c:2906-2908) is used for bus_space_read_4(..., MessageOffset + offsetof(...)) (asr.c:2917-2921) and asr_set_frame(sc, …, MessageOffset, sizeof(NOP)) (asr.c:2948-2949) β€” unbounded MMIO offsets into/out of the messaging BAR.

Contrast with the rest of the driver

asr_intr's sense copy (asr.c:3029-3053) does clamp its device-supplied length four ways β€” showing the authors clamped what they noticed; the ReplyOffset (and MessageOffset) mapping simply was never checked. The FreeBSD ancestor carries the same defect (shared-hardening).

Fix

fix.diff: reject any ReplyOffset outside the ha_Msgs pool window before forming Reply (print + break). The MessageOffset MMIO offsets additionally need a BAR-bound check β€” noted in the diff header; minimal fix ships the pool-window check, which kills the primary forged-Reply primitive.

Fix validation

not_testable β€” needs the IOP to post an out-of-window frame.

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

not_testable
baseline no→ patch + rebuild →patched clean

Baseline and fix runs both require an asr IOP posting frames; absent on the guest. fix.diff adds the pool-window check.

fix.diff
↓ fix.diffper-fix-DF-3823

Confirmed kernel references

Detail

Exploit chain

device: post out-of-window ReplyOffset -> forged Reply over DMA-controlled memory -> chosen InitiatorContext64 -> forged ccb -> callout_stop + LIST_REMOVE unlink writes + sense bcopy -> kernel code execution. Precondition: compromised/faulty IOP firmware.

Evidence (decisive lines)

VERDICT.md step-by-step trace with line refs; fix.diff (pool-window validation); ../DF-3825/run.log + ../DF-3819/env.txt (no asr hardware on guest)

PoC changes

No seed; analysis pack (interrupt-time device data cannot be synthesized without the HBA).

Verified recommended fix

Validate ReplyOffset against [ha_Msgs_Phys, ha_Msgs_Phys + ha_Msgs_Count*sizeof(frame)) before forming Reply (asr.c:2890).

Verdict

asr_intr() converts the adapter-posted FromFIFO value into a kernel pointer (ReplyOffset - ha_Msgs_Phys + ha_Msgs, asr.c:2890-2891) with no check that it lies inside the ha_Msgs reply pool. An out-of-window offset (faulty or malicious IOP) yields a forged PI2O_SCSI_ERROR_REPLY_MESSAGE_FRAME whose InitiatorContext64 (raw u64 load, asr.c:2896-2898) becomes a union asr_ccb * that ASR_ccbRemove (asr.c:2971, 785-792) writes through: callout_stop on a forged callout pointer plus LIST_REMOVE unlink writes, then status/resid/sense writes and xpt_done dispatch - a device-driven arbitrary kernel write engine. The FAIL branch additionally uses the device-supplied PreservedMFA as unbounded bus-space offsets for MMIO read/write (asr.c:2917-2921, 2948-2949). Guest has no I2O HBA (/dev/asr0 ENOENT, zero vendor-0x1044 PCI devices) so asr_intr never runs; source analysis certain. Note the same function clamps its sense lengths four ways (asr.c:3033-3042) - the offset mapping was simply never validated.