Signed-int overflow in oversized kmalloc size reconstruction (*kup << PAGE_SHIFT)
| Field | Value |
|---|---|
| ID | DF-0021 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:H |
| CWE | CWE-190 Integer Overflow or Wraparound |
| File | sys/kern/kern_slaballoc.c |
| Lines | 1202, 1261, 1432 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-06-29 |
| Reported | pending |
Summary
The oversized-allocation size is reconstructed in three places as
*kup << PAGE_SHIFT. btokup() returns int * (vm_page->ku_pagecnt is
int), and PAGE_SHIFT is int, so the shift is performed as a 32-bit
signed int. For any oversized allocation whose pagecount *kup >= 2^19
(i.e. any kmalloc() >= 2 GiB), the shift overflows β wrapping to
INT_MIN sign-extended at exactly 2 GiB, or to 0 at 4 GiB+. The corrupted
size then drives krealloc's bcopy length, _kfree's
kmem_slab_free/vm_map_remove range, and kmalloc_usable_size's return
value. This is a latent memory-safety defect: no unprivileged kernel
interface is currently known to issue a β₯2 GiB kmalloc, but the allocator
permits it (x86_64 ks_limit β KvaSize/10), so any future/unaudited caller
activates it.
Root cause
btokup() (sys/kern/kern_slaballoc.c:136):
#define btokup(z) (&pmap_kvtom((vm_offset_t)(z))->ku_pagecnt) /* int * */
ku_pagecnt is int (sys/vm/vm_page.h:189). The size reconstruction:
sys/kern/kern_slaballoc.c:1202(krealloc):osize = *kup << PAGE_SHIFT;βbcopy(ptr, nptr, min(size, osize));(:1207), thenkfree(ptr)(:1208) takes the oversized path which recomputes the same wrapped size at:1432.sys/kern/kern_slaballoc.c:1432(_kfree oversized):size = *kup << PAGE_SHIFT;βkmem_slab_free(ptr, size)βvm_map_remove(kernel_map, ptr, ptr+size)with a bogus/wrappedsize.sys/kern/kern_slaballoc.c:1261(kmalloc_usable_size):size = *kup << PAGE_SHIFT;β returned to callers that use it for bounds accounting.
_kmalloc stores *kup = size / PAGE_SIZE (:954), which does not truncate
for realistic KVA sizes, so the bug is purely in the readback/shift, not
the store. Threshold: any kmalloc >= 0x80000000 (2 GiB) takes the oversized
path (:942 size >= ZoneLimit), writes *kup >= 2^19, and overflows on
readback.
Threat model & preconditions
- Attacker position: no demonstrated attacker-reachable trigger.
Requires a kernel subsystem to
kmalloc() >= 2 GiBand thenkrealloc/kfreeit. Most large-buffer paths usekmem_alloc/contigmallocinstead. The defect is latent β recorded so it is tracked and the trivial fix applied before an unaudited/future caller turns it into a live vulnerability. - Privileges gained or impact (if reached): on
krealloca read-OOBbcopy(info leak of neighboring kmem or page-fault panic); onkfreeavm_map_removewith a wrapped range (panic / vm_map corruption); onkmalloc_usable_sizea wrong return enabling a write overflow in a caller that trusts it. - Required config or capabilities: x86_64 pc64 (KvaSize large enough that
ks_limitpermits β₯2 GiB allocations); a willing caller. - Reachability: latent; needs a β₯2 GiB
kmalloccaller (not known to be attacker-reachable today).
Proof of concept
PoC source: findings/poc/DF-0021/kmalloc_shift.c (kernel module; root to
kldload).
Build & run (root, disposable VM with β₯~2 GiB free KVA)
cc -I/sys -DKERNEL -c findings/poc/DF-0021/kmalloc_shift.c ld -r kmalloc_shift.o -o kmalloc_shift.ko kldload ./kmalloc_shift.ko
Expected output (bug present)
On krealloc(p, 4 GiB) the osize wraps, bcopy reads 4 GiB from a 2 GiB
object (read-OOB / page-fault panic), and the subsequent kfree feeds a
wrapped size into kmem_slab_free (vm_map_remove bogus range β panic /
vm_map corruption).
Impact
Latent. The code defect is certain and the threshold (β₯2 GiB kmalloc) is
permitted by the allocator on x86_64, so it is a real landmine for any caller
that requests such an allocation. Rated Low because no attacker-reachable
trigger is known; the one-line fix removes the risk entirely.
Recommended fix
Cast to size_t before the shift at all three sites:
--- a/sys/kern/kern_slaballoc.c
+++ b/sys/kern/kern_slaballoc.c
@@ -1202 +1202 @@
- osize = *kup << PAGE_SHIFT;
+ osize = (size_t)*kup << PAGE_SHIFT;
@@ -1261 +1261 @@
- size = *kup << PAGE_SHIFT;
+ size = (size_t)*kup << PAGE_SHIFT;
@@ -1432 +1432 @@
- size = *kup << PAGE_SHIFT;
+ size = (size_t)*kup << PAGE_SHIFT;
References
sys/kern/kern_slaballoc.c:1202,1261,1432β the overflowing shift.sys/kern/kern_slaballoc.c:136βbtokupreturnsint *.sys/vm/vm_page.h:189βint ku_pagecnt.- CWE-190 Integer Overflow or Wraparound.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/kern_slaballoc.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0021 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| kmalloc_shift.c | trigger-source | kernel module: kmalloc(2GiB) + kmalloc_usable_size + kfree to exercise the signed-shift overflow | 3.6 KB | view raw |
| Makefile | build-config | bsd.kmod.mk-based Makefile for building the kernel module | 92 B | β download |
| build.sh | repro-script | build wrapper | 186 B | view raw |
| run.sh | repro-script | run wrapper (kldload) | 535 B | view raw |
| build.log | build-log | module build output (final successful build) | 5.8 KB | view raw |
| run.log | run-log | decisive baseline run output with overflow marker | 1.6 KB | view raw |
| dmesg.txt | dmesg | kernel dmesg showing kmalloc_usable_size overflow | 930 B | view raw |
| panic.txt | panic-signature | panic from first exploratory run (ks_limit, not the shift bug itself β see notes) | 1.2 KB | view raw |
| env.txt | environment | uname, cc version, memory | 368 B | view raw |
| fix.diff | suggested-fix | (size_t)*kup << PAGE_SHIFT at 3 sites β git-apply-able | 761 B | view raw |
| fix_build.log | build-log | single-fix kernel build output (nativekernel, rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched-kernel run output showing correct kmalloc_usable_size | 973 B | view raw |
| VERDICT.md | verdict | full narrative analysis | 5.9 KB | β raw |
| README.md | readme | human reproduce doc | 1.5 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0021 β PoC
kmalloc_shift.c β kernel module demonstrating the signed-int overflow in the
oversized kmalloc size reconstruction (*kup << PAGE_SHIFT).
The bug
btokup() returns int * (vm_page->ku_pagecnt is int). Three sites
reconstruct the oversized size as *kup << PAGE_SHIFT
(kern_slaballoc.c:1202, :1432, :1261). Both operands are int, so the
shift is a 32-bit signed shift that overflows for any oversized allocation
whose pagecount *kup >= 2^19, i.e. any kmalloc() >= 2 GiB. The wrapped
value drives krealloc's bcopy length, _kfree's kmem_slab_free /
vm_map_remove range, and kmalloc_usable_size's return β OOB read / bad
kmem removal range / wrong usable-size accounting.
Latent: no unprivileged kernel interface is known to issue a β₯2 GiB
kmalloc (most large-buffer paths use kmem_alloc/contigmalloc). On x86_64
pc64 the per-type ks_limit (~KvaSize/10) permits it, so any future/unaudited
caller that requests β₯2 GiB via kmalloc activates it.
Build
cc -I/sys -DKERNEL -c findings/poc/DF-0021/kmalloc_shift.c ld -r kmalloc_shift.o -o kmalloc_shift.ko
Run (root, disposable VM with β₯~2 GiB free KVA)
kldload ./kmalloc_shift.ko
Expected output (bug present)
On krealloc(p, 4 GiB) the line-1202 osize wraps (INT_MIN sign-extended),
bcopy reads 4 GiB from a 2 GiB object β read-OOB (info leak of neighboring
kmem or page-fault panic); the subsequent kfree feeds a wrapped size into
kmem_slab_free β vm_map_remove bogus range (panic / vm_map corruption).
DF-0021 β VERDICT
Verdict: REPRODUCED (integer overflow confirmed; fix VALIDATED)
The signed-int overflow in oversized kmalloc size reconstruction is a real
code defect, confirmed by source-level tracing AND runtime demonstration.
The one-line (size_t) cast fix eliminates it, validated on a built single-fix
kernel (#1) vs the unpatched baseline (#0).
The bug
btokup() (sys/kern/kern_slaballoc.c:136) returns int * (pointing at
vm_page->ku_pagecnt, which is int per sys/vm/vm_page.h:189). Three sites
reconstruct the oversized-allocation size as:
*kup << PAGE_SHIFT
Both operands are int (PAGE_SHIFT=12, sys/cpu/x86_64/include/param.h:77),
so the shift is computed as a 32-bit signed int. For any oversized
allocation whose pagecount *kup >= 2^19 (i.e. any kmalloc() >= 2 GiB),
this overflows signed int:
*kup = 0x80000(524288) β0x80000 << 12 = 0x80000000=INT_MIN- Sign-extended to
unsigned longon assignment β0xFFFFFFFF80000000
The corrupted size then drives:
| Site | Function | Effect of overflow |
|---|---|---|
:1202 |
krealloc |
osize wraps β bcopy(ptr, nptr, min(size, osize)) with wrong length β read-OOB or wrong copy |
:1261 |
kmalloc_usable_size |
returns 0xFFFFFFFF80000000 instead of the true size β caller trusting it overflows |
:1432 |
_kfree |
size wraps β kmem_slab_free(ptr, size) β vm_map_remove(kernel_map, ptr, ptr+size) with a bogus wrapped range β silent KVA/page leak (start > end β vm_map_remove is a no-op) |
The assignment targets (osize, size) are unsigned long (64-bit), but the
overflow happens in the RHS before the assignment β the cast must happen
before the shift, not after.
Reproduction
Method: kernel module (kldload, root-only). The bug is LATENT: no
unprivileged kernel interface is known to issue a >= 2 GiB kmalloc (most
large-buffer paths use kmem_alloc/contigmalloc). The module directly
exercises the overflow by calling kmalloc(2 GiB) + kmalloc_usable_size() +
kfree().
Observable (unpatched #0 kernel):
poc: kmalloc_usable_size = 0xffffffff80000000 (expect 0x0000000080000000 if OK) poc: BUG CONFIRMED (line 1261): usable_size overflowed!
kmalloc_usable_sizereturns0xFFFFFFFF80000000β the sign-extended INT_MIN from the signed shift overflow. This is the line 1261 overflow, confirmed.kfreereturned without panicking: the wrappedsize = 0xFFFFFFFF80000000causesvm_map_remove(kernel_map, ptr, ptr + 0xFFFFFFFF80000000)where the end address wraps to a value less than start βvm_map_removesilently returns (no-op) β 2 GiB of KVA and physical pages leaked. Silent resource leak, not a panic.- The
kreallocpath (line 1202) was not directly exercised because the 4 GiB krealloc hits the per-typeks_limitcheck (:877panic "malloc limit exceeded") before reaching the line-1202 overflow. The line-1202 overflow is confirmed by source inspection β the identical*kup << PAGE_SHIFTexpression.
Impact assessment
Low severity, latent. The code defect is certain (verified at runtime +
source). However:
- No unprivileged trigger is known. Reaching the bug requires a kernel
subsystem to kmalloc(>= 2 GiB) and then krealloc/kfree it. Most
large-buffer paths use kmem_alloc/contigmalloc instead of kmalloc.
- Rootβkernel only. The PoC uses kldload (root). Per the bright-line
rule, this is a rootβroot demonstration (primitive characterization), NOT an
unprivilegedβroot escalation. There is no privilege boundary to cross.
- Demonstrated effects: (a) wrong kmalloc_usable_size return β could cause
a caller that trusts it to overflow; (b) silent 2 GiB KVA/page leak per free
β repeatable DoS via KVA exhaustion; (c) potential read-OOB bcopy on the
krealloc path (source-confirmed, not runtime-exercised due to ks_limit).
No uid=0 escalation is possible β this is a valid hard blocker: the bug is
reachable only from an already-root context (kldload), so there is no
privilege boundary to cross (rootβkernel is game-over by definition). No
unprivileged path to trigger a >= 2 GiB kmalloc was found.
The fix
Cast *kup to size_t (64-bit unsigned) before the shift at all three
sites:
-osize = *kup << PAGE_SHIFT;
+osize = (size_t)*kup << PAGE_SHIFT;
This makes the shift a 64-bit unsigned operation, which cannot overflow for any
realistic *kup value (*kup max = KvaSize/PAGE_SIZE, far below 2^51).
Matches the finding markdown's ## Recommended fix proposal exactly.
Fix validation (Phase 8)
| Step | Result |
|---|---|
| Baseline (#0 unpatched) | kmalloc_usable_size = 0xffffffff80000000 (BUG) |
| Apply fix.diff (3 hunks) | All succeeded at lines 1199/1258/1429 |
| Build single-fix kernel | make -j6 nativekernel rc=0 (~2.5 min, warm obj) |
| Install + reboot β #1 | kern.version = 6.5-DEVELOPMENT #1: Sun Jul 12 17:25:16 UTC 2026 |
| Patched run #1 | kmalloc_usable_size = 0x0000000080000000 (CORRECT) |
| Patched run #2 (determinism) | kmalloc_usable_size = 0x0000000080000000 (CORRECT) |
fix_status: fixed. The overflow is eliminated; the before/after contrast is clean and deterministic.
PoC changes
- Added
Makefile(usesbsd.kmod.mk) β the finding's build instructions used rawcc -I/sys -DKERNEL -c+ld -r, which doesn't resolve kernel includes correctly on DragonFlyBSD. Thebsd.kmod.mkpath handles forwarder headers. - Rewrote
kmalloc_shift.c: the original PoC didkrealloc(p, 4 GiB)which panicked at the per-typeks_limitcheck (:877) before reaching the signed-shift overflow. The revised PoC exercises the overflow directly viakmalloc(2 GiB)+kmalloc_usable_size()(line 1261, observable marker) +kfree()(line 1432, silent leak). This produces a clean, deterministic before/after marker for fix validation.
Fix verification
fixedVALIDATED: baseline returns 0xffffffff80000000 (overflow); patched #1 returns 0x0000000080000000 (correct). Deterministic across 2 runs.
baseline #0: BUG CONFIRMED (0xffffffff80000000). patched #1: correct (0x0000000080000000) x2 runs.
Confirmed kernel references
Detail
Exploit chain
none -- LATENT integer-overflow defect with no unprivileged trigger. Requires kmalloc(>= 2 GiB), which no unprivileged kernel interface is known to issue. PoC uses kldload (root-only). Demonstrated effects: wrong usable_size, silent 2 GiB KVA leak per kfree, potential read-OOB bcopy on krealloc path.
Evidence (decisive lines)
BASELINE #0: kmalloc_usable_size=0xffffffff80000000 (BUG CONFIRMED). PATCHED #1: kmalloc_usable_size=0x0000000080000000 (correct).
PoC changes
Added Makefile (bsd.kmod.mk-based). Rewrote kmalloc_shift.c to exercise overflow via kmalloc(2GiB)+kmalloc_usable_size+kfree (original krealloc(p,4GiB) panicked at ks_limit before reaching overflow). Added build.sh, run.sh, VERDICT.md, manifest.json.
Verified recommended fix
Cast kup to size_t before the shift at all three sites: 'osize = (size_t)kup << PAGE_SHIFT;' (line 1202), 'size = (size_t)*kup << PAGE_SHIFT;' (lines 1261, 1432). Matches finding markdown's proposal exactly. Full git-apply-able diff in findings/poc/DF-0021/fix.diff.
Verdict
REPRODUCED. The signed-int overflow in oversized kmalloc size reconstruction (*kup << PAGE_SHIFT) is a real code defect confirmed at source level AND runtime: kmalloc_usable_size() returns 0xffffffff80000000 (sign-extended INT_MIN) for a 2 GiB allocation. The kfree path silently leaks 2 GiB of KVA/pages because the wrapped size causes vm_map_remove to be called with end < start (no-op).
No comments yet.