kern.ncallout fetched after the only clamp β int-overflowed callwheel sizing in swi_softclock_setup: zero-byte per-CPU allocations panic vm_map (latent 2^30-iteration OOB init loop; infinite boot loop on β€2-CPU)
| Field | Value |
|---|---|
| ID | DF-2948 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-190 / CWE-787 (latent) / CWE-835 |
| File | sys/kern/subr_param.c |
| Lines | 327-330 (sinks: kern_timeout.c:386-413) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
init_param2() clamps the COMPUTED ncallout to 560hz and only then executes TUNABLE_INT_FETCH("kern.ncallout", &ncallout) β so the direct boot tunable enters the kernel with no bounds at all. The consumer swi_softclock_setup() does all sizing in int: target = ncallout/ncpus + 16, cwheelsize doubles to the next power of two, and 'int wheel_sz = sizeof(struct wheel) * cwheelsize' β sizeof(struct wheel)=24 on x86_64, so cwheelsize=2^30 gives wheel_sz = 24*2^30 β‘ 0 (mod 2^32). VERIFIED on the 6-vCPU guest with kern.ncallout=2147483647: target= 357913966 β cwheelsize=2^30 β wheel_sz=0 β the six per-CPU zero-length callwheel allocations collide β 'panic: vm_map_entry_link: dup addr' during SI_SUB_SOFTCLOCKS, dead in DDB. Behind that map panic the sink code shows the latent outcome: the init loop runs 2^30 iterations of spin_init/TAILQ_INIT over a zero-byte allocation (~25 GB OOB write); on β€2-vCPU boxes target can be driven into (2^30, 2^31) where the doubling loop executes 1<<31 (signed overflow β negative β 0) and hangs boot forever. Distinct from DF-0174 (ncallout overflow via unbounded kern.maxfiles): the direct tunable bypasses the sum entirely. Fix: clamp the tunable after the fetch + size_t wheel_sz + target sanity cap + KKASSERT on allocation.
Timeline
- 2026-09-02 Discovered during pass-2 audit of subr_param.c (GLM 5.3); boot panic reproduced twice on stock guest (DF-2947 unused).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2948 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 3.0 KB | β raw | |
| VERDICT.md | file | 3.7 KB | β raw | |
| env.txt | file | 218 B | view raw | |
| fix.diff | file | 1.9 KB | view raw | |
| manifest.json | file | 1.3 KB | view raw | |
| panic.txt | file | 5.2 KB | view raw | |
| plant.sh | file | 2.5 KB | view raw | |
| run.sh | file | 650 B | view raw | |
| sha256.txt | file | 148 B | view raw | |
| verdict.json | file | 5.2 KB | view raw |
DF-2948 β kern.ncallout fetched after the only clamp β int-truncated callwheel sizing β boot panic / latent OOB init
sys/kern/subr_param.c:327-330 clamps the computed ncallout to
5*60*hz and only then fetches the kern.ncallout tunable β which
therefore enters the kernel with no bounds at all. The consumer
swi_softclock_setup() (sys/kern/kern_timeout.c:374-413) does all its math
in int:
target = ncallout / ncpus + 16; /* :386 */
cwheelsize = 1;
while (cwheelsize < target) cwheelsize <<= 1; /* :389-390 signed shift */
cwheelmask = cwheelsize - 1;
...
int wheel_sz; /* :398 */
wheel_sz = sizeof(*sc->callwheel) * cwheelsize; /* :406 24 * 2^30 β 0 (mod 2^32) */
sc->callwheel = kmem_alloc3(kernel_map, wheel_sz, ...); /* :407 zero-length alloc per CPU */
memset(sc->callwheel, 0, wheel_sz);
for (i = 0; i < cwheelsize; ++i) /* :410 2^30 OOB init writes */
spin_init(&sc->callwheel[i].spin, "wheel");
sizeof(struct wheel) = 8 (spinlock) + 16 (TAILQ_HEAD) = 24 on x86_64, so
wheel_sz = 24 * 2^30 = 25769803776 β‘ 0 (mod 2^32) truncates to 0.
- 6-vCPU guest (
kern.ncallout=2147483647): target = 357913966 β cwheelsize = 2^30 β wheel_sz = 0 β six per-CPU zero-lengthkmem_alloc3(kernel_map, 0)calls collide βpanic: vm_map_entry_link: dup addrduring boot (demonstrated). Had the map not panicked first, the2^30-iteration init loop wouldspin_init/TAILQ_INIT~25 GB past a zero-byte allocation β an out-of-bounds kernel write. - β€2-vCPU boxes:
targetcan be pushed into (2^30, 2^31) where the doubling loop executes1 << 31(signed overflow β negative β 0) and spins forever: infinite boot loop. - Degenerate corner (
targetwrapped negative viancallout + 16overflow at INT_MAX on 1 CPU): cwheelsize stays 1 β every callout in one bucket β pathological softclock behavior.
Distinct from DF-0174 (ncallout overflow via unbounded kern.maxfiles):
this is the direct tunable path, which no maxfiles clamp can reach, and
the sink is the cwheelsize/wheel_sz int math in kern_timeout.c.
Reproduce (host side, guest up & clean)
dfbsd-qemu/vm.sh run_root 'cp /boot/loader.conf /boot/loader.conf.df2948bak && printf "kern.ncallout=\"2147483647\"\n" >> /boot/loader.conf && sync'
dfbsd-qemu/vm.sh run_root 'shutdown -r now'
sleep 80; dfbsd-qemu/vm.sh status # -> down
grep -n "panic" dfbsd-qemu/boot.log
dfbsd-qemu/vm.sh reset with-src
Expected output
Guest never reaches ssh; serial console (panic.txt) shows, right after "Initialize MI interrupts for 6 cpus":
panic: vm_map_entry_link: dup addr map 0xffffffff8159aea0 ent 0xffffffff81709b18
cpuid = 0
Trace beginning at frame 0xffffffff817a9e30 ...
Debugger("panic")
CPU0 stopping CPUs: 0x0000003e
Stopped at -0x7f433c74: movb $0,0xbd77f9(%rip)
db>
Impact: unbootable system. Trust boundary: /boot/loader.conf (root/loader prompt), same class as DF-0173/DF-0174.
DF-2948 β VERDICT
Status: reproduced (impact: panic β boot-time kernel panic via integer truncation in callwheel sizing; pristine single-variable run)
What was run
Stock guest, kernel DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC,
6 vCPU, 4 GB). Single change: one line appended to /boot/loader.conf:
kern.ncallout="2147483647"
then shutdown -r now.
What happened (vs. what was predicted)
Predicted chain: TUNABLE_INT_FETCH("kern.ncallout", &ncallout) at
subr_param.c:330 runs after the only clamp (5*60*hz, lines 328-329), so
INT_MAX enters untouched β target = ncallout/ncpus + 16 = 357913966 β
cwheelsize doubles to 2^30 β int wheel_sz = 24 * 2^30 truncates to 0
(mod 2^32) β six per-CPU kmem_alloc3(kernel_map, 0).
Observed (panic.txt): boot dies right after "Initialize MI interrupts for 6 cpus" β the SI_SUB_SOFTCLOCKS phase β with:
panic: vm_map_entry_link: dup addr map 0xffffffff8159aea0 ent 0xffffffff81709b18
Trace beginning at frame 0xffffffff817a9e30
Debugger("panic")
Stopped at -0x7f433c74: movb $0,0xbd77f9(%rip)
db>
i.e. the second per-CPU zero-length callwheel allocation collided with the
first in kernel_map and the map code panicked. The guest sat in DDB; ssh
never came up; vm.sh status β down.
The panic fired before the predicted OOB stomp (for (i = 0; i <
cwheelsize; ++i) spin_init(&sc->callwheel[i].spin, ...) β 2^30 Γ 24 bytes
β 25 GB of init writes past a zero-byte allocation, kern_timeout.c:410-413),
so the demonstrated impact is boot DoS, while the code path clearly shows
the latent memory-corruption consequence whenever the zero-length
allocation does not trip the map assertion first (e.g. differing vm_map
behavior, or values of ncallout that yield a small positive truncated
wheel_sz with a huge cwheelsize, such as ncallout giving cwheelsize = 2^30
with sizeof 24 β 0; ncpus=1..2 with target in (2^30, 2^31) instead
overflows the doubling loop 1 << 31 into an infinite boot loop).
Two runs performed: (1) with a stale kern.nbuf="-1048576" line still
present β same panic (and that line had been separately proven harmless,
see DF-2946 VERDICT.md negative results); (2) pristine single-variable
re-run after vm.sh reset β identical panic, identical addresses.
Why it is a finding and not a duplicate of DF-0174
DF-0174 covers ncallout = 16 + maxproc + maxfiles overflow via
unbounded kern.maxfiles. This finding is the direct tunable input:
kern.ncallout is fetched after the only clamp in the file
(subr_param.c:330 vs 328-329), so it bypasses every bound regardless of
maxfiles, and the defective sink is the int target / int cwheelsize /
int wheel_sz arithmetic in swi_softclock_setup (kern_timeout.c:386-407)
β wheel_sz truncation to 0 and the signed-shift overflow are not
reachable through the maxfiles path on multi-CPU boxes (maxproc is capped
at limsize*40 β 327k and even INT_MAX maxfiles only wraps the sum,
which the 5*60*hz clamp then re-bounds when reached through line 327).
Fixing DF-0174 by clamping maxfiles would leave kern.ncallout fully
unclamped.
Trust boundary / severity
Boot tunable β requires root/loader-prompt/console. No runtime sysctl exists for ncallout (kern_mib.c has none; it is boot-only). Ceiling: persistent unbootable system. Rated Low (privileged boot DoS), consistent with DF-0173/DF-0174 ratings.
Fix validation
Not performed in-guest (Low boot-DoS class; the demonstrated manifestation
is a panic, not corruption β the corruption path is latent behind the
vm_map panic). fix.diff clamps kern.ncallout to the same 5*60*hz
bound after the fetch (subr_param.c) and fixes the int math in
swi_softclock_setup (size_t wheel sizing with overflow guard).
Fix verification
not_testableFix not built in-guest: demonstrated manifestation is a boot panic (DoS), not memory corruption β the OOB-init-loop outcome is latent behind the vm_map_entry_link panic, so per contract the kernel-rebuild validation cycle is reserved for findings with demonstrated corruption; fix.diff is a minimal line-accurate clamp + sink hardening authored against the read-only sys/ tree.
['fix.diff']
Confirmed kernel references
Detail
Exploit chain
1) privileged/console plant: kern.ncallout="2147483647" in /boot/loader.conf; 2) init_param2 fetches it AFTER the 560hz clamp -> unbounded int ncallout; 3) swi_softclock_setup: target=ncallout/ncpus+16, cwheelsize doubles to 2^30, int wheel_sz truncates 24*2^30 to 0; 4) per-CPU kmem_alloc3(kernel_map, 0) -> second allocation collides -> panic vm_map_entry_link: dup addr; machine will not boot until loader.conf repaired. Latent variants: zero/small truncated wheel_sz with intact 2^30 init loop -> massive kernel OOB write; 1-2 vCPU -> 1<<31 signed-shift overflow -> infinite boot loop. No post-boot or unprivileged trigger (kern.ncallout has no runtime sysctl).
Evidence (decisive lines)
['panic.txt β \'panic: vm_map_entry_link: dup addr map 0xffffffff8159aea0 ent 0xffffffff81709b18\' + trace + Debugger("panic") + db> right after \'Initialize MI interrupts for 6 cpus\' (SI_SUB_SOFTCLOCKS)', 'env.txt β stock kernel identity (6.5-DEVELOPMENT #0, X86_64_GENERIC, hw.ncpu=6)', 'run.sh β exact host-side sequence (pristine single-variable plant after a clean reset)', 'VERDICT.md β arithmetic derivation (24*2^30 mod 2^32 == 0), two runs (one with stale harmless kern.nbuf line, one pristine, identical panic), duplicate-separation vs DF-0174', 'fix.diff β clamp kern.ncallout after the fetch in init_param2 + size_t wheel sizing with sanity cap and KKASSERT in swi_softclock_setup']
PoC changes
no compilation needed: trigger is a single loader.conf line; value 2147483647 (INT_MAX) chosen so target=INT_MAX/6+16 lands below 2^31 (avoiding only the unreachable-on-6-cpu infinite-loop variant) while forcing cwheelsize=2^30 and the wheel_sz int truncation to exactly 0
Verified recommended fix
Move the 560hz (and a lower-bound) clamp to AFTER the TUNABLE_INT_FETCH("kern.ncallout") in init_param2; harden swi_softclock_setup with a target sanity cap and size_t wheel_sz.
Verdict
init_param2() clamps the computed ncallout to 560hz (subr_param.c:328-329) and only THEN fetches the kern.ncallout tunable (line 330), so the tunable enters the kernel with no bounds at all. The consumer swi_softclock_setup() (kern_timeout.c:374-413) does all sizing in int: with kern.ncallout=2147483647 on the 6-vCPU guest, target=357913966 doubles cwheelsize to 2^30 and 'int wheel_sz = sizeof(struct wheel)cwheelsize' = 242^30 = 25769803776 truncates to 0 (mod 2^32); the six per-CPU kmem_alloc3(kernel_map, 0) zero-length callwheel allocations collide and the kernel panics 'vm_map_entry_link: dup addr' during SI_SUB_SOFTCLOCKS, dead in DDB, ssh never up β unbootable until loader.conf is repaired. Behind that map panic lies the latent outcome spelled out in the sink code: a 2^30-iteration spin_init/TAILQ_INIT loop writing ~25 GB past a zero-byte allocation (kern_timeout.c:410-413), and on <=2-vCPU boxes target can be driven into (2^30,2^31) where the cwheelsize doubling loop itself executes 1<<31 (signed overflow -> negative -> 0) and hangs boot forever. Distinct from DF-0174 (ncallout overflow via unbounded kern.maxfiles): the direct tunable bypasses the sum entirely, and no maxfiles clamp can bound it.
No comments yet.