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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3435 Β· 7 files
FileTypeDescriptionSize
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")).

VERDICT.md
↓ download raw

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 injector state_null_tap.c pushing 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_state itself.

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_testable
baseline no→ patch + rebuild →patched clean

The 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 16:56:02 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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')).