β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2460

_reject dereferences pq->mp without NULL check panicking on data-less REJECT from malicious target

Summary

_reject() unconditionally dereferences received PDUs mbuf chain as pdu=mtod(pq->mp pdu_t*). so_recv() never attaches mbuf when PDU carries no AHS no data segment no digests - pq->mp stays NULL. Target sending REJECT with DSLength=0 and AHSLength=0 (with digests off default) drives mtod(NULL) read from address 0 kernel panic. Remote DoS. Attacker: iSCSI target full-feature phase send 48-byte REJECT BHS opcode=0x3f AHSLength=0 DSLength=0. Default config vulnerable.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2460 Β· 10 files
FileTypeDescriptionSize
mtarget.c trigger-source malicious iSCSI target: injects data-less REJECT 3.6 KB view raw
idrv.c trigger-source minimal initiator driver: ISCSISETSOC starts receiver pre-login 2.8 KB view raw
build.sh build-script cc -o mtarget mtarget.c; cc -o idrv idrv.c 123 B view raw
run.sh run-script launch mtarget reject + idrv 769 B view raw
panic.txt panic-signature Fatal trap 12 va=0x10 ism_recv+0xee 654 B view raw
dmesg.txt dmesg patched: _reject NULL-mbuf drop message 88 B view raw
env.txt environment uname, kern.version, cc, module sha256 482 B view raw
fix.diff suggested-fix NULL-check pq->mp in _reject, drop data-less REJECT 910 B view raw
VERDICT.md verdict full analysis 5.0 KB ↓ raw
manifest.json manifest this catalog 2.1 KB view raw
VERDICT.md verdict full analysis
↓ download raw

DF-2460 β€” _reject dereferences pq->mp without a NULL check

Verdict

REPRODUCED β€” remote kernel panic (DoS) from a malicious iSCSI target. The bug is a NULL-pointer read (page fault), not a write primitive, so no privilege escalation chain applies; the realistic impact ceiling is a remote denial of service against any host running the iscsi_initiator module that connects to a malicious/compromised iSCSI target. Fix validated on a rebuilt module.

Mechanism (trigger β†’ primitive β†’ effect)

  1. A root user starts an iSCSI session and hands a connected TCP socket to the kernel via the ISCSISETSOC ioctl. i_setsoc() (sys/dev/disk/iscsi/initiator/iscsi.c:413) immediately calls isc_start_receiver() (iscsi.c:430), which spawns the isc_soc receiver thread (isc_soc.c:568). The receiver runs before iSCSI login completes, so it processes whatever the target sends next.
  2. The malicious target sends a 48-byte REJECT PDU (opcode=0x3f) with AHSLength=0, DSLength=0 and no digests (the default, no CRC32C).
  3. so_recv() (isc_soc.c:365) computes len = 0 for such a PDU and therefore skips the if(len){...} block that assigns pq->mp = sbp.sb_mb (isc_soc.c:428-481); pq->mp stays NULL (set by memset in pdu_alloc, iscsivar.h:321).
  4. ism_recv() dispatches the REJECT to _reject() (isc_sm.c:448 β†’ isc_sm.c:79). The very first statement dereferences the NULL mbuf: c pdu = mtod(pq->mp, pdu_t *); /* isc_sm.c:87 */ mtod(m,t) expands to (t)((m)->m_data); m_data lives at offset 0x10 in struct mbuf, so the kernel reads from address 0x10 β†’ page fault.

Evidence (panic signature, unpatched #0 kernel)

Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x10
fault code      = supervisor read data, page not present
Stopped at      ism_recv+0xee:  movq    0x10(%rdx),%rdx
db>

The faulting instruction (movq 0x10(%rdx),%rdx with %rdx==0) is exactly the mtod(NULL,...) read of m_data at offset 0x10. Reproduced by mtarget reject (sends the REJECT) + idrv (passes a connected socket to the kernel via ISCSISETSOC).

Threat model / privilege boundary

  • The attacker is the iSCSI target (a malicious or compromised storage server). The victim is the kernel of any host whose iscsi_initiator connects to it. No authentication is required by the iSCSI default (RFC 3720), and the bug fires before login completes, so the malicious PDU needs no valid session state.
  • Triggering requires ISCSISETSOC, which needs open("/dev/iscsi") β€” the device is UID_ROOT, GID_WHEEL, 0600 (iscsi.c:641,762), so a privileged user must initiate the connection. That is the normal operational model (iscontrol(8) runs as root). Once a session exists, the attacker (target) controls the damage; the boundary crossed is network β†’ kernel.

Why no escalation (Phase 6 hard blocker)

The primitive is a read from a fixed low address (NULL + 0x10) β€” it is a NULL-pointer dereference, not an attacker-controlled write. There is no attacker-influenced destination, no content control, and no freed/reused object. This is the "genuinely read-only / no write primitive" valid hard blocker from Phase 6: the only achievable effect is crashing the kernel. (The controlled fault address 0x10 is not mappable on this kernel and conveys no primitive.)

PoC changes

  • mtarget.c (new): minimal TCP server that injects a crafted PDU per mode. For DF-2460, sends a 48-byte REJECT BHS (opcode=0x3f, F=1, AHS=0, DS=0).
  • idrv.c (new): minimal initiator driver β€” open /dev/iscsi, ISCSISETSES, open /dev/iscsiN, socket()+connect(), ISCSISETSOC β€” to start the kernel receiver without performing iSCSI login, exactly the precondition i_setsoc() creates.
  • fix.diff (new): NULL-check pq->mp in _reject and drop the PDU.

Fix (fix.diff)

Add a NULL guard at sys/dev/disk/iscsi/initiator/isc_sm.c:87 (_reject): if pq->mp == NULL (data-less REJECT), log via xdebug and pdu_free() the PDU instead of mtod()-dereferencing it. Without the data segment the rejected task tag cannot be recovered, so dropping the PDU is the correct safe action. Supersedes the finding markdown's proposal (the markdown suggested a NULL check; this implements it at the exact faulting line with the drop semantics).

Fix validation (rebuilt single module, not a full kernel)

  • Baseline (unpatched #0 module): PoC panics the guest β€” Fatal trap 12, fault virtual address = 0x10, ism_recv+0xee. (panic.txt)
  • Patched module (rebuilt iscsi_initiator.ko, reinstalled, kldloaded, sha256 b1e16868...): PoC runs clean β€” idrv reports "still alive after 6s (no panic)"; dmesg shows ">>> _reject: 0] REJECT with NULL mbuf (no data segment) - dropping".
  • The module rebuild was used because this is a loadable KLD (bsd.kmod.mk), so a full nativekernel rebuild is unnecessary; the patched .c is compiled with the same flags/KERNCONF and linked into the module.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: mtarget(reject)+idrv PANICS on unpatched #0 module ('Fatal trap 12 va=0x10 Stopped at ism_recv+0xee') and does NOT panic on single-fix module (idrv 'still alive after 6s', dmesg '>>> _reject: 0] REJECT with NULL mbuf (no data segment) - dropping').

baseline: Fatal trap 12 va=0x10 supervisor read data, Stopped at ism_recv+0xee: movq 0x10(%rdx),%rdx. patched: '>>> _reject: 0] REJECT with NULL mbuf (no data segment) - dropping' -- 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 b1e16868cd81d9071000ebe70960c6f2e45b348447c35ba2108b626dbd1c8bd7

Confirmed kernel references

Detail

Exploit chain

none (NULL-pointer READ -- mtod(NULL) deref at fixed offset 0x10). Read-only NULL-deref, not write primitive; no escalation derivable (Phase 6 read-only hard blocker). Realistic impact: remote kernel panic / DoS from malicious iSCSI target.

Evidence (decisive lines)

Fatal trap 12: page fault while in kernel mode | fault virtual address = 0x10 | fault code = supervisor read data, page not present | Stopped at ism_recv+0xee: movq 0x10(%rdx),%rdx [mtod(pq->mp==NULL)]

PoC changes

Wrote mtarget.c (reject mode: 48-byte REJECT BHS opcode=0x3f F=1 AHS=0 DS=0) and idrv.c (ISCSISETSOC starts receiver pre-login). Plus fix.diff (NULL-check pq->mp in _reject).

Verified recommended fix

In _reject (isc_sm.c:87) NULL-check pq->mp before mtod; if NULL (data-less REJECT) xdebug+pdu_free+return (without data segment the rejected task tag cannot be recovered). Supersedes finding proposal (implements NULL check at faulting line with drop semantics). Full git-apply-able diff in findings/poc/DF-2460/fix.diff.

Verdict

REPRODUCED. _reject (isc_sm.c:87) does pdu = mtod(pq->mp, pdu_t*) unconditionally. so_recv (isc_soc.c:428-481) only assigns pq->mp inside if(len){...}; a REJECT with AHSLength=0, DSLength=0, no digests has len=0 so pq->mp stays NULL (pdu_alloc memset). mtod(NULL) reads m_data at offset 0x10 -> page fault at 0x10. Confirmed by 'Stopped at ism_recv+0xee: movq 0x10(%rdx),%rdx' with fault virtual address 0x10.