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

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), then kfree(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/wrapped size.
  • 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 GiB and then krealloc/kfree it. Most large-buffer paths use kmem_alloc/contigmalloc instead. 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 krealloc a read-OOB bcopy (info leak of neighboring kmem or page-fault panic); on kfree a vm_map_remove with a wrapped range (panic / vm_map corruption); on kmalloc_usable_size a wrong return enabling a write overflow in a caller that trusts it.
  • Required config or capabilities: x86_64 pc64 (KvaSize large enough that ks_limit permits β‰₯2 GiB allocations); a willing caller.
  • Reachability: latent; needs a β‰₯2 GiB kmalloc caller (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.

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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0021 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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).

VERDICT.md verdict full narrative analysis
↓ download raw

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 long on 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_size returns 0xFFFFFFFF80000000 β€” the sign-extended INT_MIN from the signed shift overflow. This is the line 1261 overflow, confirmed.
  • kfree returned without panicking: the wrapped size = 0xFFFFFFFF80000000 causes vm_map_remove(kernel_map, ptr, ptr + 0xFFFFFFFF80000000) where the end address wraps to a value less than start β†’ vm_map_remove silently returns (no-op) β†’ 2 GiB of KVA and physical pages leaked. Silent resource leak, not a panic.
  • The krealloc path (line 1202) was not directly exercised because the 4 GiB krealloc hits the per-type ks_limit check (:877 panic "malloc limit exceeded") before reaching the line-1202 overflow. The line-1202 overflow is confirmed by source inspection β€” the identical *kup << PAGE_SHIFT expression.

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 (uses bsd.kmod.mk) β€” the finding's build instructions used raw cc -I/sys -DKERNEL -c + ld -r, which doesn't resolve kernel includes correctly on DragonFlyBSD. The bsd.kmod.mk path handles forwarder headers.
  • Rewrote kmalloc_shift.c: the original PoC did krealloc(p, 4 GiB) which panicked at the per-type ks_limit check (:877) before reaching the signed-shift overflow. The revised PoC exercises the overflow directly via kmalloc(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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline returns 0xffffffff80000000 (overflow); patched #1 returns 0x0000000080000000 (correct). Deterministic across 2 runs.

baseline #0: BUG CONFIRMED (0xffffffff80000000). patched #1: correct (0x0000000080000000) x2 runs.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 17:25:16 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

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