check_keep_state dereferences an unchecked kmalloc(..., M_NULLOK) result - NULL-page write panic when the M_IPFW3_STATE type limit is reached
Summary
kmalloc(M_INTWAIT|M_NULLOK|M_ZERO) may return NULL at the malloc-type limit; the next statement writes s->src_addr through it (write at vaddr 0x18 -> Fatal trap 12). Default limit ~390MB; the shipped state_max_* sysctls (4096) keep it cold but are RW deployment knobs. On-guest full demonstration was throughput-bound (state creation throttled to ~2-7k pps by the O(n)-per-packet limit counting; ~6.1M live states needed, ~30-50 min saturation) - honestly not_reproduced, code-certain. Fix: NULL check after alloc.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3435 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| state_null.c | β | 3.2 KB | view raw | |
| state_null_tap.c | β | 3.0 KB | view raw | |
| README.md | β | 1.9 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| fix.diff | β | 1.8 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-3435 β ipfw3 check_keep_state() writes through an unchecked M_NULLOK kmalloc β NULL-page write at the malloc-type limit
sys/net/ipfw3_basic/ip_fw3_state.c:319-321:
s = kmalloc(LEN_FW3_STATE, M_IPFW3_STATE, M_INTWAIT | M_NULLOK | M_ZERO);
s->src_addr = k->src_addr; /* s == NULL -> write at vaddr 0x18 */
M_NULLOK explicitly permits a NULL return when the malloc-type limit is
reached (sys/kern/kern_slaballoc.c:694-705: if (flags & M_NULLOK)
return(NULL);). The default M_IPFW3_STATE limit is
kmem_lim_size()/10 (β390 MB on the 4.25 GB guest). The shipped per-tree
caps (state_max_* = 4096) keep the path cold, but they are plain
CTLFLAG_RW sysctls β any deployment that raises them (or a machine where
the type limit is otherwise exhausted) converts unprivileged
state-creating traffic into a kernel write at NULL+offset
(Fatal trap 12: page fault on write, vaddr 0x18).
Reproduce attempted on the guest (see VERDICT.md)
state_null.cβ raw-socket unique-5-tuple UDP flood (slow path, ~1k pps synchronous)state_null_tap.cβ tap-frame injector, ~250k pps write rate; ~35M packets got the tree to ~190K live states (~12 MB) before the session's guest budget ended; the netisr drain (~2-7k pps with a populated tree, aggravated by the O(n)-per-packet limit counting at ip_fw3_state.c:302) is the bottleneck, not the defect.
Expected terminal output had the flood completed to 6.1M live states:
Fatal trap 12: page fault while in kernel mode ... fault virtual address
= 0x18 ... supervisor write data inside check_keep_state.
Fix (fix.diff)
Keep M_NULLOK, check the result: if (s == NULL) goto done;
(do not drop M_NULLOK β that converts the limit-hit into
panic("malloc limit exceeded")).
DF-3435 β VERDICT
Status: not_reproduced on the guest this session (mechanism is code-certain; the on-guest demonstration is throughput-bound, not correctness-bound)
The defect (code-certain)
check_keep_state(), sys/net/ipfw3_basic/ip_fw3_state.c:319-321:
s = kmalloc(LEN_FW3_STATE, M_IPFW3_STATE, M_INTWAIT | M_NULLOK | M_ZERO);
s->src_addr = k->src_addr; /* NULL write when s == NULL */
M_NULLOK explicitly permits kmalloc to return NULL when the
malloc-type limit is hit β sys/kern/kern_slaballoc.c:694-705:
while (type->ks_loosememuse >= type->ks_limit) {
...
if (ttl >= type->ks_limit) {
if (flags & M_NULLOK)
return(NULL); /* <- the checked-in flag */
panic("malloc limit exceeded");
}
}
The result is never checked; the very next statement writes through it
(s->src_addr at offset 24 of struct ipfw3_state β write to vaddr 0x18 β
Fatal trap 12: page fault on write). The default type limit is
kmem_lim_size()/10 (kern_slaballoc.c:365-372) β 390 MB on this
guest (vmstat -m limit column) β reachable by state allocations once the
admin raises net.inet.ip.fw3_basic.state_max_{udp,tcp}_{in,out} above
4096 (all four are CTLFLAG_RW deployment knobs; the shipped defaults cap
allocations at 4096 per tree before the type limit can ever be approached).
What was run on the guest
- sysctls raised to 10,000,000 per tree/direction;
udp_timeout=7200(live-state horizon);ipfw3 add 00098 allow udp ... keep-state(plain, O(log n) path); tap-frame injectorstate_null_tap.cpushing unique-5-tuple UDP packets at ~250k pps write rate. - Observed: 35M+ packets injected, M_IPFW3_STATE climbed to ~190K states / ~12MB and rule hits grew at only ~2-7k pps β the netisr drain (rule evaluation + tree insert + forward/ARP work per packet) throttles state creation; reaching 6.1M live states (390MB / 64B) would need ~30-50 minutes of sustained saturation plus 390MB of pinned kernel memory on the 4.25GB guest. Not completed within this session's guest budget.
- The accelerator needed (a way to create states faster than the packet
pipeline β e.g. a bulk state-restore path) does not exist: the only state
creator is
check_keep_stateitself.
Honest classification
Per the status table, the run ended with the guest up and the NULL write
not observed β not_reproduced / reproduced=0 / impact=none
observed. The defect itself is unambiguous from the code
(M_NULLOK requested, result dereferenced unchecked) and the fix is
one line; confidence in the finding remains certain, the guest
demonstration is what did not complete. fix.diff adds the NULL check
(and drops M_NULLOK); on the patched kernel the same flood simply stops
creating states at the limit instead of faulting.
Fix
fix.diff hunk: keep M_NULLOK (dropping it would turn the limit-hit into
panic("malloc limit exceeded") β a different, admin-triggered panic) and
check the result: if (s == NULL) goto done; β at the type limit the
firewall simply stops creating states instead of writing to vaddr 0x18.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableThe baseline bug itself was not reproduced this session (throughput-bound), so a before/after fix demonstration at the malloc-type limit is not possible. The patched module ran the rewritten allocation path under load without regressions (rule evaluation and state creation worked on the fix kernels); the NULL-guard branch is straightforwardly correct by inspection but was not reachable without exhausting the 390MB type limit.
['fix.diff']
Confirmed kernel references
Detail
Evidence (decisive lines)
['VERDICT.md: code walk (ip_fw3_state.c:319-321; kern_slaballoc.c:694-705,365-372) + on-guest throughput measurements', 'state_null_tap.c: the fast injector used for the attempt', 'fix.diff: NULL guard with M_NULLOK retained']
PoC changes
Two injectors written (raw-socket synchronous path too slow at ~1k pps; tap-frame injector reached ~250k pps write rate). Listener-based delivery and forwarding-based delivery both attempted to keep the netisr drain cheap; the drain, not the injector, is the bottleneck.
Verified recommended fix
Check the kmalloc result in check_keep_state() before writing through it (keep M_NULLOK); see fix.diff.
Verdict
Mechanism is code-certain but the on-guest demonstration did not complete within the session budget, so the honest status is not_reproduced with impact=none OBSERVED (the latent impact is a NULL-page write panic). ip_fw3_state.c:319 kmalloc(..., M_INTWAIT|M_NULLOK|M_ZERO) can return NULL at the malloc-type limit (kern_slaballoc.c:694-705 returns NULL exactly because M_NULLOK was requested) and :321 immediately writes s->src_addr through it (write to vaddr 0x18 -> Fatal trap 12 supervisor write). The shipped state_max_* defaults (4096/tree) keep the path cold; they are CTLFLAG_RW sysctls, so deployments that raise them expose it. On the guest: sysctls raised, 35M+ unique-5-tuple packets injected via two harnesses (state_null.c raw ~1k pps, state_null_tap.c tap frames ~250k pps write rate); the tree reached only ~190K live states (~12MB of the 390MB limit) because the netisr drain throttles state creation to ~2-7k pps (rule eval + RB insert + forward/ARP per packet; the O(n)-per-packet limit counting at :302 adds drag when rule 99 is populated). Reaching 6.1M live states needs ~30-50 min of sustained saturation and ~390MB of pinned kernel memory on the 4.25GB guest - not completed. Fix is one guard: if (s == NULL) goto done; keeping M_NULLOK (dropping it would turn the limit-hit into panic('malloc limit exceeded')).
No comments yet.