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

Integer overflow in sbuf_extend/sbuf_extendsize yields undersized reallocation and ~2GiB heap overflow

Summary

sbuf_extend computes new buffer size as sbuf_extendsize(s->s_size+addlen) where sum narrowing-converted to int. If s->s_size+addlen>INT_MAX truncated argument falls in negative range sbuf_extendsize takes size<4096 branch returns minimum 16 bytes KASSERT does NOT catch (16>=INT_MIN). sbuf_extend kmallocs 16 bytes then memcpy old s_size ~2GiB into it catastrophic heap overflow. Corrupted s_size(now 16) plus still-huge s_len turns every further sbuf_put_byte into wild write at offset ~2GiB. sbuf_bcopyin/sbuf_copyin pass attacker-influenced len-SBUF_FREESPACE as addlen. Today sbuf_bcopyin/sbuf_copyin/sbuf_uionew have ZERO in-tree callers latent until wired.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2552 Β· 15 files
FileTypeDescriptionSize
arith_proof.c trigger-source userspace arithmetic proof replicating exact sbuf_extendsize/sbuf_extend logic 6.8 KB view raw
trigger.c trigger-source userspace trigger for the kernel-module harness 1.2 KB view raw
sbuftest_mod/sbuftest.c exploit-chain kernel module that calls sbuf_bcopyin with crafted length to trigger int-truncation heap overflow 3.7 KB view raw
sbuftest_mod/Makefile build-script kld module Makefile 115 B ↓ download
build.sh build-script builds arith_proof and trigger 367 B view raw
run.sh run-script runs the arithmetic proof 186 B view raw
fix.diff suggested-fix overflow guard in sbuf_extend: reject s_size+addlen > INT_MAX 629 B view raw
build.log build-log arithmetic proof + trigger build output 211 B view raw
run.log run-log arithmetic proof run output showing int-truncation 2.1 KB view raw
module_baseline.log run-log kernel-module harness baseline output (unpatched #0): post-overflow s_size=16 454 B view raw
fix_run.log run-log kernel-module harness output on patched #1 kernel: post-overflow s_size=4096 (no overflow) 375 B view raw
env.txt environment uname, cc version, sysctl state 311 B view raw
VERDICT.md verdict full analysis: mechanism, reachability, fix validation 6.6 KB ↓ raw
README.md readme how to reproduce 2.3 KB ↓ raw
manifest.json manifest this file 2.6 KB view raw
README.md readme how to reproduce
↓ download raw

DF-2552 β€” sbuf_extend / sbuf_extendsize int-truncation heap overflow

Summary

sbuf_extend() in sys/kern/subr_sbuf.c computes the new buffer size via sbuf_extendsize(s->s_size + addlen). The sum (ssize_t + int) is narrowed to int (the parameter type of sbuf_extendsize). When the sum exceeds INT_MAX, the narrowed argument is negative, sbuf_extendsize returns 16, and sbuf_extend's memcpy overflows the 16-byte allocation by s_size - 16 bytes.

Status: The code defect is real and confirmed at the harness level. The bug is latent β€” sbuf_bcopyin/sbuf_copyin/sbuf_uionew (the only functions that pass large addlen to sbuf_extend) have zero in-tree callers, so it is not reachable from unprivileged userspace on the current kernel. A fix.diff is provided as defense-in-depth and validated on a single-fix kernel.

Files

  • arith_proof.c β€” userspace arithmetic proof (replicates exact sbuf arithmetic)
  • trigger.c β€” userspace trigger for the kernel-module harness
  • sbuftest_mod/sbuftest.c β€” kernel module that creates /dev/sbuftest and calls sbuf_bcopyin with crafted length
  • sbuftest_mod/Makefile β€” kld module Makefile
  • build.sh β€” builds arith_proof and trigger
  • run.sh β€” runs arith_proof (non-destructive)
  • fix.diff β€” git-apply-able fix (overflow guard in sbuf_extend)
  • VERDICT.md β€” full analysis
  • manifest.json β€” artifact catalog

Build

./build.sh    # builds arith_proof and trigger (userspace, as unprivileged user)

Run

./run.sh      # runs the arithmetic proof (non-destructive)

Expected output (arithmetic proof)

The proof shows: - Normal case (sbuf_put_byte path, addlen=1): correct, no overflow. - Bug case (sbuf_bcopyin path, crafted len): sbuf_extendsize returns 16 for a 4096-byte buffer β†’ 4080-byte overflow. - Fixed version: detects overflow, returns ENOMEM.

Kernel-module harness (requires root to load)

# On the guest (as root):
cd /root/sbuftest_mod && make obj && make
cp /usr/obj/root/sbuftest_mod/sbuftest.ko /root/sbuftest.ko
kldload /root/sbuftest.ko

# As unprivileged user:
cd ~/poc/DF-2552 && ./trigger

# Check result (as root):
dmesg | grep SBUFTEST
# Unpatched: "post-overflow s_size=16" (overflow occurred)
# Patched:   "post-overflow s_size=4096" (extend refused, no overflow)
VERDICT.md verdict full analysis: mechanism, reachability, fix validation
↓ download raw

DF-2552 β€” sbuf_extend / sbuf_extendsize int-truncation heap overflow

Verdict: REPRODUCED (primitive confirmed at code + harness level); LATENT β€” not reachable from unprivileged userspace on the current kernel.

Severity: Medium (latent code defect; defense-in-depth fix applied)

The bug (confirmed)

sys/kern/subr_sbuf.c:158 β€” sbuf_extend() computes the new buffer size as:

newsize = sbuf_extendsize(s->s_size + addlen);
  • s->s_size is ssize_t (64-bit on x86_64).
  • addlen is int (32-bit).
  • The sum s->s_size + addlen is computed as ssize_t (no 64-bit overflow for realistic values).
  • But sbuf_extendsize() takes int size (subr_sbuf.c:132), so the ssize_t sum is narrowed to int when passed.

If s->s_size + addlen > INT_MAX (0x7FFFFFFF), the narrowing produces a negative int. sbuf_extendsize() then: 1. Takes the size < SBUF_MAXEXTENDSIZE (4096) branch (true for any negative int). 2. Returns SBUF_MINEXTENDSIZE = 16. 3. The KASSERT(newsize >= size) at line 143 passes (16 >= negative).

Back in sbuf_extend():

newbuf = SBMALLOC(newsize);         // kmalloc(16, M_SBUF, M_WAITOK|M_ZERO)
memcpy(newbuf, s->s_buf, s->s_size); // memcpy(16-byte-buf, old-buf, s_size=4096) β†’ 4080-byte overflow!

The memcpy writes s->s_size bytes (e.g. 4096) into the 16-byte allocation, producing a heap overflow of s->s_size - 16 bytes.

Arithmetic proof (run on guest as unprivileged user)

arith_proof.c replicates the exact sbuf_extendsize/sbuf_extend arithmetic. Key result for the trigger scenario:

s_size=4096, addlen=2147483647 (INT_MAX)
s_size + addlen = 2147487743  (as ssize_t)
(int)(sum)      = -2147479553  (narrowed β€” what sbuf_extendsize receives)
BUGGY newsize   = 16
*** HEAP OVERFLOW: kmalloc(16) then memcpy(4096 bytes) => 4080-byte overflow! ***

Kernel-module harness (proves the actual primitive in the running kernel)

Since sbuf_extend/sbuf_extendsize are static, the only way to drive a large addlen into sbuf_extend from the exported API is through sbuf_bcopyin() (or sbuf_copyin()), which computes addlen = (int)(len - SBUF_FREESPACE(s)).

The harness module (sbuftest_mod/) creates /dev/sbuftest (mode 0666). Its ioctl handler: 1. Creates an sbuf via sbuf_new(NULL, NULL, 4096, SBUF_AUTOEXTEND) β†’ s_size=4096. 2. Calls sbuf_bcopyin(sb, &dummy, 2147487742) β†’ addlen = (int)(2147487742 - 4095) = INT_MAX. 3. Inside sbuf_extend: s_size + addlen = 4096 + INT_MAX > INT_MAX β†’ narrowed to negative β†’ sbuf_extendsize returns 16 β†’ kmalloc(16) β†’ memcpy(4096) β†’ 4080-byte heap overflow.

Loading the module requires root (kldload). This is a HARNESS that proves the primitive exists in the actual kernel code — NOT a valid unprivileged→root escalation chain (see bright-line rule). The trigger (ioctl) is unprivileged, but the module setup is root-only.

Baseline (#0 unpatched kernel) β€” module output (dmesg):

SBUFTEST: sbuf created, s_size=4096, s_len=0, freespace=4095
SBUFTEST: trigger_len=2147487742, addlen will be (int)2147483647 = 2147483647
SBUFTEST: calling sbuf_bcopyin -> about to overflow kernel heap!
SBUFTEST: SURVIVED (unexpected β€” heap is corrupted, expect panic soon)
SBUFTEST: post-overflow s_size=16 (should be 16)

post-overflow s_size=16 is the smoking gun: sbuf_extend allocated 16 bytes and copied 4096 bytes into it. The kernel survived because the old buffer was zeroed (M_ZERO), so the overflow wrote zeros into adjacent slab chunks β€” a silent heap corruption, even more dangerous than a panic.

Reachability analysis (why this is LATENT)

sbuf_bcopyin(), sbuf_copyin(), and sbuf_uionew() are the only functions that pass a user-influenced addlen to sbuf_extend(). Verified via rg -n 'sbuf_bcopyin|sbuf_copyin|sbuf_uionew' sys/ (excluding subr_sbuf.c and sbuf.h):

ZERO in-tree callers. These functions are exported (T in nm /boot/kernel/kernel) but never called by any kernel code.

All live sbuf growth in the kernel happens one byte at a time: - sbuf_put_byte() β†’ sbuf_extend(s, 1) β€” addlen is always the constant 1. - sbuf_bcat(), sbuf_cat(), sbuf_printf() all route through sbuf_put_byte().

For the sbuf_extend(s, 1) path to trigger the bug, s_size would need to already be at or above INT_MAX - 1 (~2 GiB). Reaching that via byte-at-a-time growth requires writing ~2 GiB of data into an sbuf and ~28 doublings of the allocation β€” an enormous kernel heap allocation that would fail (OOM) long before approaching INT_MAX. No kernel path does this.

Conclusion: The int-truncation defect is real at the code level and confirmed by the harness, but it is not reachable from unprivileged userspace on the current kernel. It is a latent code defect β€” a future caller that wires sbuf_bcopyin/sbuf_copyin into a syscall/ioctl path would instantly turn it into an exploitable heap overflow.

Fix

fix.diff adds an overflow guard in sbuf_extend() before the sbuf_extendsize() call:

if (addlen < 0 || s->s_size > (ssize_t)0x7fffffff - addlen)
    return (-1);

This rejects any extend request where s->s_size + addlen would exceed INT_MAX, preventing the narrowing-to-negative and the resulting undersized allocation. The caller (sbuf_bcopyin/sbuf_copyin) already handles a failed extend gracefully (clamps the write to SBUF_FREESPACE); sbuf_put_byte sets s->s_error = ENOMEM.

Fix validation (single-fix kernel)

Built X86_64_GENERIC with only this diff applied (#1, Sat Aug 8 21:04:27 UTC 2026).

Kernel Post-overflow s_size Overflow?
#0 unpatched 16 YES β€” kmalloc(16), memcpy(4096), 4080-byte overflow
#1 patched 4096 (unchanged) NO β€” sbuf_extend returns -1, no realloc

The fix is validated: on the patched kernel, sbuf_extend correctly detects the overflow and returns -1, leaving s_size at 4096. No heap overflow occurs.

How to reproduce

# Build the arithmetic proof + trigger
cd findings/poc/DF-2552
./build.sh          # builds arith_proof and trigger

# Run the arithmetic proof (non-destructive, runs as unprivileged user)
./run.sh            # shows the int-truncation arithmetic

# Kernel-module harness (requires root to load):
# On the guest:
cd /root/sbuftest_mod && make obj && make
cp /usr/obj/root/sbuftest_mod/sbuftest.ko /root/sbuftest.ko
kldload /root/sbuftest.ko
# As unprivileged user:
./trigger           # triggers sbuf_bcopyin with crafted length
# Check dmesg for "post-overflow s_size=16" (unpatched) or "s_size=4096" (patched)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: kernel-module harness triggers 4080-byte heap overflow on unpatched #0 kernel (post-overflow s_size=16) and does NOT on single-fix #1 kernel (post-overflow s_size=4096 β€” sbuf_extend correctly returns -1, no realloc, no overflow). Fix closes the bug.

baseline (#0 unpatched): SBUFTEST: post-overflow s_size=16 β€” 4080-byte heap overflow occurred. patched (#1 fixed): SBUFTEST: post-overflow s_size=4096 β€” sbuf_extend returned -1, s_size unchanged at 4096, no overflow.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 21:04:27 UTC 2026 (single-fix)

Confirmed kernel references

Detail

Exploit chain

BLOCKED by valid hard blocker: dead/unreachable code path. Vulnerable functions sbuf_bcopyin/sbuf_copyin/sbuf_uionew have zero in-tree callers (rg -n returns nothing). All live sbuf extend paths use sbuf_extend(s,1) via sbuf_put_byte with constant addlen=1 β€” to trigger via this path s_size would need to already be at INT_MAX-1 (~2GiB), requiring ~2GiB of prior byte-at-a-time writes and ~28 allocation doublings, which no kernel path does and would OOM long before reaching. Primitive (4080-byte heap overflow) proven at harness level via root-loaded kernel module (sbuftest_mod/sbuftest.c) calling sbuf_bcopyin with crafted length β€” module loading requires root (kldload), primitive characterization NOT a valid unprivileged->root escalation per bright-line rule. No uid0 escalation pursued because code path is dead from userspace.

Evidence (decisive lines)

Arithmetic proof: s_size=4096, addlen=2147483647 (INT_MAX), s_size+addlen=2147487743, (int)(sum)=-2147479553, BUGGY newsize=16, HEAP OVERFLOW: kmalloc(16) then memcpy(4096) => 4080-byte overflow. Kernel module harness (unpatched #0): SBUFTEST: sbuf created, s_size=4096 | SBUFTEST: trigger_len=2147487742, addlen will be (int)2147483647 = 2147483647 | SBUFTEST: calling sbuf_bcopyin -> about to overflow kernel heap! | SBUFTEST: post-overflow s_size=16. Zero callers: rg 'sbuf_bcopyin|sbuf_copyin|sbuf_uionew' sys/ (excluding subr_sbuf.c/sbuf.h) = empty.

PoC changes

Authored all PoC files from scratch (dir empty). arith_proof.c: userspace arithmetic proof replicating exact sbuf_extendsize/sbuf_extend logic. trigger.c: userspace trigger for module. sbuftest_mod/sbuftest.c: kernel module creating /dev/sbuftest (0666) whose ioctl handler calls sbuf_bcopyin with crafted length 2147487742 to trigger the int-truncation overflow. fix.diff: overflow guard in sbuf_extend.

Verified recommended fix

Add overflow guard in sbuf_extend (subr_sbuf.c:158) before sbuf_extendsize call: if (addlen < 0 || s->s_size > (ssize_t)0x7fffffff - addlen) return (-1). Rejects any extend request where s_size+addlen exceeds INT_MAX, preventing narrowing-to-negative and undersized allocation. Supersedes finding proposal. Full git-apply-able diff in findings/poc/DF-2552/fix.diff.

Verdict

REPRODUCED (latent code defect). The int-truncation in sbuf_extendsize(int) is real: sbuf_extend (subr_sbuf.c:158) computes sbuf_extendsize(s->s_size + addlen) where the ssize_t sum is narrowed to int; when s_size+addlen > INT_MAX the narrowed argument is negative, sbuf_extendsize returns 16, and memcpy(newbuf, s->s_buf, s_size) overflows the 16-byte allocation by s_size-16 bytes (4080 bytes for s_size=4096). Confirmed by (1) arithmetic-proof PoC replicating exact kernel arithmetic, and (2) kernel-module harness calling sbuf_bcopyin with crafted length 2147487742 on running #0 kernel β€” dmesg shows post-overflow s_size=16 (smoking gun: kmalloc(16) then memcpy(4096)). HOWEVER the bug is LATENT: sbuf_bcopyin/sbuf_copyin/sbuf_uionew (the only functions that pass user-influenced addlen to sbuf_extend) have ZERO in-tree callers (verified by rg), so NOT reachable from unprivileged userspace. All live sbuf growth goes through sbuf_put_byte->sbuf_extend(s,1) with constant addlen=1 which can never trigger the overflow.