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)
PoC verification
Evidence pack
findings/poc/DF-2463 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 makesobjcache_get()sleep until a PDU is available and never returns NULL. The code instead re-usesM_NOWAIT. pdu_alloc()(iscsivar.h:296) checks the per-sessionfreepduTAILQ first, then falls back toobjcache_get(isc->pdu_zone, wait). Thepdu_zoneis 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_allocreturns NULL on both calls; line 545 then dereferencespqβ 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:
- The per-session
freepduTAILQ must be empty.pdu_allocserves from it before ever calling the objcache. For every received PDU the receiver dispatches it throughism_recv()and the opcode handler frees the pdu immediately β_nop_in(isc_sm.c:195) frees atisc_sm.c:255,_reject/_async/_r2t/_read_dataall callpdu_free()at the end of their handler. So in steady-state reception each pdu is recycled back onto the freepdu list before the nextpdu_alloc; the list almost never drains, and the objcacheM_NOWAITpath is almost never taken. - System memory pressure must make
kmalloc(M_NOWAIT)fail. Even with the objcache path reached, small-slab allocations (pduq_tis 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 untilmallocfails, 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 forcesso_inputβpdu_alloc).idrv2463.c(new, from DF-2459'sidrv.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 forv_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_testablenot_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;'.
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.
No comments yet.