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

NULL-deref panic in so_input: pdu_alloc retry uses M_NOWAIT instead of M_WAITOK then dereferences pq unconditionally

Summary

so_input() allocates PDU with pdu_alloc(M_NOWAIT) on failure retries with pdu_alloc(M_NOWAIT) again despite trailing comment OK to WAIT then unconditionally writes pq->pdu.ipdu.bhs at :545 with no NULL check. Under pdu-pool exhaustion or kernel memory pressure second allocation also fails pq stays NULL write at :545 dereferences NULL panicking kernel. Author intent (comment) was to block on retry via M_WAITOK which makes objcache_get() sleep until PDU available never returns NULL. pdu pool bounded MAX_PDUS=65536 exhaustible by attacker who can create enough outstanding sessions/commands. M_NOWAIT also fails under system-wide low-memory any local user can induce. iSCSI commonly used for root/boot volumes panic can wedge box whose root on iSCSI.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2463 Β· 10 files
FileTypeDescriptionSize
memhog.c trigger-source unprivileged memory-pressure driver 1.4 KB view raw
mtarget2463.c trigger-source malicious target blasting NOP-IN stream 2.5 KB view raw
idrv2463.c trigger-source starts kernel receiver (no login), 30s window 1.8 KB view raw
build.sh build-script cc -O2 all three 179 B view raw
run.sh run-script swap off -> memhog -> pressure -> mtarget+idrv 1.4 KB view raw
build.log build-log build + fix module compile check 469 B view raw
run.log run-log v_free_count driven to 9237, no panic 921 B view raw
env.txt environment uname, cc, vm stats 274 B view raw
fix.diff suggested-fix NULL check after pdu_alloc retry -> ENOBUFS 549 B view raw
VERDICT.md verdict full narrative 5.9 KB ↓ raw
VERDICT.md verdict full narrative
↓ download raw

DF-2463 β€” NULL-deref panic in so_input: pdu_alloc retry uses M_NOWAIT then derefs NULL

Verdict

INCONCLUSIVE β€” confirmed latent NULL-deref, but the live trigger is impractical to reproduce in this guest. The bug is unambiguously present in the source (the retry uses M_NOWAIT despite the comment "OK to WAIT", and the subsequent pq->pdu.ipdu.bhs = sp->bhs deref at isc_soc.c:545 is unconditional); reproducing the actual panic requires pdu_alloc(M_NOWAIT) to fail twice in a row while a PDU is being received, a condition the receiver's freepdu recycling defeats under all realistic workloads. The fix is straightforward and compiles.

Mechanism (confirmed by source)

// sys/dev/disk/iscsi/initiator/isc_soc.c:540-545
pq = pdu_alloc(sp->isc, M_NOWAIT);
if(pq == NULL) { // XXX: might cause a deadlock ...
     debug(3, "out of pdus, wait");
     pq = pdu_alloc(sp->isc, M_NOWAIT);  // OK to WAIT     <-- M_NOWAIT, not M_WAITOK
}
pq->pdu.ipdu.bhs = sp->bhs;                               <-- unconditional NULL deref
  • Author intent (the trailing comment "OK to WAIT") was to block on the retry via M_WAITOK, which makes objcache_get() sleep until a PDU is available and never returns NULL. The code instead re-uses M_NOWAIT.
  • pdu_alloc() (iscsivar.h:296) checks the per-session freepdu TAILQ first, then falls back to objcache_get(isc->pdu_zone, wait). The pdu_zone is an unbounded malloc-backed objcache (iscsi.c:721, objcache_malloc_alloc).
  • If the freepdu list is empty and kmalloc(sizeof(pduq_t), M_NOWAIT) fails (system low-memory), pdu_alloc returns NULL on both calls; line 545 then dereferences pq β†’ kernel NULL-deref panic.

Why it did not reproduce (reachability hard blocker)

Two genuine preconditions must coincide, and the receiver's design defeats the first under all normal workloads:

  1. The per-session freepdu TAILQ must be empty. pdu_alloc serves from it before ever calling the objcache. For every received PDU the receiver dispatches it through ism_recv() and the opcode handler frees the pdu immediately β€” _nop_in (isc_sm.c:195) frees at isc_sm.c:255, _reject/_async/_r2t/_read_data all call pdu_free() at the end of their handler. So in steady-state reception each pdu is recycled back onto the freepdu list before the next pdu_alloc; the list almost never drains, and the objcache M_NOWAIT path is almost never taken.
  2. System memory pressure must make kmalloc(M_NOWAIT) fail. Even with the objcache path reached, small-slab allocations (pduq_t is a few hundred bytes) are served from existing slab objects / per-CPU magazines and succeed long after the page allocator is strained.

I engineered genuine memory pressure in the guest (unprivileged memhog allocating + touching memory with vm.swap_enabled=0, driving vm.stats.vm.v_free_count down to 9237 pages (~36 MB), far below vm.v_free_min=5055's neighborhood) while a malicious target blasted a 30 s stream of 48-byte NOP-IN PDUs into the kernel receiver (mtarget2463 + idrv2463). No panic. Because the receiver recycles each pdu via the freepdu list, pdu_alloc(M_NOWAIT) always found a free pdu and the objcache failure path was never hit. Exhausting the freepdu list would require a contrived workload that holds thousands of pdus outstanding simultaneously (e.g. many concurrent sessions with commands the target never completes) and simultaneous memory pressure β€” a real but impractical-to-engineer condition for a single-PoC guest run.

This is therefore an honest inconclusive: the deref is real and unconditional in the source, but the failure precondition (double M_NOWAIT failure while a PDU is in flight) does not manifest on this guest under the strongest pressure I could apply.

Threat model / severity

  • Latent kernel NULL-deref panic. The realistic trigger is an attacker who can both (a) exhaust the per-session pdu pool and (b) induce system memory pressure, then drive the receiver. iSCSI is commonly used for root/boot volumes, so a panic here can wedge a box whose root is on iSCSI. Severity Medium is appropriate (latent DoS, hard to trigger).

PoC changes

  • memhog.c (new): aggressive unprivileged memory-pressure driver (allocates+touches memory until malloc fails, then churns the resident set).
  • mtarget2463.c (new): malicious target that accepts one connection and blasts a continuous stream of 48-byte NOP-IN PDUs (each forces so_input β†’ pdu_alloc).
  • idrv2463.c (new, from DF-2459's idrv.c): starts the kernel receiver (ISCSISETSES+ISCSISETSOC) without login and keeps it running for 30 s.
  • build.sh / run.sh: build all three; run swap-off β†’ memhog β†’ wait for v_free_count < 12000 β†’ mtarget+idrv.

Fix (fix.diff)

Add a NULL check after the retry so a double failure drops the PDU and returns ENOBUFS instead of dereferencing NULL (minimal, targeted at the unconditional deref; honours the author's deadlock concern by not switching to M_WAITOK):

  if(pq == NULL) {
       xdebug("pdu_alloc failed in so_input, dropping PDU");
       return ENOBUFS;
  }

Supersedes the finding markdown's framing: the finding suggested switching the retry to M_WAITOK; this fix instead keeps M_NOWAIT (avoiding the author's deadlock worry) and adds the missing NULL guard, which is the defensive minimum.

Fix validation

Status: not_testable (the live trigger is impractical on this guest, so a before/after panic comparison cannot be run). Validated that fix.diff applies cleanly to sys/dev/disk/iscsi/initiator/isc_soc.c and that the patched iscsi_initiator module compiles (make in sys/dev/disk/iscsi/initiator β†’ iscsi_initiator.ko, MAKE_RC=0, no warnings under -Werror). The change is a 4-line NULL check; it cannot change behaviour on the unpatched path and only prevents the deref on the failure path.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: live panic trigger cannot be induced on this guest (freepdu recycling + small-slab kmalloc survive v_free_count=9237 pressure), so no before/after panic comparison possible. Validated fix.diff applies cleanly to sys/dev/disk/iscsi/initiator/isc_soc.c and patched iscsi_initiator module compiles (make in sys/dev/disk/iscsi/initiator -> iscsi_initiator.ko, MODULE_MAKE_RC=0, no warnings under -Werror). 4-line NULL check; only fires on (unreachable here) failure path.

Baseline: no panic achievable even at v_free_count=9237 (freepdu recycling). Fix compile-check: 'cc ... -c isc_soc.c' + 'cc -Wl,... -o iscsi_initiator.ko iscsi.o isc_cam.o isc_soc.o isc_sm.o isc_subr.o iscsi_subr.o' / MODULE_MAKE_RC=0. Patched region verified: 'if(pq == NULL) { xdebug(...); return ENOBUFS; }' inserted before 'pq->pdu.ipdu.bhs = sp->bhs;'.
↓ fix.diffn/a (module-only compile check; live trigger impractical)

Confirmed kernel references

Detail

Exploit chain

none (not memory-corruption primitive that fired; latent NULL-deref whose failure precondition could not be induced). DoS-class latent panic: IF pdu_alloc(M_NOWAIT) ever fails twice in so_input (pool exhaustion + low memory), kernel NULL-derefs at isc_soc.c:545. No escalation chain applies.

Evidence (decisive lines)

memhog drove v_free_count=847399->527380->298256->9237 (below v_free_min=5055 neighborhood) with swap off; idrv2463 receiver ran 30s under continuous NOP-IN stream: 'idrv: receiver running for 30s / idrv: alive after 30s / IDRV_EXIT=0' β€” NO panic. Source confirmation: isc_soc.c:540-545 (M_NOWAIT retry + unconditional deref), iscsivar.h:300-308 (freepdu TAILQ checked before objcache), isc_sm.c:255 (_nop_in frees pdu -> recycles).

PoC changes

Created memhog.c (aggressive unprivileged memory-pressure driver), mtarget2463.c (malicious target blasting continuous NOP-IN stream), idrv2463.c (idrv variant with 30s receiver window), build.sh, run.sh, fix.diff. Removed 3GB cap and added pressure polling.

Verified recommended fix

Add NULL check after pdu_alloc retry in so_input() (isc_soc.c:545): if(pq==NULL){ xdebug('pdu_alloc failed in so_input, dropping PDU'); return ENOBUFS; }. Honors author's deadlock worry by NOT switching to M_WAITOK (comment 'might cause a deadlock') while preventing unconditional NULL deref. Supersedes finding markdown framing (suggested M_WAITOK); NULL guard is defensive minimum. Full git-apply-able diff in findings/poc/DF-2463/fix.diff.

Verdict

INCONCLUSIVE β€” confirmed latent NULL-deref, but live trigger impractical on this guest. Bug unambiguous in source: so_input() retries pdu_alloc() with M_NOWAIT again despite trailing comment 'OK to WAIT' (isc_soc.c:543), then unconditionally derefs pq->pdu.ipdu.bhs at isc_soc.c:545. Reproduction requires pdu_alloc(M_NOWAIT) to fail TWICE while PDU being received. pdu_alloc() serves from per-session freepdu TAILQ before objcache (iscsivar.h:296), and every received PDU's opcode handler frees it back immediately (_nop_in frees at isc_sm.c:255), so in steady-state reception freepdu list never drains and objcache M_NOWAIT path virtually never hit. Drove vm.stats.vm.v_free_count to 9237 pages (~36MB, below vm.v_free_min) with unprivileged memhog under vm.swap_enabled=0 while malicious target blasted 30s NOP-IN stream into kernel receiver (idrv2463) β€” no panic: small-slab kmalloc still succeeded and freepdu recycling defeated pool exhaustion. Exhausting freepdu list would require contrived workload holding thousands of PDUs outstanding simultaneously + memory pressure β€” real but impractical-to-engineer condition for single-PoC guest run.