DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2458

Target-controlled buffer offset in scsi_decap gives malicious iSCSI target an arbitrary-offset kernel heap write

Summary

_read_data() dispatches every inbound SCSI Data-In PDU to scsi_decap() which copies target-supplied data segment into csio->data_ptr+ntohl(rcmd->bo) without ever validating rcmd->bo (Data-Data-Offset) plus data length falls inside initiators SCSI read buffer. Malicious/compromised iSCSI target can write attacker-controlled bytes at attacker-controlled offset past end of kernel heap buffer. iscsi_subr.c:566 only checks ntohl(cmd->edtlen)>=pq->pdu.ds_len (chunk fits expected transfer length) never checks placement. offset=ntohl(rcmd->bo) and dp=csio->data_ptr+offset computed from rcmd->bo 32-bit field fully controlled by target. i_mbufcopy memcpy len bytes from target mbuf into dp. Any offset>0 with offset+len>edtlen OOB write. offset can be negative (int conversion of 0x80000000) for underflow write before buffer. Attacker: iSCSI target no mutual auth required by RFC default. Impact: heap-write primitive privilege escalation or remote DoS panic.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2458 · 9 files
FileTypeDescriptionSize
mtarget2458.c trigger-source malicious iSCSI target: login + bad-bo Data-In 10.2 KB view raw
build.sh build-script cc -o mtarget2458 mtarget2458.c 63 B view raw
run.sh run-script mtarget2458 + iscontrol login->FFP->CAM INQUIRY 1.1 KB view raw
panic.txt panic-signature write fault in memcpy (scsi_decap i_mbufcopy) 1.4 KB view raw
dmesg.txt dmesg patched: scsi_decap bad data-in dropped 99 B view raw
env.txt environment uname, kern.version, module sha256 494 B view raw
fix.diff suggested-fix bounds-check rcmd->bo in scsi_decap 1.1 KB view raw
VERDICT.md verdict full analysis + escalation analysis 7.0 KB ↓ raw
manifest.json manifest this catalog 2.0 KB view raw
VERDICT.md verdict full analysis + escalation analysis
↓ download raw

DF-2458 — Target-controlled buffer offset in scsi_decap → arbitrary-offset kernel heap write

Verdict

REPRODUCED — a malicious iSCSI target obtains an attacker-controlled write-what-where primitive against the initiator kernel: the target's Data-In PDU supplies a 32-bit Buffer Offset (rcmd->bo) that scsi_decap() uses directly as dp = csio->data_ptr + ntohl(bo) and then memcpy()s the target's data segment to that address, with no bounds check. Demonstrated as a remote kernel panic (write to an unmapped page); the realistic ceiling is remote kernel memory corruption / RCE on a host that connects to a malicious target. Fix validated on a rebuilt module.

Mechanism (trigger → primitive → effect)

  1. A privileged user runs iscontrol against the malicious target. The kernel receiver starts at ISCSISETSOC (iscsi.c:430), the login exchange completes (Security→Operational→FullFeature), CAM probes LUN 0, and the initiator issues a SCSI INQUIRY (a read, R=1, edtlen = alloc length). The outstanding command is placed on the held list (isc_sm.c:527-528).
  2. The malicious target replies with a SCSI Data-In (opcode=0x25) whose itt matches the INQUIRY and whose Buffer Offset rcmd->bo is attacker-chosen (here 0x80000000).
  3. ism_recv_read_data (isc_sm.c:144) → i_search_hld finds the opq → scsi_decap (iscsi_subr.c:531). The only size guard is ntohl(cmd->edtlen) >= pq->pdu.ds_len (iscsi_subr.c:566) — it never checks placement: c offset = ntohl(rcmd->bo); /* iscsi_subr.c:572 -- target-controlled */ dp = csio->data_ptr + offset; /* iscsi_subr.c:573 -- NO bounds check */ i_mbufcopy(pq->mp, dp, len); /* iscsi_subr.c:574 -- memcpy to dp */
  4. i_mbufcopy memcpy()s the target's data segment (attacker content, length ds_len) to csio->data_ptr + bo. With bo = 0x80000000 the destination is unmapped → write page-fault → panic. The offset is int, so values with the sign bit set (≥ 0x80000000) also yield underflow writes before the buffer.

Evidence (panic signature, unpatched #0)

With bo = 0x80000000 (and identically with bo = 0x40000000):

panic: assertion "obj != NULL" failed in vm_object_hold_shared at vm_object.c:330
vm_object_hold_shared()
vm_object_hold_shared()
vm_fault()
trap_pfault()
trap()
--- trap 000000000000000c, rip=ffffffff80bcabf6, rbp=... ---
memcpy() at memcpy+0x66

and (earlier run, bo = 0x40000000):

Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0xfffff80157b32860
fault code             = supervisor write data, page not present     <-- WRITE fault
Stopped at      memcpy+0x66:    movq    %rdx,(%rdi)                  <-- i_mbufcopy

The supervisor write data fault inside memcpy (called by i_mbufcopy from scsi_decap) at an address = data_ptr + bo is the proof that the target's Buffer Offset is honored with no bounds check and that this is a write. Reproduced by mtarget2458 (login + malicious Data-In) + iscontrol (root, drives login→FFP→CAM INQUIRY). See panic.txt, mt2458.log.

Threat model / privilege boundary

  • The attacker is the iSCSI target (malicious/compromised storage server). The victim is the kernel of any host whose iscsi_initiator connects to it. iSCSI needs no mutual auth by default (RFC 3720). The bug fires in normal full-feature operation (no special setup beyond connecting + a SCSI read probe, which CAM issues automatically on LUN discovery). The network→kernel boundary is crossed.
  • Starting the session needs a privileged iscontrol (/dev/iscsi is 0600 root:wheel), which is the normal operational model.

Phase 6 — escalation analysis

This is a write-capable primitive (write-what-where relative to csio->data_ptr), so Phase 6 applies. What the attacker controls: - destination offset bo (full 32-bit, signed → underflow possible), - written content (the Data-In data segment bytes), - length ds_len (bounded by the negotiated MaxRecvDataSegmentLength).

What limits a clean uid=0 / RCE chain on this guest: - The destination is csio->data_ptr + bo — a relative write. To overwrite a specific kernel object (function pointer / struct ucred *) the attacker must know data_ptr's absolute heap address. KASLR is off, so kernel symbol addresses are fixed, but data_ptr is a dynamic heap allocation whose address is not known to a remote target without an info leak or successful heap grooming (the target can influence slab churn only indirectly, via the I/O it serves). The attacker is a network peer and has no local process on the victim, so the usual "spray/fork/pipe to fix the layout then pivot to userspace shellcode" local-escalation recipe is not directly applicable; RCE would require (a) a heap info-leak to pin data_ptr, then (b) a groomed adjacent victim object, then (c) redirecting a function pointer to shellcode staged in a kernel mbuf the attacker controls. - This is therefore reported at its demonstrated impact: remote kernel panic / DoS, with remote-kernel-RCE as the realistic (but not demonstrated in this PoC) ceiling. It is correctly rated Critical as a remote attacker-controlled kernel write primitive; the unpriv→uid=0 framing does not fit (there is no local unprivileged actor — the attacker is the network peer, the victim is the kernel).

PoC changes

  • mtarget2458.c (new): malicious iSCSI target implementing the Login exchange (Security→Operational→FFP, permissive agreements) and replying to the first SCSI command with a Data-In carrying an attacker-chosen bo.
  • fix.diff (new): bounds-check rcmd->bo in scsi_decap.

Fix (fix.diff)

In scsi_decap (iscsi_subr.c:572), before computing dp, require the placement to lie entirely inside the expected transfer buffer: offset >= 0 && offset + len <= edtlen; otherwise xdebug + drop the PDU (the command stays outstanding for a valid completion). Supersedes the finding markdown's proposal (which identified the unchecked rcmd->bo); this implements the exact bounds check at the faulting site.

A sibling instance of the same unchecked-bo pattern exists in so_recv's douio path (isc_soc.c:452, iov->iov_base = csio->data_ptr + ntohl(rcmd->bo)) which is reached only when sp->douio is enabled (default off); the same bounds check should be applied there. This fix targets the cited/reproduced scsi_decap path.

Fix validation (rebuilt single module)

  • Baseline (unpatched #0): malicious Data-In bo=0x80000000panic: assertion obj!=NULL in vm_object_hold_shared via memcpy+0x66 (write fault) (panic.txt). Same with bo=0x40000000.
  • Patched module (sha256 aadc9875...): same malicious Data-In → bounds check fires, guest stays up, dmesg: ">>> scsi_decap: 0] bad data-in offset=-2147483648 len=16 edtlen=36 - dropping". No panic, no da device (the malicious target never served valid data, as expected).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: mtarget2458(iscontrol) PANICS on unpatched #0 module ('panic: assertion obj!=NULL' write fault in memcpy from i_mbufcopy/scsi_decap) and does NOT panic on single-fix module (same bad-bo Data-In caught: dmesg '>>> scsi_decap: 0] bad data-in offset=-2147483648 len=16 edtlen=36 - dropping', guest stays up) => fix closes write path.

baseline: panic: assertion obj!=NULL in vm_object_hold_shared via memcpy+0x66 (supervisor WRITE data). patched: '>>> scsi_decap: 0] bad data-in offset=-2147483648 len=16 edtlen=36 - dropping' -- no panic, guest up.
↓ fix.diffkernel kern.version unchanged (#0, Thu Jul 2 06:02:54 UTC 2026) -- fix validated by rebuilding loadable iscsi_initiator.ko module (bsd.kmod.mk, same KERNENV/flags) and kldload-ing it; patched module sha256 aadc9875d4efd6da6c75e555f12bcc1566f16b93391dfd4a774d13f706b2344a

Confirmed kernel references

Detail

Exploit chain

Write-what-where primitive relative to csio->data_ptr (attacker controls bo offset + data-segment content + ds_len). Demonstrated impact: remote kernel panic (DoS) via write to unmapped page. uid=0/RCE NOT achieved: attacker is REMOTE network peer (the iSCSI target) with no local process on victim, so destination is a RELATIVE write (data_ptr + bo) and a precise function-pointer/ucred overwrite requires absolute heap address of data_ptr, which a remote attacker cannot obtain without a separate heap info-leak (KASLR off so kernel symbol addrs fixed, but heap-object addrs not). Valid Phase 6 stop for escalation (no local presence to spray/leak), with concrete next iteration: chain with heap info-leak (e.g. DF-1870 R2T OOB-read) to pin data_ptr, groom an adjacent CAM/mbuf ops vector, overwrite a function pointer to mbuf-staged shellcode.

Evidence (decisive lines)

panic: assertion 'obj != NULL' failed in vm_object_hold_shared ... vm_fault() trap_pfault() trap() --- trap 0xc rip=memcpy+0x66 --- memcpy() at memcpy+0x66 [earlier run: 'Fatal trap 12 fault virtual address=0xfffff80157b32860 fault code=supervisor write data Stopped at memcpy+0x66: movq %rdx,(%rdi)']

PoC changes

Wrote mtarget2458.c (malicious iSCSI target: permissive Login Security->Operational->FFP exchange + bad-bo Data-In reply to first SCSI command) and fix.diff (bounds-check rcmd->bo in scsi_decap). No prior PoC existed.

Verified recommended fix

In scsi_decap (iscsi_subr.c:572) require offset>=0 && offset+len<=edtlen before dp=csio->data_ptr+offset, else xdebug+drop the PDU. Supersedes finding proposal (implements exact bounds check at faulting site). Sibling unchecked-bo exists in so_recv's douio path (isc_soc.c:452, sp->douio default off). Full git-apply-able diff in findings/poc/DF-2458/fix.diff.

Verdict

REPRODUCED. scsi_decap() (iscsi_subr.c:572-574) computes dp = csio->data_ptr + ntohl(rcmd->bo) from the target-controlled Data-In Buffer Offset with NO bounds check (only guard at iscsi_subr.c:566 checks edtlen>=ds_len, never placement) and memcpy()s the target's data segment there. A malicious iSCSI target (mtarget2458.c) completes Login->FFP, then on the CAM INQUIRY probe replies with a Data-In whose bo=0x80000000; confirmed by supervisor-WRITE page fault inside memcpy+0x66 (i_mbufcopy) -> vm_fault -> 'panic: assertion obj!=NULL in vm_object_hold_shared'. This is a write-what-where primitive (attacker controls offset, content, length) relative to csio->data_ptr.