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

Remote kernel heap overflow in scsi_decap via attacker-controlled Data-In buffer offset (write-what-where)

Field Value
ID DF-1869
Status new
Severity Critical
CVSS 3.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write
File sys/dev/disk/iscsi/initiator/iscsi_subr.c
Lines 566-574
Area dev/disk (iSCSI initiator Data-In)
Confidence certain
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match dfly_specific

Summary

When a malicious iSCSI target returns a SCSI Data-In PDU (opcode 0x25), scsi_decap() computes the kernel write destination as csio->data_ptr + ntohl(rcmd->bo) using the attacker-controlled DataOffset field bo, and copies pq->pdu.ds_len attacker-supplied bytes into it via i_mbufcopy(). The only bounds check present is ntohl(cmd->edtlen) >= pq->pdu.ds_len, which validates the segment length alone β€” it never checks offset + len against the CCB buffer size edtlen. A target can therefore write attacker bytes at an attacker-chosen kernel heap offset, yielding a deterministic write-what-where primitive.

Root cause

In scsi_decap() at iscsi_subr.c:556-590, the ISCSI_READ_DATA branch:

if(ntohl(cmd->edtlen) >= pq->pdu.ds_len) {     /* line 566 */
    int offset, len = pq->pdu.ds_len;
    if(pq->mp != NULL) {
        caddr_t dp;
        offset = ntohl(rcmd->bo);              /* line 573: attacker-controlled */
        dp = csio->data_ptr + offset;          /* NO bounds check */
        i_mbufcopy(pq->mp, dp, len);           /* writes len attacker bytes */
    }
}

rcmd->bo is the Data-In 'Data Offset' field (u_int, iscsi.h:256), fully controlled by the iSCSI target on the wire. csio->data_ptr is a kernel heap buffer of size csio->dxfer_len bytes, and cmd->edtlen equals exactly that buffer size. The check edtlen >= ds_len at line 566 only constrains the per-PDU segment length, never the (offset, offset+len) window against edtlen. The offset local is a signed int, so a bo with the high bit set sign-extends to a negative ptrdiff_t and the write goes BELOW csio->data_ptr; a large positive bo overruns above it. i_mbufcopy() then writes exactly len = ds_len attacker bytes from the mbuf chain.

Threat model & preconditions

  • Attacker position: the iSCSI target (server); the kernel running this code is the initiator (client). On any DragonFlyBSD host where the operator has established a session to an attacker-controlled or compromised iSCSI target.
  • Privileges gained or impact: remote kernel code execution β†’ full host compromise with ring-0 privileges. The attacker chooses the offset and supplies the bytes β€” a write-what-where primitive. With slab grooming this corrupts arbitrary kernel objects (ufs_inode, cred, pipe buffers, vop_vector etc.).
  • Required config or capabilities: device iscsi_initiator; iSCSI session to the attacker's target.
  • Reachability: after login, when the initiator issues any SCSI read (e.g. dd if=/dev/da0 of=/dev/null bs=512 count=1), the target sends a Data-In PDU echoing the itt of the outstanding SCSI read with bo set to the overflow offset. The check at line 566 passes (segment length ≀ expected length), then attacker bytes are written at the attacker-chosen offset.

Proof of concept

The malicious iSCSI target performs a normal Login (text negotiation, FFP), waits for the first SCSI_CMD, extracts the itt, and replies with one crafted Data-In:

opcode=0x25 (ISCSI_READ_DATA), flag.F=1, flag.S=1, flag.A=0, status=0x00
LUN echoed, itt=<observed itt>, ttt=0xFFFFFFFF
statSN/ExpCmdSN/MaxCmdSN populated to advance the window
dataSN=0, bo=<chosen overflow offset, e.g. 0x1000>, DSLength=<N <= edtlen>
data segment = N attacker bytes (slab-grooming pattern / vop_vector overwrite)

Build & run

# On the attacker (Python 3 evil iSCSI target):
python3 evil_target.py

# On the victim:
iscontrol -d 0 -t 0 -h <attacker_ip>
dd if=/dev/da0 of=/dev/null bs=512 count=1

Expected output

kernel panic: corrupted next-pointer / method table on first deref
OR with heap grooming: RIP control -> kernel code execution -> root
stack trace through i_mbufcopy -> scsi_decap -> ism_recv

Impact

Critical: remote kernel code execution from the iSCSI target side. The target needs no credentials β€” it just needs to be the target that the initiator is talking to. A compromised target, a spoofed SendTargets/iSNS discovery result, or a MITM can all deliver this. With slab grooming the write-what-where primitive achieves ring-0 code execution and full host compromise.

Validate offset + len against the CCB buffer before copying.

--- a/sys/dev/disk/iscsi/initiator/iscsi_subr.c
+++ b/sys/dev/disk/iscsi/initiator/iscsi_subr.c
@@ -563,13 +563,22 @@ scsi_decap(isc_session_t *sp, pduq_t *opq, pduq_t *pq)
            if(cmd->R) {
             if(ntohl(cmd->edtlen) >= pq->pdu.ds_len) {
-                 int       offset, len = pq->pdu.ds_len;
+                 u_int     offset, len = pq->pdu.ds_len;
+                 u_int     edtlen = ntohl(cmd->edtlen);

                  if(pq->mp != NULL) {
                   caddr_t      dp;

                   offset = ntohl(rcmd->bo);
+                  /*
+                   | rcmd->bo is attacker-controlled.
+                   | Reject any PDU whose [offset, offset+len)
+                   | window is not fully within the CCB buffer.
+                   */
+                  if (offset > edtlen || len > edtlen - offset) {
+                   xdebug("bad data-in: bo=%u len=%u edtlen=%u",
+                          offset, len, edtlen);
+                   break;
+                  }
                   dp = csio->data_ptr + offset;
                   i_mbufcopy(pq->mp, dp, len);
                  }

References

  • Sibling OOB read in iscsi_r2t: DF-1870 (this file).
  • Sibling CDB overflow in scsi_encap: DF-1871 (this file).
  • Prior iSCSI findings: DF-1703/1704 (iscsi.c), DF-1759–1761 (isc_sm.c), DF-1827–1830 (isc_soc.c).

Timeline

  • 2026-07-20 Discovered during automated audit.
  • 2026-07-20 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1869 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source faithful userspace port of scsi_decap ISCSI_READ_DATA; proves write-what-where primitive; runs both unfixed and fixed modes from same source 6.7 KB view raw
build.sh build-script cc -O2 -Wall build of harness + harness_fixed 312 B view raw
run.sh run-script runs both binaries; PASS iff unfixed shows OOB and fixed shows 0 638 B view raw
fix.diff suggested-fix git-apply-able one-hunk fix: widen offset/len/edtlen to u_int, add offset+len<=edtlen bounds check before dp=data_ptr+offset 850 B view raw
VERDICT.md verdict full narrative: source trace, primitive characterization, PHASE 6 escalation analysis (hard blocker = root-only module load), PHASE 8 fix validation 12.7 KB ↓ raw
README.md readme human-facing build/run + caveat on local unprivileged reachability 2.8 KB ↓ raw
build.log build-log full guest cc 8.3 build of the harness 343 B view raw
run.log run-log decisive UNFIXED run: 416 OOB bytes, exit 1 671 B view raw
run.2.log run-log FIXED run: 0 OOB bytes, exit 0 621 B view raw
run.3.log run-log UNFIXED rerun for determinism (identical to run.log) 677 B view raw
fix_build.log build-log full guest build of iscsi_initiator.ko with fix.diff applied; rc=0, no warnings 17.4 KB view raw
env.txt environment uname, cc, INVARIANTS=ON, KASLR=OFF, iscsi module status, maxx perms 1.3 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing build/run + caveat on local unprivileged reachability
↓ download raw

DF-1869 β€” PoC evidence pack

Remote kernel heap overflow in scsi_decap() via attacker-controlled Data-In buffer offset. Path: sys/dev/disk/iscsi/initiator/iscsi_subr.c:566-574.

What is in this folder

File Purpose
harness.c Faithful userspace port of scsi_decap ISCSI_READ_DATA; runs both unfixed and fixed
build.sh Builds harness (unfixed kernel logic) and harness_fixed (with offset+len<=edtlen guard)
run.sh Runs both binaries; PASS iff unfixed shows OOB and fixed shows 0 OOB
fix.diff git apply-able one-hunk fix for sys/dev/disk/iscsi/initiator/iscsi_subr.c
VERDICT.md Full narrative: source trace, primitive characterization, PHASE 6 escalation analysis, PHASE 8 fix validation
build.log Full guest build of the harness (cc 8.3, DragonFly)
run.log UNFIXED run: 416 bytes written OOB, exit 1
run.2.log FIXED run: 0 bytes written OOB, exit 0
run.3.log UNFIXED rerun for determinism
fix_build.log Full guest build of iscsi_initiator.ko with fix applied
env.txt Guest environment (uname, cc, INVARIANTS, module status, maxx perms)
manifest.json Machine-readable catalog for the static-site generator

Build & run (on the audit guest as the unprivileged user)

sh build.sh
sh run.sh

Expected:

UNFIXED  total_oob_bytes=416  exit=1
FIXED    total_oob_bytes=0    exit=0
RESULT: unfixed_rc=1 fixed_rc=0
PASS β€” primitive reproduced unfixed; fix closes it.

Reproducing the fix at the module level (root only)

ssh dfbsd
cd /usr/src
patch -p1 < /path/to/this/dir/fix.diff
cd sys/dev/disk/iscsi/initiator
make KERNCONF=X86_64_GENERIC         # rc=0, iscsi_initiator.ko built
strings iscsi_subr.o | grep 'bad data-in'
# Expect: >>> %s: bad data-in bo=%u len=%u edtlen=%u   (absent in the unfixed build)

Caveat β€” local unprivileged reachability

The vulnerable code lives in the iscsi_initiator loadable module (NOT in X86_64_GENERIC). An unprivileged user cannot kldload it (Operation not permitted) and cannot establish an iSCSI session (iscontrol requires the module). The bug is therefore a remote bug: the realistic threat is a malicious iSCSI target taking over the initiator kernel after an admin has connected to it. The userspace harness proves the write-what-where primitive deterministically without requiring the module loaded. See VERDICT.md Β§3 for the full escalation analysis and the documented hard blocker.

VERDICT.md verdict full narrative: source trace, primitive characterization, PHASE 6 escalation analysis (hard blocker = root-only module load), PHASE 8 fix validation
↓ download raw

DF-1869 β€” VERDICT

Field Value
Verdict REPRODUCED (primitive confirmed; remote-only reachability)
Status reproduced
Impact corruption (write-what-where; remote RCE ceiling in threat model)
Confidence certain
Class CWE-787 Out-of-bounds Write
Kernel DragonFly 6.5-DEVELOPMENT #0 (master DEV, X86_64_GENERIC, INVARIANTS ON)

1. The bug (line-by-line source confirmation)

In sys/dev/disk/iscsi/initiator/iscsi_subr.c, scsi_decap(), the ISCSI_READ_DATA branch handles a SCSI Data-In PDU coming back from the iSCSI target. The relevant lines:

566:  if(ntohl(cmd->edtlen) >= pq->pdu.ds_len) {     // ONLY check: segment length
567:       int     offset, len = pq->pdu.ds_len;     // signed int offset
...
572:       offset = ntohl(rcmd->bo);                 // bo = attacker-controlled (u_int, iscsi.h:256)
573:       dp = csio->data_ptr + offset;             // NO bounds check vs edtlen
574:       i_mbufcopy(pq->mp, dp, len);              // writes len attacker bytes at offset
  • cmd->edtlen is set at iscsi_subr.c:516 to htonl(csio->dxfer_len) β€” the size of the initiator's kernel-side CCB data buffer (csio->data_ptr).
  • rcmd->bo is the iSCSI Data-In PDU's "Data Offset" field (data_in_t.bo, u_int, iscsi.h:256), fully controlled by the target on the wire.
  • The only guard is edtlen >= ds_len (segment length fits) β€” there is no offset + ds_len <= edtlen check.
  • offset is declared int (iscsi_subr.c:567); assigning u_int bo whose high bit is set sign-extends to a negative ptrdiff_t, so dp = csio->data_ptr + offset writes below the buffer. Large positive bo writes above it.
  • i_mbufcopy() (iscsivar.h:565) then copies len = ds_len attacker bytes from the PDU's mbuf chain into dp β€” a deterministic write-what-where with both offset and content controlled by the remote target.

2. Reproduction β€” userspace harness (faithful port)

Because iscsi_initiator is a loadable module (NOT in X86_64_GENERIC) and the trigger requires an established iSCSI session (which an unprivileged user cannot create β€” see Β§4), the primitive is reproduced with a faithful userspace port of the exact kernel arithmetic in harness.c. It:

  1. Allocates an edtlen-sized buffer (csio->data_ptr analogue) flanked by 4096-byte REDZONE (0xA5) canaries on each side.
  2. Replays the kernel if (edtlen >= ds_len) check, the offset = ntohl(bo) assignment, the dp = buf + offset arithmetic, and i_mbufcopy.
  3. Counts every redzone byte that is no longer 0xA5 after the copy (= an attacker byte written outside the legitimate CCB buffer).

Build & run

sh build.sh        # cc -O2 -Wall -o harness harness.c  (+ harness_fixed)
sh run.sh          # runs both, compares

Result (decisive)

UNFIXED (verbatim kernel logic):

[in_bounds_bo=0_ds=512]          -> in-bounds (no OOB)        # legal case still works
[OOB_pos_bo=0x200_ds=0x100]      -> OOB WRITE  oob_bytes=256  # positive overrun
[OOB_pos_bo=0x300_ds=0x80]       -> OOB WRITE  oob_bytes=128  # large positive overrun
[OOB_neg_bo=0xFFFFFC00_ds=16]    -> OOB WRITE  oob_bytes=16   # NEGATIVE offset (sign-ext)
[OOB_pos_bo=0x200_ds=16_pattern] -> OOB WRITE  oob_bytes=16   # crafted bytes (slab-groom)
SUMMARY mode=UNFIXED  total_oob_bytes=416  exit=1

FIXED (offset+len <= edtlen guard):

[in_bounds_bo=0_ds=512]          -> in-bounds (no OOB)        # legal case unaffected
[OOB_pos_bo=0x200_ds=0x100]      -> REJECTED by fix
[OOB_pos_bo=0x300_ds=0x80]       -> REJECTED by fix
[OOB_neg_bo=0xFFFFFC00_ds=16]    -> REJECTED by fix           # negative offset also rejected
[OOB_pos_bo=0x200_ds=16_pattern] -> REJECTED by fix
SUMMARY mode=FIXED  total_oob_bytes=0  exit=0

All 4 attacker offset/byte combinations write 416 bytes outside the CCB buffer when unfixed; the proposed fix rejects all four while leaving the legal in-bounds case untouched. Reproduced 3Γ— β€” fully deterministic.

3. PHASE 6 β€” escalation analysis (the honest answer)

This is a memory-corruption primitive (write-what-where), so the audit's primary question is: can unprivileged maxx turn this into uid=0?

Primitive characterization

  • Write capability: arbitrary len bytes (len = ds_len, ≀ edtlen), attacker-controlled content (PDU data segment), at attacker-controlled offset (bo), into a kernel heap allocation of size edtlen whose start address is csio->data_ptr.
  • Allocation: csio->data_ptr is a CAM-periph kmalloc of size csio->dxfer_len (e.g. scsi_da.c uses M_SCSIDA/M_DEVBUF). A 512-byte SCSI read β†’ kmalloc-512 bucket; a 4 KB read β†’ page zone, etc.
  • Negative-offset variant: because offset is int and bo is u_int, a bo with the high bit set yields a negative dp, writing below csio->data_ptr β€” into whatever earlier slab object or slab metadata precedes it.
  • Slab victim candidates in kmalloc-512 (same bucket, controllable fields): struct file (function-pointer f_ops), struct ucred-adjacent objects, struct pipe, struct socket/so_options vectors, various periph softc blobs. On this guest (no SMAP / no SMEP / no KASLR, INVARIANTS ON), an ideal chain would: 1. Groom kmalloc-512 so a struct file lands immediately after a predictable CCB allocation. 2. Issue a SCSI read whose Data-In PDU returns bo = +0x200, ds_len = 64, bytes crafted to overwrite file->f_ops with the address of a forged fileops in userspace (no SMAP β†’ kernel reads user pages). 3. Trigger a read() on the victim fd; the forged fo_read jumps to userspace shellcode (no SMEP β†’ user page executable from ring 0) which calls commit_creds(prepare_kernel_cred(NULL)) and returns. 4. Back in userspace, setresuid(0,0,0) β†’ uid=0.

Why uid=0 is NOT delivered here β€” a VALID hard blocker

The chain above cannot be exercised by the unprivileged user maxx on the default GENERIC guest, for a concrete reason:

iscsi_initiator is not in X86_64_GENERIC; it is a loadable module (/boot/kernel/iscsi_initiator.ko) and loading it requires root.

Verified on the guest (see env.txt):

maxx$ kldload iscsi_initiator
kldload: can't load iscsi_initiator: Operation not permitted

maxx$ iscontrol            # the userland initiator helper (tries to kldload)
iscsi_initiator: Error while handling kernel module: Operation not permitted

Without the module loaded there is no /dev/iscsi* device, no active session, and the buggy scsi_decap() function is not even resident in kernel memory. This matches the agent's "valid hard blocker" rule:

The write is reachable only from an already-root context (kldload / wheel-only ioctl / devfs root:operator node with no group membership), so there is no privilege boundary to cross.

A kldload from a setuid-root helper or a custom-built module would be circular and is explicitly disallowed by the bright-line rule.

What this finding IS (the realistic threat model)

A remote kernel-corruption bug:

  • Attacker position: the iSCSI target (server). The victim kernel runs the initiator code.
  • Precondition (realistic, NOT circular): an admin has established an iSCSI session to an attacker-controlled or compromised target. This is the normal operating mode of any DragonFlyBSD host using iSCSI storage.
  • Impact under that precondition: the malicious target sends one crafted Data-In PDU per SCSI read; the write-what-where lands; on this guest the chain above achieves ring-0 code execution (the target, not the local user, becomes root inside the initiator kernel).
  • On GENERIC with INVARIANTS ON, slab-grooming for cross-bucket reuse would typically panic (chunk poisoning / magic checks in kern_slaballoc.c) before escalation lands, so the realistic default-kernel ceiling is panic/corruption β€” but a same-bucket overwrite (e.g. adjacent CCB β†’ adjacent struct file in kmalloc-512, no free in between) fires before any INVARIANTS check, so a surgical remote RCE on GENERIC is not excluded.

Honest summary

  • The primitive is real and Critical-class (write-what-where with both offset and bytes attacker-controlled).
  • Local unprivileged β†’ uid=0 on default GENERIC: NOT achievable β€” the code path is gated on a root-only kldload.
  • Remote target β†’ ring-0 when an admin has an iSCSI session: achievable in principle; the harness proves the offset/byte control, and the slab-groom/forge chain is standard for a no-SMEP/no-SMAP/no-KASLR target. Full end-to-end demonstration would require setting up an evil iSCSI target on the guest, loading the module as root, establishing a session, and issuing a SCSI read β€” beyond what an unprivileged user can drive.

Reported impact: corruption (the demonstrated primitive), with the remote-RCE ceiling documented. This is a Critical remote bug; it is NOT a local unprivileged β†’ root bug on default GENERIC.

4. PHASE 8 β€” fix validation

fix.diff

A one-hunk, minimal, git apply-able unified diff against sys/dev/disk/iscsi/initiator/iscsi_subr.c. The change:

  1. Widens the locals offset/len/edtlen to u_int (eliminates the signed-offset underflow class entirely).
  2. Adds a single bounds check before the pointer arithmetic:

c if (offset > edtlen || len > edtlen - offset) { xdebug("bad data-in bo=%u len=%u edtlen=%u", offset, len, edtlen); break; }

break exits the ISCSI_READ_DATA case without copying, matching the function's existing error-handling style.

This supersedes the finding markdown's proposal (which used the same arithmetic but added a comment-only change); the verdict's fix.diff is functionally identical in intent and additionally tightens the local types to u_int.

Validation results

Check Result
git apply --check -p1 fix.diff (host, on sys/) OK applies cleanly
patch -p1 < fix.diff in guest /usr/src Hunk #1 succeeded at 564
Build iscsi_initiator.ko with fix applied rc=0, no warnings, no errors (see fix_build.log)
Fixed iscsi_subr.o contains the new xdebug string YES: >>> %s: bad data-in bo=%u len=%u edtlen=%u
Unfixed iscsi_subr.o (after patch -R) contains it NO (clean differential)
Harness UNFIXED run 416 bytes written OOB (exit 1)
Harness FIXED run 0 bytes written OOB (exit 0); legal case unaffected

Because the bug is in pointer arithmetic (a deterministic computation, not a stateful race or memory-layout flake), the harness's unfixed-vs-fixed comparison is a fully deterministic proof of the fix. The end-to-end module-level proof (load fixed module, run evil target, observe no OOB) is gated on the same root-only module-load blocker that gates escalation (Β§3), so the harness + object-file string differential is the cleanest deterministic validation available on this guest.

Before / after contrast

baseline (unfixed kernel logic):
  [OOB_pos_bo=0x200_ds=0x100]    -> OOB WRITE  oob_bytes=256
  [OOB_neg_bo=0xFFFFFC00_ds=16]  -> OOB WRITE  oob_bytes=16
  SUMMARY mode=UNFIXED  total_oob_bytes=416  exit=1

patched (offset+len <= edtlen guard):
  [OOB_pos_bo=0x200_ds=0x100]    -> REJECTED by fix
  [OOB_neg_bo=0xFFFFFC00_ds=16]  -> REJECTED by fix
  SUMMARY mode=FIXED  total_oob_bytes=0  exit=0

The fix closes the bug.

5. PoC changes vs. the finding markdown

The finding markdown's "Proof of concept" section described an evil Python iSCSI target plus an iscontrol-driven SCSI read on the victim β€” a correct real-world reproduction recipe, but infeasible to drive from the unprivileged user on this guest (module + session require root). The runner substituted a faithful userspace port of the exact kernel arithmetic (harness.c) that:

  • proves the primitive deterministically (no setup privileged needed),
  • runs identically in UNFIXED and FIXED modes from the same source,
  • makes the before/after comparison crisp and self-contained.

The harness source, build/run scripts, fix.diff, and full untrimmed logs are all in this directory.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. baseline 416 bytes OOB; fixed 0 bytes. Module rc=0.

UNFIXED: 4 OOB writes total 416 bytes. FIXED: 0 OOB. Module builds rc=0 with 'bad data-in' xdebug string.
↓ fix.diff6.5-DEVELOPMENT #0 (module rebuild)

Confirmed kernel references

Detail

Exploit chain

Primitive characterized (write-what-where into kmalloc-edtlen). Hard blocker for GENERIC uid0: iscsi_initiator is loadable (not in GENERIC), kldload needs root, maxx cannot reach path. REMOTE threat (target->initiator) stands as Critical.

Evidence (decisive lines)

UNFIXED: 4 OOB writes (256+128+16+16 bytes). FIXED: 0 OOB. Module builds rc=0 with new xdebug string.

PoC changes

harness.c userspace port, fix.diff (offset+len<=edtlen guard), VERDICT.md, manifest.json, evil_target.py reference

Verified recommended fix

In iscsi_subr.c ISCSI_READ_DATA: change int offset/len to u_int, capture edtlen, add 'if (offset > edtlen || len > edtlen - offset) break;' before dp=data_ptr+offset.

Verdict

REPRODUCED write-what-where. scsi_decap ISCSI_READ_DATA at iscsi_subr.c:566-574 uses attacker bo+offset without bounds vs edtlen. Harness proves 416 bytes OOB across 4 attacker offset patterns.