DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2466

Heap overflow in i_send via ahs_len/ds_len padding mismatch with i_prepPDU

Summary

i_prepPDU computes data-segment padding so total PDU length (BHS48+ahs_len+hdrDigest?+ds_len) is 4-byte aligned i_send allocates pq->len-48 bytes from that. But i_send padding loop rounds up only ds_len ignoring ahs_len. When user supplies ahs_len not multiple of 4 two computations diverge by 1-3 bytes and padding loop writes 1-3 NUL bytes past kmallocd pq->buf. Concrete: ahs_len=3 ds_len=1 i_prepPDU total=52 pad=0 alloc=4 bytes i_send writes ahs(3)+ds(1)+ds-pad(3)=7 bytes into 4-byte buffer 3-byte NUL overflow. Whenever (ahs_len+ds_len)%4==0 but ds_len%4!=0 i_prepPDU adds 0 pad while i_send adds (4-(ds_len&3)) yielding 1-3 byte overflow. Root only device 0600 root:wheel. Reliable kernel-memory-corruption/panic DoS.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2466 · 9 files
FileTypeDescriptionSize
df2466.c trigger-source ISCSISEND ahs_len=7 ds_len=1 heap-overflow PoC (root) 4.3 KB view raw
build.sh build-script cc -O2 df2466.c 92 B view raw
run.sh run-script kldload iscsi_initiator; ./df2466 7 1 8000 640 B view raw
run.log run-log baseline panic x3 + fixed-module no-panic 1.1 KB view raw
panic.txt panic-signature panic: overflowed mbuf, m_free->sbdrop->tcp_input 389 B view raw
env.txt environment uname, cc, module state 316 B view raw
fix.diff suggested-fix reject ahs_len not multiple of 4 in i_send 856 B view raw
fix_build.log build-log single-fix kernel build log (35421 lines) 5.6 MB ↓ download
VERDICT.md verdict full narrative + fix validation 4.8 KB ↓ raw
VERDICT.md verdict full narrative + fix validation
↓ download raw

DF-2466 — Heap overflow in i_send via ahs_len/ds_len padding mismatch

Verdict

REPRODUCED — root-triggerable kernel heap overflow, deterministically panics the default GENERIC kernel; fix validated on a rebuilt module. The ISCSISEND ioctl (/dev/iscsiN, 0600 root:wheel) lets a root process supply an attacker-chosen ahs_len/ds_len. i_prepPDU() sizes pq->buf rounding (48+ahs+ds) to 4, but i_send()'s data-segment pad loop rounds only ds_len to 4. When ahs_len is not a multiple of 4 the two diverge by 1–3 bytes and i_send() writes NUL pad bytes past the allocation. This is a root→kernel corruption (the iSCSI device is root-only), so it is a panic / DoS / hardening gap, not an unprivileged escalation.

Mechanism (trigger → primitive → effect)

i_prepPDU (isc_sm.c:266-291):  len = 48 + ahs_len + ds_len;  if(len&3) len += 4-(len&3);
                               pq->len = len;                 // total rounded
i_send (iscsi.c:461):          bp = kmalloc(pq->len - 48, ...) // = ahs+ds+prep_pad
i_send (iscsi.c:467-490):      copyin ahs (ahs_len); bp += ahs_len;
                               copyin ds  (ds_len);  bp += ds_len;
                               while(n & 03){ n++; *bp++ = 0; }  // n == ds_len ONLY
  • prep_pad = (4 - ((ahs_len+ds_len)&3)) & 3; send_pad = (4 - (ds_len&3)) & 3.
  • Overflow = send_pad - prep_pad bytes when positive, i.e. whenever (ahs_len+ds_len)%4 == 0 but ds_len%4 != 0 (possible only when ahs_len%4 != 0).
  • Concrete (PoC): ahs_len=7, ds_len=1prep_pad=0, send_pad=3, alloc = 8 (exact kmalloc-8 chunk) → 3 NUL bytes written at offsets 8,9,10, 3 bytes into the adjacent kmalloc-8 slab chunk. (Same root cause also overflows in isc_sendPDU at isc_soc.c:235, where iv->iov_len is rounded on ds_len alone and sosend reads past pp->ds.)

Evidence (panic, unpatched #0 + stock module)

panic: overflowed mbuf 0xfffff80117665a00m_free → m_freem → sbdrop → tcp_input → Debugger("panic"). The 3-byte overflow corrupts an adjacent slab object (the slab zone is shared across malloc types of the same size, so an M_ISCSI kmalloc-8 chunk sits next to an mbuf's metadata); when TCP later frees that mbuf, m_free()'s INVARIANTS check fires. Reproduced deterministically on 3 independent fresh vm.sh reset with-src runs (panic addresses 0x...665a00, 0x...e0a000, 0x...d02800). See panic.txt.

Threat model / privilege boundary

  • The trigger is the ISCSISEND ioctl on /dev/iscsiN (0600 root:wheel, confirmed in-guest). So the attacker is a root process (or a flawed root daemon). Root→kernel corruption is game-over by definition; the realistic demonstrated impact is therefore kernel heap corruption / panic DoS, not unpriv→root. Severity Low is appropriate (root-only, requires the non-default iSCSI initiator module, DoS-class).
  • The ahs_len value is fully attacker-controlled from the ioctl argument; the overflow content is NUL bytes (limited control), the offset/size is controlled. On a non-INVARIANTS kernel the corruption is silent.

PoC changes

  • df2466.c (new): opens /dev/iscsi, ISCSISETSES → opens /dev/iscsiN, connects a socket to a local dummy listener, ISCSISETSOC, then loops ISCSISEND with ahs_len=7, ds_len=1 (3-byte overflow past kmalloc-8).
  • build.sh / run.sh.

Fix (fix.diff)

Root-cause fix: reject a non-multiple-of-4 ahs_len at the i_send entry (the iSCSI BHS AHSLength field is counted in 4-byte words, so ahs_len must be a multiple of 4). A non-multiple-of-4 ahs_len is what makes the i_send/isc_sendPDU per-field padding diverge from i_prepPDU's total-length rounding; rejecting it removes the divergence at the source and also closes the sibling isc_sendPDU read overflow (isc_soc.c:235).

     if(pp->ahs_len & 0x3) {
          xdebug("i_send: ahs_len=%d not a multiple of 4", pp->ahs_len);
          error = EINVAL;
          goto out;
     }

Supersedes a pad-only fix: I first tried making i_send's pad loop round (ahs_len+ds_len) — that closes the write overflow but leaves the isc_sendPDU read overflow (and the malformed BHS AHSLength) reachable. The validation fix is one logical change at the root cause and closes both.

Fix validation (rebuilt module)

iscsi_initiator is a loadable module, so the validation rebuilds and installs the module (iscsi_initiator.ko), not the kernel proper: - Baseline (stock iscsi_initiator.ko, sha256 99e1710b..., kernel #0): ./df2466 7 1 8000panic: overflowed mbuf (3/3 fresh-reset runs). - Fixed (iscsi_initiator.ko rebuilt with fix.diff, sha256 977b62dc..., installed at /boot/kernel/iscsi_initiator.ko, rebooted): ./df2466 7 1 8000 → every ISCSISEND returns EINVAL (ok=0 fail=8000), no panic, guest stays up (uptime healthy). fix_status = fixed.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: ./df2466 7 1 8000 deterministically panics ('overflowed mbuf') on stock iscsi_initiator.ko (3/3 fresh-reset baseline runs) and does NOT panic on fixed module (rebuilt with fix.diff, installed /boot/kernel/iscsi_initiator.ko, rebooted) — every ISCSISEND returns EINVAL (ahs_len%4 guard), ok=0 fail=8000, guest stays up => fix closes overflow. NOTE: iscsi_initiator is loadable MODULE not compiled into kernel, so validation rebuilt and replaced /boot/kernel/iscsi_initiator.ko.

baseline (stock module 99e1710b...): panic: overflowed mbuf 0xfffff80117665a00 / m_free->sbdrop->tcp_input (3/3 runs). fixed (module 977b62dc...): ISCSISEND: Invalid argument x8000, ok=0 fail=8000, PATCHED_RC=124 (timeout on lingering forked listener), uptime '10:11AM up 1 min' healthy, NO panic.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; iscsi_initiator loadable module — validated by rebuilding+installing module, sha256 977b62dc3320516c534aa15f02c05217d93d1e009e493561a75ed3cfc457b8e6)

Confirmed kernel references

Detail

Exploit chain

Root-only ioctl (/dev/iscsi 0600 root:wheel) so root->kernel corruption — no unprivileged boundary crossed (root->kernel game-over by definition). Demonstrated impact = deterministic kernel heap corruption / panic DoS on GENERIC (INVARIANTS): 3-byte overflow corrupts adjacent slab object (slab zone shared across malloc types of same size, so M_ISCSI kmalloc-8 chunk sits next to mbuf metadata); m_free()'s INVARIANTS check fires -> 'panic: overflowed mbuf'. No unpriv->root escalation path exists for root-gated primitive; corruption content is NUL bytes (limited control).

Evidence (decisive lines)

Baseline (stock iscsi_initiator.ko, #0): 'panic: overflowed mbuf 0xfffff80117665a00' / m_free()->m_freem()->sbdrop()->tcp_input()->Debugger('panic') — reproduced 3x on fresh resets (addrs 0x...665a00, 0x...e0a000, 0x...d02800). Fixed module (sha256 977b62dc...): all 8000 ISCSISEND return EINVAL (ahs_len%4 guard), ok=0 fail=8000, no panic, guest up.

PoC changes

Created df2466.c (ISCSISEND heap-overflow PoC: opens /dev/iscsi, ISCSISETSES, ISCSISETSOC to local dummy listener, loops ISCSISEND with ahs_len=7 ds_len=1), build.sh, run.sh, fix.diff. First fix attempt (pad-consistency) superseded after discovering isc_sendPDU sibling overflow — root-cause validation fix rejects non-multiple-of-4 ahs_len.

Verified recommended fix

Reject non-multiple-of-4 ahs_len at i_send entry (iscsi.c:456): if(pp->ahs_len & 0x3){ xdebug(...); error=EINVAL; goto out; }. iSCSI BHS AHSLength field counted in 4-byte words, so ahs_len must be multiple of 4; rejecting removes i_send/isc_sendPDU per-field padding divergence from i_prepPDU's total rounding and closes BOTH overflows (i_send write overflow at iscsi.c:486 and isc_sendPDU read overflow at isc_soc.c:235). Full git-apply-able diff in findings/poc/DF-2466/fix.diff.

Verdict

REPRODUCED — root-triggerable kernel heap overflow, deterministically panics default GENERIC kernel. ISCSISEND ioctl (/dev/iscsiN 0600 root:wheel) lets root process supply attacker-chosen ahs_len/ds_len. i_prepPDU() (isc_sm.c:283) sizes pq->buf rounding (48+ahs+ds) to 4, but i_send()'s data-segment pad loop (iscsi.c:486) rounds only ds_len to 4. When ahs_len not multiple of 4 the two diverge: with ahs_len=7, ds_len=1, alloc=8 (exact kmalloc-8 chunk), i_send writes 3 NUL pad bytes at offsets 8,9,10 — 3 bytes into adjacent kmalloc-8 slab chunk. (Same root cause also overflows in isc_sendPDU at isc_soc.c:235, where iv->iov_len rounded on ds_len alone and sosend reads past pp->ds.) Confirmed by panic on 3 independent fresh-reset runs.