i_prepPDU truncates PDU length to u_int and maxBurstLength accepts negative values enabling kernel heap overflow via ISCSISEND
Summary
i_prepPDU() accumulates ahs_len+ds_len+digest+padding into 64-bit size_t then stores into pq->len which is 32-bit u_int truncating without overflow check. Safety net if(sp->opt.maxBurstLength && len>sp->opt.maxBurstLength) bypassed because i_setopt() copies maxBurstLength verbatim with only !=0 guard so negative maxBurstLength accepted sign-extends to huge size_t in comparison E2BIG always false. With maxBurstLength=-1 local privileged user can choose ahs_len/ds_len so truncated pq->len tiny while i_send() still copyin()s original large ahs_len into tiny kmalloc buffer controlled kernel heap overflow. Concrete: ahs_len=100 ds_len=0xFFFFFFDC size_t len=0x100000070 pq->len truncates to 0x70=112 i_send kmalloc(64) copyin 100 bytes 36-byte heap overflow.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2461 · 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df2461.c | trigger-source | root ISCSISEND ioctl driver: E2BIG-bypass + truncation demos | 5.2 KB | view raw |
| mtarget.c | trigger-source | hold mode: keeps session socket connected | 2.7 KB | view raw |
| build.sh | build-script | cc mtarget + cc -I<iscsi.h> df2461 | 168 B | view raw |
| run.sh | run-script | mtarget hold + df2461 | 707 B | view raw |
| panic.txt | panic-signature | baseline: panic overflowed mbuf in isc_sendPDU | 1.1 KB | view raw |
| dmesg.txt | dmesg | patched: i_prepPDU 'pdu len exceeds 32 bits' (E2BIG) | 318 B | view raw |
| env.txt | environment | uname, kern.version, module sha256 | 465 B | view raw |
| fix.diff | suggested-fix | i_setopt reject negative maxBurst; i_prepPDU reject len>32bit | 1.7 KB | view raw |
| VERDICT.md | verdict | full analysis | 5.5 KB | ↓ raw |
| manifest.json | manifest | this catalog | 2.2 KB | view raw |
DF-2461 — i_prepPDU truncates PDU length to u_int; maxBurstLength accepts negative
Verdict
REPRODUCED — a root-only kernel memory-corruption primitive via the
ISCSISEND ioctl. The two cited integer defects are both real and confirmed:
i_setopt() accepts a negative maxBurstLength (only != 0 is guarded),
which disables i_prepPDU()'s E2BIG safety net, and i_prepPDU() stores a
64-bit size_t length into the 32-bit pq->len without an overflow check.
On the default GENERIC kernel (INVARIANTS ON) the resulting corruption
manifests as a deterministic kernel panic. Fix validated on a rebuilt module.
Privilege / impact ceiling (Phase 6 honesty)
- Trigger requires root.
ISCSISENDis dispatched fromiscsi_ioctl()(iscsi.c:234) with no internal privilege check, but/dev/iscsi*is createdUID_ROOT, GID_WHEEL, 0600(iscsi.c:641,762), so only root can issue it. Root→kernel is game-over by definition (root cankldload), so this is not an unprivileged→root escalation; it is a hardening / defense-in-depth defect. There is no unprivileged path: the ISCSISEND overflow needs a root-opened fd, andi_prepPDU's outgoing-PDU truncation only matters for attacker-controlledahs_len/ds_len(the CAM-generated SCSI-CMD path uses small fixed sizes). This is the "write reachable only from an already-root context" valid hard blocker for an escalation claim. - The demonstrated effect on GENERIC is therefore panic (DoS) from a privileged user; on a noinv kernel it would be a silent heap/mbuf overflow (still root-only).
Mechanism (the negative-maxBurstLength bypass → mbuf overflow)
i_setopt()(isc_subr.c:99) storesmaxBurstLengthwith only anif(opt->maxBurstLength != 0)guard, somaxBurstLength = -1is accepted.i_prepPDU()(isc_sm.c:293) computeslenassize_t, then testsif(sp->opt.maxBurstLength && (len > sp->opt.maxBurstLength)). WithmaxBurstLength == -1(int) andlen(size_t), the int is converted to(size_t)0xFFFFFFFFFFFFFFFF, so the comparison is always false — E2BIG is never returned for any size.i_send()(iscsi.c:461) proceeds;isc_qout()queues the PDU;proc_out()→isc_sendPDU()(isc_soc.c:120) doesbcopy(pp->ahs, mh->m_data + mh->m_len, pp->ahs_len). The mbuf header has only~MHLENbytes of room, so a 70000-byte AHS overflows it.
The truncation (separate, same routine)
i_prepPDU() (isc_sm.c:291) does pq->len = len; where len is size_t
(64-bit) and pq->len is u_int (32-bit, iscsivar.h:182). Choosing
ahs_len=200, ds_len=0xFFFFFF68 makes len = 0x100000060, which truncates to
pq->len = 0x60 = 96; then i_send() does kmalloc(96-48=48) (kmalloc-64
chunk) but copyin(pp->ahs, bp, 200) — a 152-byte heap overflow. On GENERIC
this is silently corrupted into adjacent slab chunks (copyin traps any
slab-page-boundary fault and returns EFAULT, so no immediate panic unless the
neighbour chunks are later alloc/freed and INVARIANTS catches them).
Evidence
Baseline (unpatched #0) — negative-maxBurstLength bypass → mbuf overflow panic:
>>> i_prepPDU: 0] pdu len=70000 > 65536 <- test (A): default maxBurst -> E2BIG (correct) panic: overflowed mbuf 0xfffff801187af400 <- test (A2): maxBurst=-1 -> bypass -> isc_sendPDU overflow cpuid = 3 m_free() at m_free+0x351 m_freem() at m_freem+0x15 sosend() at sosend+0xf1 isc_sendPDU() at isc_sendPDU+0xa12 ism_proc() at ism_proc+0x3ee Stopped at Debugger+0x7c
(The (A) line is the default-maxBurst case correctly returning E2BIG; (A2)
sets maxBurstLength=-1, bypasses E2BIG, and the oversized AHS overflows the
mbuf header in isc_sendPDU.)
Reproduced by df2461 (root ioctl driver) + mtarget hold (keeps the session
socket connected so i_send does not short-circuit with ENOTCONN). See
panic.txt for the full trace.
PoC changes
mtarget.c(new): malicious-target harness;holdmode keeps the TCP connection open soISCSISETSOCsetssp->socfor the ISCSISEND path.df2461.c(new): root ioctl driver demonstrating (A) E2BIG with default maxBurst, (A2) the negative-maxBurst bypass, and (B) the size_t→u_int truncation. Includes the kerneliscsi.h(matchessizeof(pdu_t)=96,sizeof(isc_opt_t)=208) for correct ioctl encoding.fix.diff(new): two-part fix.
Fix (fix.diff) — two logical changes
isc_subr.c:99i_setopt(): change the guard from!= 0to> 0, so a negativemaxBurstLengthis rejected (the default 65536 is kept). This also makes thelen > sp->opt.maxBurstLengthcomparison ini_prepPDUcorrect (no sign-extension to a huge size_t).isc_sm.c:291i_prepPDU(): rejectlen > 0xffffffff(E2BIG) before thepq->len = lentruncation, so a wrapped length cannot produce an undersizedkmallocfollowed by a full-lengthcopyin.
Matches the finding markdown's ## Recommended fix proposal (which
identified both the truncation and the negative-acceptance); this implements
both at the exact lines with return-E2BIG semantics.
Fix validation (rebuilt single module)
- Baseline (unpatched
#0): test (A2) →panic: overflowed mbufinisc_sendPDU(panic.txt). - Patched module (sha256
b5181f50...): all three tests return E2BIG cleanly — (A) oversized/default-maxBurst → E2BIG; (A2) negativemaxBurstLengthrejected (stays default) → E2BIG; (B) wrapped length → E2BIG, dmesg shows">>> i_prepPDU: 0] pdu len=4294967392 exceeds 32 bits". No panic, guest up.
Fix verification
fixedVALIDATED: df2461 test (A2) [maxBurst=-1, ahs_len=70000] PANICS on unpatched #0 module ('panic: overflowed mbuf' in isc_sendPDU) and returns E2BIG (no panic) on single-fix module -- negative maxBurst now rejected (stays default 65536). Test (B) [truncation] also returns E2BIG on patched, dmesg '>>> i_prepPDU: 0] pdu len=4294967392 exceeds 32 bits'.
baseline: panic: overflowed mbuf in isc_sendPDU->sosend->m_free (negative-maxBurst bypass). patched: (A) E2BIG, (A2) E2BIG (negative rejected), (B) E2BIG + dmesg 'pdu len=4294967392 exceeds 32 bits' -- no panic, guest up.
Confirmed kernel references
Detail
Exploit chain
none for escalation (valid Phase 6 blocker: write path ISCSISEND requires already-root credential -- /dev/iscsi 0600 root:wheel, opened by root only -- so no privilege boundary to cross; root can already kldload/modify kernel). Demonstrated impact: root-triggered kernel memory corruption (mbuf overflow) -> panic on GENERIC (INVARIANTS); on noinv kernel silent heap/mbuf overflow, still root-only. The negative-maxBurstLength bypass and size_t->u_int truncation both real and reproduced.
Evidence (decisive lines)
>>> i_prepPDU: 0] pdu len=70000 > 65536 [test A: default maxBurst -> E2BIG, correct] / panic: overflowed mbuf 0xfffff801187af400 [test A2: maxBurst=-1 -> bypass -> isc_sendPDU overflow] / m_free() m_freem() sosend() isc_sendPDU() at isc_sendPDU+0x2a2 ism_proc() at ism_proc+0x3ee
PoC changes
Wrote df2461.c (root ioctl driver: opens /dev/iscsi, ISCSISETSES, ISCSISETSOC via held mtarget socket, ISCSISETOPT maxBurstLength=-1, then ISCSISEND with oversized AHS and with truncation values; demonstrates E2BIG bypass + truncation), mtarget.c (hold mode), fix.diff (two-part).
Verified recommended fix
Two changes: (1) isc_subr.c:99 i_setopt: change guard from '!= 0' to '> 0' so negative maxBurstLength rejected (also makes i_prepPDU size_t-vs-int comparison correct); (2) isc_sm.c:291 i_prepPDU: reject len>0xffffffff (E2BIG) before size_t->u_int truncation into pq->len. Matches finding markdown's Recommended fix proposal (implements both at exact lines). Full git-apply-able diff in findings/poc/DF-2461/fix.diff.
Verdict
REPRODUCED (root-only). i_setopt (isc_subr.c:99) accepts a NEGATIVE maxBurstLength (guard is only '!=0'); with maxBurstLength=-1, i_prepPDU's safety check 'len(size_t) > maxBurstLength(int)' sign-extends -1 to (size_t)0xFFFFFFFFFFFFFFFF so E2BIG never returned. The oversized PDU (ahs_len=70000) then reaches isc_sendPDU (isc_soc.c:120) which bcopy()s the 70000-byte AHS into the ~MHLEN-byte mbuf header -> 'panic: overflowed mbuf' in m_free<-sosend<-isc_sendPDU. The separate size_t->u_int truncation (pq->len at isc_sm.c:291) also confirmed: ahs_len=200+ds_len=0xffffff68 makes len=0x100000060, truncates to pq->len=0x60, i_send kmalloc(48) but copyin(200) -- a heap overflow, caught at code level and by new 32-bit guard on patched. PRIVILEGE: /dev/iscsi* is UID_ROOT,GID_WHEEL,0600 (iscsi.c:641,762) and ISCSISEND has no internal priv check, so this is a ROOT-ONLY path -- root->kernel game-over by definition, hardening/defense-in-depth defect, NOT unprivileged escalation.
No comments yet.