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

sbuf_drain() does not validate the drain callback's return value β€” a drain returning 0 yields a silent, unbounded, content-controlled linear kernel-heap overflow on production kernels

Field Value
ID DF-2845
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE CWE-20 Improper Input Validation (missing contract check)
File sys/kern/subr_sbuf.c
Lines 324-351 (KASSERT :336-337)
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

sbuf_drain() takes the drain callback's return value on faith: the only guard against a callback returning 0 ("consumed nothing, no error") or a value greater than s_len is a KASSERT, compiled out of production kernels. With a drain returning 0, sbuf_put_byte() falls through to s->s_buf[s->s_len++] = c; while SBUF_FREESPACE(s) ≀ 0 β€” it writes past the end of the heap allocation, once per byte, for as long as the caller keeps appending, and neither s_error nor the return of sbuf_cat()/sbuf_printf() ever signals a problem. With a drain returning len > s_len, s->s_len -= len goes negative and the following memmove uses a huge size_t.

Threat model & preconditions

No in-tree drain misbehaves (sbuf_sysctl_drain returns len or -error) and installing one requires root (KLD/custom kernel) β€” no unprivileged uid0 route today. This is a hardening gap whose failure mode is full kernel heap corruption for any future or third-party drain regression: content-controlled linear overflow.

Proof of contest

VERIFIED on a purpose-built no-INVARIANTS DragonFly kernel (compiled-in trigger via debug.df2845 sysctl): sbuf_cat rc=0 s_error=0 s_len=255 s_size=32 + "OVERFLOW CONFIRMED: 224 byte(s) written past the end of the 32-byte heap allocation". On the INVARIANTS kernel the same trigger panics Bad drain amount 0 for sbuf (panic.txt). fix.diff (runtime check: len==0 || len>s_len β†’ s_error=EDOOFUS) validated in-guest: patched no-INVARIANTS kernel stays in-bounds (s_len=31) and propagates rc=βˆ’1/EDOOFUS.

See findings/poc/DF-2845/fix.diff (validated).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_sbuf.c (GLM 5.3); no-INVARIANTS overflow reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2845 Β· 13 files
FileTypeDescriptionSize
df2845_test.c β€” 2.0 KB view raw
baddrain.c β€” 2.6 KB view raw
Makefile β€” 56 B ↓ download
build.sh β€” 313 B view raw
run.sh β€” 274 B view raw
build.log β€” 2.4 KB view raw
run.log β€” 12.7 KB view raw
run.2.log β€” 424 B view raw
panic.txt β€” 793 B view raw
env.txt β€” 186 B view raw
fix.diff β€” 569 B view raw
verdict.json β€” 4.7 KB view raw
manifest.json β€” 1.2 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel: identical trigger leaves s_len=31/s_size=32 (in-bounds), sets s_error=EDOOFUS(88), sbuf_cat returns -1, zero OOB bytes. Baseline (same config, unpatched) overflowed by 224 bytes silently. Bad behavior gone.

["run.2.log: 'DF2845: [4] sbuf_cat rc=-1 s_error=88 s_len=31 s_size=32 ... BLOCKED'", 'fix.diff: runtime validation replacing KASSERT at sys/kern/subr_sbuf.c:336']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Wed Sep 2 07:54:44 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/DF2845_NOINV (fix.diff applied) x86_64

Confirmed kernel references

Detail

Exploit chain

Trigger chain (root-gated): kldload (or compile in) a drain callback that returns 0 on a sbuf used for formatting -> every appended byte past s_size-1 lands beyond the heap allocation with attacker-influenced content, no error signaled. No unprivileged in-tree path to a misbehaving drain exists, so no uid0 chain; ceiling is kernel-heap corruption primitive behind a root-loaded/future-buggy drain.

Evidence (decisive lines)

["run.log: 'DF2845: [4] sbuf_cat rc=0 s_error=0 s_len=255 s_size=32' + 'OVERFLOW CONFIRMED: 224 byte(s) written past the end of the 32-byte heap allocation' (unpatched no-INVARIANTS kernel; 224 drain calls each returning 0)", "panic.txt: 'panic: Bad drain amount 0 for sbuf 0xfffff80117a49380' with stack sbuf_drain<-sbuf_put_byte<-sbuf_cat<-df2845_sysctl (unpatched INVARIANTS kernel)", "run.2.log: 'DF2845: [4] sbuf_cat rc=-1 s_error=88 s_len=31 s_size=32' + 'BLOCKED' (patched no-INVARIANTS kernel, fix.diff applied)", 'build.log: three in-guest nativekernel builds (no-INVARIANTS baseline, patched, INVARIANTS) and the KLD variant build']

PoC changes

Original sketch planned a KLD trigger; kldload of any freshly-built module crashes this snapshot's prebuilt kernel (GCC 8.3 toolchain vs Jul-2026 kernel; minimal hello-world KLD crashes identically at strcmp+0x10 in the loader before MOD_LOAD). Same test body was compiled into custom kernels via sys/conf/files + debug.df2845_trigger sysctl (df2845_test.c).

Verified recommended fix

Replace the sbuf_drain() KASSERT with a runtime check: if (len == 0 || len > s->s_len) { s->s_error = EDOOFUS; return (s->s_error); }

Verdict

sbuf_drain() (sys/kern/subr_sbuf.c:324-351) validates the drain callback's return only with a KASSERT (:336-337) that is compiled out of production kernels. Demonstrated on a purpose-built no-INVARIANTS DragonFly kernel: a drain returning 0 ('consumed nothing, no error') let sbuf_put_byte() write 224 bytes past a 32-byte FIXEDLEN sbuf allocation (s_len=255 vs s_size=32) while sbuf_cat() returned 0 with s_error=0 - a silent, unbounded, content-controlled linear kernel-heap overflow. On an INVARIANTS kernel the same trigger panics 'Bad drain amount 0 for sbuf' with stack sbuf_cat->sbuf_put_byte->sbuf_drain. No in-tree drain misbehaves (sbuf_sysctl_drain returns len or -error) and installing one requires root (KLD/custom kernel), so there is no unprivileged uid0 route today - this is a hardening gap whose failure mode is full kernel heap corruption for any future or 3rd-party drain regression. fix.diff adds a runtime check (len==0 || len>s_len -> s_error=EDOOFUS) and was validated in-guest: identical trigger on the patched no-INVARIANTS kernel stays in-bounds (s_len=31) and propagates rc=-1/EDOOFUS.