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)
PoC verification
Evidence pack
findings/poc/DF-2466 · 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_padbytes when positive, i.e. whenever(ahs_len+ds_len)%4 == 0butds_len%4 != 0(possible only whenahs_len%4 != 0). - Concrete (PoC):
ahs_len=7, ds_len=1→prep_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 inisc_sendPDUatisc_soc.c:235, whereiv->iov_lenis rounded onds_lenalone andsosendreads pastpp->ds.)
Evidence (panic, unpatched #0 + stock module)
panic: overflowed mbuf 0xfffff80117665a00 — m_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_lenvalue 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 withahs_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 8000 → panic: 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
fixedVALIDATED: ./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.
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.
No comments yet.