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)
PoC verification
Evidence pack
findings/poc/DF-2552 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 harnesssbuftest_mod/sbuftest.cβ kernel module that creates /dev/sbuftest and calls sbuf_bcopyin with crafted lengthsbuftest_mod/Makefileβ kld module Makefilebuild.shβ builds arith_proof and triggerrun.shβ runs arith_proof (non-destructive)fix.diffβ git-apply-able fix (overflow guard in sbuf_extend)VERDICT.mdβ full analysismanifest.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)
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_sizeisssize_t(64-bit on x86_64).addlenisint(32-bit).- The sum
s->s_size + addlenis computed asssize_t(no 64-bit overflow for realistic values). - But
sbuf_extendsize()takesint size(subr_sbuf.c:132), so thessize_tsum is narrowed tointwhen 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
fixedVALIDATED: 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.
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.
No comments yet.