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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2797 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)β everyalist_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 whilebl_freestill claims full. - T2:
HANG REPRODUCED: still spinning after 5sβalst_leaf_alloc'sfor (j = 0; j <= n; j += count)never increments (j += 0). In-kernel this spins with the caller's lock held (vm_contig_spinforvm_contig_alist) β permanent system hang. - T3:
HANG REPRODUCEDβalist_create(3000000000):radix *= 16wraps pastalist_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(seepanic.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.
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)-1evaluates0|0xFFFFFFFF == 0xFFFFFFFFβ TRUE-equal β count=0 treated as a power of 2;alst_leaf_alloccomputesn = 32-0 = 32,mask = (u32)-1 >> 32(C UB; x86shrl %clfolds to>> 0β 0xFFFFFFFF), thej-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 whilebl_freestill 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)withj += 0never advances; verified spinning >5s (SIGALRM kill, exit 42). In-kernel the caller holdsvm_contig_spin(vm_page.c:2808/2875) β every latervm_page_alloc_contig/vm_page_free_contigon 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_RADIXoverflowsalist_blk_t(u32) β 0 βwhile (radix < blocks)never exits (verified >5s spin). Same loop inalist_initwould 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) rejectscount == 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
fixedfix.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
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).
No comments yet.