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

alist API performs no release-kernel domain validation: alist_alloc(count==0) silently wipes whole 32-block leaves or spins forever with the caller's lock held; alist_create/init with blocks>2^29 radix-wraps into an infinite loop; count==bl_radix full-capacity allocation off-by-one on leaf-root alists

Field Value
ID DF-2797
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-20 / CWE-835 / CWE-191
File sys/kern/subr_alist.c
Lines 250-251, 264, 494-502, 152-157, 196-201
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

The only guard against count==0 is the INVARIANTS-only KKASSERT at :250. On production kernels count=0 passes the power-of-2 test and reaches alst_leaf_alloc, where mask=(u32)-1>>32 (UB; x86 β†’ 0xFFFFFFFF): a fully-free leaf is entirely cleared "allocating 0 blocks" (2048 calls strand the whole 65536-block vm_contig DMA-reserve geometry while bl_free still claims 65536; verified live), and a partially-free leaf hangs forever in for(j=0;j<=n;j+=count) β€” in-kernel that spin holds vm_contig_spin, permanently hanging the system. alist_create/init radix computation overflows alist_blk_t for blocks>2^29 despite the header advertising 2 billion blocks β€” infinite loop at create/boot. Separately the top-level guard count < bl->bl_radix rejects count==bl_radix, so a leaf-root (≀32-block) alist can never allocate its full capacity (fuzzer-found legal-input off-by-one). No in-tree caller passes invalid inputs β€” caller-bug amplification / exported-KLD-API hardening; 5.1M legal-op fuzz proved the legal state machine sound.

Proof of contest

VERIFIED (findings/poc/DF-2797/): KLD on stock INVARIANTS β†’ panic at :250 (panic.txt); -DNDEBUG standalone β†’ 2048 zero-count allocs strand all 65536 blocks and the 1-block alloc then returns NONE; hang reproduced via SIGALRM after 5s spins. Fix (explicit domain rejection: count==0 β†’ ALIST_BLOCK_NONE, blocks bounds β†’ NULL/panic, < β†’ <=) validated on rebuilt kernel #1: KLD clean, standalone 0 wipes / no hangs. No uid0 route (availability only).

See findings/poc/DF-2797/fix.diff (validated).

Timeline

  • 2026-08-31 Discovered during pass-2 audit of subr_alist.c (GLM 5.3); INVARIANTS panic + release-kernel wipe/hang reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2797 Β· 17 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
trigger_domain.c β€” 3.3 KB view raw
kld_adomain/adomain.c β€” 1.3 KB view raw
kld_adomain/Makefile β€” 86 B ↓ download
build.sh β€” 253 B view raw
run.sh β€” 129 B view raw
build.log β€” 143 B view raw
run.log β€” 998 B view raw
run.2.log β€” 998 B view raw
panic.txt β€” 797 B view raw
env.txt β€” 584 B view raw
fix.diff β€” 1.5 KB view raw
fix_validation.log β€” 1.7 KB view raw
fuzz_alist.c β€” 12.9 KB view raw
fuzz.log β€” 69.6 KB view raw
fuzz_seeds.txt β€” 2.2 KB view raw

DF-2797 β€” alist API lacks release-kernel domain validation (count==0 wipes/hangs; blocks>2^29 radix-wrap; full-capacity off-by-one)

What this pack contains

artifact what it is
trigger_domain.c standalone trigger compiling the real sys/kern/subr_alist.c with -DNDEBUG (emulates a production kernel where KKASSERT is a no-op)
kld_adomain/ KLD that calls alist_alloc(own_alist, 0, 0) in kernel mode (legitimate allocator unit-testing pattern)
panic.txt serial-console capture of the resulting stock-kernel (INVARIANTS) panic at subr_alist.c:250
run.log full standalone trigger output (T1 silent leaf-wipe, T2 infinite loop, T3 create-wrap infinite loop)
fuzz_alist.c, fuzz.log 4.4M-op differential model-test of legal API sequences β€” CLEAN (negative-result evidence scoping this finding to invalid inputs only)
build.sh, run.sh exact commands
fix.diff git-apply-able fix (validated: see VERDICT.md)

Build

cc -O2 -g -DNDEBUG -I <srcroot>/sys -o trigger_domain trigger_domain.c

(-DNDEBUG = production-kernel emulation; the in-kernel proof uses kld_adomain/.)

Run

./trigger_domain            # T1/T2/T3, ~10s (T2/T3 self-limit via SIGALRM)
# kernel-mode (root): cd kld_adomain && make && kldload ./adomain.ko

Expected output (baseline, unfixed)

  • T1: 2048 zero-count allocs 'succeeded'; bl_free=65536 (of 65536); 1-block alloc now -> NONE (FAILURE) β€” every alist_alloc(bl,0,0) silently "allocates" an entire 32-block leaf (mask (u32)-1 >> (32-count-…) UB β†’ 0xFFFFFFFF at count=0), wiping the whole 256MB-DMA-reserve-equivalent bookkeeping while bl_free still claims full.
  • T2: HANG REPRODUCED: still spinning after 5s β€” alst_leaf_alloc's for (j = 0; j <= n; j += count) never increments (j += 0). In-kernel this spins with the caller's lock held (vm_contig_spin for vm_contig_alist) β†’ permanent system hang.
  • T3: HANG REPRODUCED β€” alist_create(3000000000): radix *= 16 wraps past alist_blk_t (u32) to 0 β†’ infinite loop in the radix computation.
  • KLD on stock INVARIANTS kernel: panic: assertion "count" failed in alist_alloc at /usr/src/sys/kern/subr_alist.c:250 (see panic.txt) β€” proving the only guard is INVARIANTS-only.

Fixed behavior (see VERDICT.md)

count==0 β†’ ALIST_BLOCK_NONE (T1: 0 wipes, alloc still works; T2/T3 return instantly, no hang); blocks > 2^29 β†’ alist_create returns NULL / alist_init panics with an explicit message; count == bl_radix allocatable on leaf-root alists.

VERDICT.md
↓ download raw

DF-2797 β€” VERDICT

Status: reproduced (kernel panic on stock INVARIANTS guest + production-kernel arithmetic demonstrated standalone). Impact: panic on the audit guest (INVARIANTS); on production (non-INVARIANTS) kernels: silent capacity theft / permanent system hang (see below). No memory corruption, no privilege escalation β€” this is a domain-validation/hardening finding (DF-2790 class, the blist sibling of the same defect family).

How reproduced

1. Kernel mode (stock INVARIANTS guest, fresh vm.sh reset with-src)

KLD kld_adomain/adomain.ko creates its own alist via alist_create(65536, M_TEMP) (legitimate allocator unit-testing pattern; the live vm_contig_alist is never touched), frees it all, then calls alist_alloc(bl, 0, 0):

adomain: creating own alist(65536)
adomain: bl_free=65536, calling alist_alloc(bl,0,0) -- expect KKASSERT(count) panic
panic: assertion "count" failed in alist_alloc at /usr/src/sys/kern/subr_alist.c:250
cpuid = 2
Trace: alist_alloc+0x129 <- adomain_modevent+0x92 <- module_register_init ...

Full capture: panic.txt. This proves the count==0 guard is INVARIANTS-only β€” there is no release-kernel validation whatsoever.

2. Production-kernel arithmetic (standalone, -DNDEBUG, identical code)

run.log (decisive lines):

  • T1 silent leaf-wipe β€” alist_alloc(bl, 0, 0) on a fully-free leaf: KKASSERT(count) gone; the power-of-2 test (count|(count-1)) != (count<<1)-1 evaluates 0|0xFFFFFFFF == 0xFFFFFFFF β†’ TRUE-equal β†’ count=0 treated as a power of 2; alst_leaf_alloc computes n = 32-0 = 32, mask = (u32)-1 >> 32 (C UB; x86 shrl %cl folds to >> 0 β†’ 0xFFFFFFFF), the j-loop matches the fully-free leaf, "allocates 0 blocks" by clearing all 32 bits, and returns blk. bl_free -= 0. 2048 calls wipe the entire 65536-block alist while bl_free still reports 65536 free; a subsequent 1-block allocation returns NONE. For the vm_contig consumer this silently strands the whole 256MB DMA reserve (bookkeeping-full, bookkeeping-lying).
  • T2 infinite loop β€” count=0 on a partially-free leaf: for (j = 0; j <= n; j += count) with j += 0 never advances; verified spinning >5s (SIGALRM kill, exit 42). In-kernel the caller holds vm_contig_spin (vm_page.c:2808/2875) β†’ every later vm_page_alloc_contig/vm_page_free_contig on any CPU spins forever β†’ permanent system hang (impact class: local DoS given any caller passing 0).
  • T3 radix wrap β€” alist_create(3000000000): radix *= ALIST_META_RADIX overflows alist_blk_t (u32) β†’ 0 β†’ while (radix < blocks) never exits (verified >5s spin). Same loop in alist_init would hang at boot. The header advertises "2 billion blocks" but the arithmetic caps at 2^29 (32*16^6); every blocks value in (2^29, 2^32) wraps.
  • Off-by-one (legal-input availability bug, same family) β€” top-level guard count < bl->bl_radix (:264) rejects count == bl_radix, so a root-leaf alist (blocks ≀ 32) can never allocate its full capacity even when completely free (alist_free(bl, 0, 32) works β€” asymmetric). Found by the differential fuzzer (STRICT failure at geometry 32; see fuzz log in this pack's discovery history / task_log). vm_contig (65536 blocks) is unaffected; tiny alists only.

Why this is not higher severity

The sole in-kernel consumer is vm_contig_alist (vm_page.c:435) and both parameters reaching alist_alloc are kernel-computed page counts (vm_page.c:815, 2809); no in-tree caller passes 0 (verified by grep over vm_page_alloc_contig/kmem_alloc_contig/contigmalloc call sites). The alist is also exported KLD API (sys/alist.h) β€” out-of-tree kernel code gets none of the INVARIANTS protection on production kernels.

Exploit chain

None (hardening). The strongest primitive is the T2 spin-with-lock-held: any future/out-of-tree caller passing a rounded-to-0 size converts a driver bug into a permanent whole-system hang.

Fix validation

fix.diff applied to the guest's /usr/src, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC), A/B rerun β€” see fix_validation.log in this pack: T1 wipes drop to 0 and the 1-block alloc succeeds; T2/T3 return instantly (no hang); the KLD now prints SURVIVED r=ffffffff (clean ALIST_BLOCK_NONE) instead of panicking; the production-emulation standalone shows identical results.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, kernel rebuilt (make -j8 nativekernel KERNCONF=X86_64_GENERIC) and installed; on the fixed kernel the KLD loads/unloads cleanly with r=ffffffff (no panic) and the standalone production-emulation shows 0 leaf wipes, no T2/T3 hangs, create returns NULL for 3e9 - all baseline bad behaviors gone.

fix_run.log (kernel + standalone A/B), fix_build.log (build/install/new uname), fix_validation.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 12:02:52 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt (stock-kernel panic at :250 with KLD trace); run.log T1 line '2048 zero-count allocs succeeded; bl_free=65536; 1-block alloc now -> NONE'; T2/T3 '[SIGALRM] HANG REPRODUCED' exit=42; fix_run.log (fixed kernel: SURVIVED r=ffffffff, 0 wipes, no hangs); fuzz_seeds.txt (negative-result scope)

PoC changes

Seed sketch had none; authored fresh: standalone trigger compiles the real sys/kern/subr_alist.c with -DNDEBUG to emulate a production kernel (KKASSERT no-op, panic() real) and forks per hang-case with SIGALRM self-limiting; KLD variant for the INVARIANTS guest using alist_create(M_TEMP) on a private alist (legitimate allocator unit-test pattern).

Verified recommended fix

alist_alloc: reject count==0 explicitly; alist_create/alist_init: validate blocks (0 < blocks <= 2^29); alist_alloc: allow count == bl_radix (see fix.diff)

Verdict

Reproduced on the stock INVARIANTS guest via a KLD calling alist_alloc(own_alist, 0, 0): panic 'assertion "count" failed in alist_alloc at subr_alist.c:250' (panic.txt), proving the count==0 guard is INVARIANTS-only. The production-kernel (non-INVARIANTS) arithmetic was demonstrated with the identical source compiled -DNDEBUG standalone: count==0 passes the power-of-2 test (0|0xFFFFFFFF == 0xFFFFFFFF), then (T1) on a fully-free leaf silently clears all 32 bitmap bits per call - 2048 calls wipe the entire 65536-block alist (the vm_contig DMA-reserve geometry) while bl_free still reports 65536 and a 1-block allocation then fails; (T2) on a partially-free leaf the j+=count loop never advances - infinite spin, which in-kernel holds vm_contig_spin and hangs the system permanently; (T3) alist_create(3000000000) wraps radix (u32 *=16 -> 0) into an infinite loop (the alist.h contract '2 billion blocks' is really 2^29). Also found by the differential fuzzer: the top-level guard count < bl->bl_radix (:264) makes count==bl_radix unallocatable on leaf-root (<=32-block) alists - a legal-input availability off-by-one. No in-tree caller passes 0 or oversized counts (verified by grep over vm_page_alloc_contig/kmem_alloc_contig/contigmalloc call sites), so severity is hardening/Low; ~5.1M-op legal-sequence differential fuzz (fuzz_seeds.txt) proved the legal state machine sound and confined all divergence to known DF-0052. Fix validated on a rebuilt guest kernel (#1 Sep 1 12:02:52): count==0 returns ALIST_BLOCK_NONE (KLD: 'SURVIVED r=ffffffff', clean load/unload), create rejects oversized counts, no hangs; standalone production-emulation A/B identical (fix_run.log).