Off-by-one NUL-terminator clobber in sysctl_vm_zone name padding causes kernel-stack OOB read in unprivileged vm.zone output (13+ char zone names, in-tree trigger rfcomm_credit)
| Field | Value |
|---|---|
| ID | DF-2830 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-170 (βCWE-125) |
| File | sys/vm/vm_zone.c |
| Lines | 835-836, 841, 854 (buffer :816) |
| Area | vm |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sysctl_vm_zone pads zone names into char tmpname[14]: the clamp allows
len==13, the space-fill loop NUL-terminates at tmpname[13], then
tmpname[len]=':' overwrites that NUL for any name β₯13 chars. The
following ksnprintf("%s ...") walks past the 14-byte stack buffer until
a zero byte, copying kernel-stack residue into the sysctl string any
local user can read. Rows also lose their newline and columns get
truncated. In-tree trigger: netbt's 13-char 'rfcomm_credit' zone on
device-bluetooth kernels; KLD-created β₯13-char names trigger it too.
Threat model & preconditions
Unprivileged repeated sysctl vm.zone reads return up to ~113 bytes of
kernel-stack residue per affected zone row (demonstrated self-echoing β
the previous read's tmpbuf on the reused kernel stack β amplifying;
first touch yields whatever that stack last held). Kernel stack
disclosure channel (pointers/flags) on bluetooth/custom kernels. No
userβroot route (read primitive only).
Proof of contest
VERIFIED on the stock INVARIANTS kernel (findings/poc/DF-2830/): KLD
creates 13- and 16-char named zones β unpriv sysctl vm.zone |
hexdump -C shows >100 bytes past tmpname[13] with self-echoes and
merged rows, stable across 3 runs. Fix (clamp to sizeof-2) validated on
rebuilt kernel #1: clean rows, zero echo.
Recommended fix
Clamp the name so the ':' and NUL both fit (maintainers may alternatively widen tmpname) β validated fix.diff in the pack.
Timeline
- 2026-08-31 Discovered during pass-2 audit of vm_zone.c (GLM 5.3); stack leak reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2830 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df2830_harness.c | β | 1.5 KB | view raw | |
| build.sh | β | 367 B | view raw | |
| run.sh | β | 583 B | view raw | |
| build.log | β | 5.5 KB | view raw | |
| run.log | β | 483 B | view raw | |
| run.hexdump.log | β | 3.0 KB | view raw | |
| leak_sample.txt | β | 2.5 KB | view raw | |
| fix_validation.log | β | 725 B | view raw | |
| fix_hex.log | β | 632 B | view raw | |
| fix.diff | β | 336 B | view raw | |
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| verdict.json | β | 3.5 KB | view raw | |
| env.txt | β | 655 B | view raw |
DF-2830 β Off-by-one NUL clobber in sysctl_vm_zone name padding β kernel-stack OOB read in unprivileged sysctl vm.zone output
What
sysctl_vm_zone() pads zone names into a 14-byte stack buffer
(sys/vm/vm_zone.c:815-841):
char tmpname[14];
len = strlen(curzone->zname);
if (len >= (sizeof(tmpname) - 1)) /* len >= 13 */
len = (sizeof(tmpname) - 1); /* len = 13 */
for (i = 0; i < sizeof(tmpname) - 1; i++)
tmpname[i] = ' ';
tmpname[i] = 0; /* tmpname[13] = NUL */
memcpy(tmpname, curzone->zname, len); /* fills [0..12] */
tmpname[len] = ':'; /* len==13 β overwrites the NUL */
For any zone name of length β₯ 13 the only NUL terminator of tmpname is
replaced by ':' , and the subsequent ksnprintf("%s ...", tmpname, ...)
(vm_zone.c:854) walks past the end of the 14-byte buffer until it finds a
zero byte β copying kernel stack residue into the sysctl string that any
unprivileged local user can read (vm.zone is CTLFLAG_RD, world-readable).
In-tree trigger: netbt's rfcomm_credit zone β exactly 13 characters
(sys/netbt/rfcomm_session.c:162) on kernels built with device bluetooth
(not in stock GENERIC; present in LINT/custom BT kernels). Any KLD creating
a vm_zone with a β₯13-char name triggers it too.
Reproduce
Guest (stock X86_64_GENERIC kernel #0):
build.shβ builddf2830.ko(creates zones namedAAAAAAAAAAAAA(13) andBBBBBBBBBBBBBBBB(16)).run.shas root:kldload /root/df2830/df2830.ko, then as nobody:sysctl vm.zone | hexdump -C.
Expected (vulnerable kernel)
The affected rows contain >100 bytes of leaked stack after the name β in
practice the over-read lands on the previous read's tmpbuf content from
the reused kernel stack, so repeated reads self-echo and amplify:
TS\n\nAAAAAAAAAAAAA:AAAAAAAAAAAAA: ... (9+ echoes) ... :ASWAPMETA: ...
Rows run into each other (the row's trailing '\n' is truncated away by the
127-byte ksnprintf cap) and the stats columns themselves get corrupted.
On the patched kernel the same zones render as clean 12-char truncated
labels: AAAAAAAAAAAA: 000064, ... with no echo.
Files
df2830_harness.cβ KLD creating 13/16-char zonesbuild.sh/run.shbuild.log/run.log/run.hexdump.logβ decisive unpriv readsleak_sample.txtβ 3 successive samples (stable fixed-point echo)fix_validation.log,fix_hex.logβ patched-kernel rerun (clean)fix.diffβ git-apply-able fix (clamp to 12 chars so ':' + NUL fit)manifest.json,verdict.json
DF-2830 VERDICT β REPRODUCED (leak)
Bottom line
Classic off-by-one on a NUL terminator: for zone names of length β₯ 13,
tmpname[len] = ':' (sys/vm/vm_zone.c:841) writes over the only NUL of the
14-byte stack buffer tmpname (the clamp at vm_zone.c:835-836 allows
len == 13 == sizeof(tmpname)-1, and the space-fill/NUL loop terminates at
i == 13). ksnprintf's %s (vm_zone.c:854) then reads past the buffer,
copying kernel-stack bytes after tmpname[13] into the vm.zone sysctl
output β world-readable, confirmed readable by an unprivileged user.
How it was reproduced
Stock guest (kernel #0). A KLD created two zones with 13- and 16-character
names ("AAAAAAAAAAAAA", "BBBBBBBBBBBBBBBB"). Unprivileged
sysctl vm.zone | hexdump -C then showed, per affected row, over 100
bytes of stack residue appended to the zone label (run.hexdump.log,
leak_sample.txt):
...TS\n\nBBBBBBBBBBBBB:\nBBBBBBBBBBBBB:\n ... \nBBBBBBBBBBBB 000064,... ...AAAAAAAAAAAAA:AAAAAAAAAAAAA:AAAAAAAAAAAAA:...:ASWAPMETA:...
Mechanism (verified empirically, matches the code): the bytes immediately
following tmpname on the handler's stack frame are the previous
execution's tmpbuf (the sysctl handler reuses pooled kernel stacks), so
the over-read echoes the previous row and each successive read amplifies
the leak until ksnprintf's 127-byte truncation cap stops it β the output
then reaches a stable fixed point (identical md5 across 3 unprivileged
reads). Side effects of the same bug: rows lose their terminating newline
and merge, and the SIZE/LIMIT/USED/FREE/REQUESTS columns get truncated
(stats corruption).
The echo content is self-referential because the dominating prior use of that stack is the sysctl itself; the first read on any given kernel stack emits whatever that stack last held β an unprivileged reader can trigger reads from fresh threads at will. The over-read copies stack memory past a 14-byte boundary into userspace regardless of content (CWE-125/CWE-170).
Reachability
Requires a zone whose name is β₯ 13 chars: rfcomm_credit
(sys/netbt/rfcomm_session.c:162) on device bluetooth kernels, or any
KLD-created zone. Stock GENERIC zone names (MAP ENTRY/PV ENTRY/SWAPMETA)
are β€ 9 chars, so default-config kernels are not affected today β hence
Medium, not High.
Exploit chain
Read primitive only: unprivileged sysctl vm.zone returns up to ~113 bytes
of kernel-stack residue per affected zone row, repeatedly. No write
primitive, no escalation.
Fix validation
fix.diff clamps the name to 12 chars (sizeof(tmpname) - 2) so ':' lands
at tmpname[12] and the NUL at tmpname[13] survives. Applied together with
DF-2829's fix in the guest /usr/src, make nativekernel
KERNCONF=X86_64_GENERIC, installed, rebooted into kernel #1
(DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 20:57:06 UTC 2026).
Re-ran the exact PoC on the patched kernel with both zones loaded:
AAAAAAAAAAAA: 000064, 00000000, 000000, 000000, 00000000 β clean padded
label, no echo, no merged rows (fix_hex.log). Baseline reproduced /
patched not reproduced β fix_status=fixed.
Kernel references
- sys/vm/vm_zone.c:816 (char tmpname[14])
- sys/vm/vm_zone.c:834-841 (clamp + fill + memcpy + ':' clobber of NUL)
- sys/vm/vm_zone.c:854-858 (ksnprintf %s over-read β SYSCTL_OUT to user)
- sys/netbt/rfcomm_session.c:162 ("rfcomm_credit", 13 chars β in-tree trigger)
Fix verification
fixedApplied fix.diff (with DF-2829's fix) to guest /usr/src/sys/vm/vm_zone.c, make nativekernel KERNCONF=X86_64_GENERIC, installkernel + reboot into #1. Identical PoC rerun: 'AAAAAAAAAAAA:' / 'BBBBBBBBBBBB:' rows render cleanly, no stack echo, no merged rows, intact newlines - OOB read eliminated.
findings/poc/DF-2830/fix_validation.log and fix_hex.log (patched), run.hexdump.log + leak_sample.txt (baseline)
Confirmed kernel references
Detail
Exploit chain
unpriv user -> repeated sysctl vm.zone reads -> up to ~113 bytes of kernel-stack residue per affected zone row per read (CWE-125 read primitive via CWE-170 NUL clobber); no write primitive, no escalation
Evidence (decisive lines)
run.hexdump.log / leak_sample.txt: 'AAAAAAAAAAAAA:AAAAAAAAAAAAA:...(9+ echoes)...:ASWAPMETA:' with rows merged (no newline) from 'su -m nobody'-readable output; fix_hex.log: patched kernel prints 'AAAAAAAAAAAA: 000064, 00000000, 000000, 000000, 00000000' cleanly
PoC changes
self-authored harness (no seed): KLD creates zones named AAAAAAAAAAAAA (13) and BBBBBBBBBBBBBBBB (16); 'leakmark_zone' (13 chars, DF-2829 harness) independently exhibited the same over-read
Verified recommended fix
Clamp len to sizeof(tmpname)-2 so the ':' fits at tmpname[12] and the NUL at tmpname[13] survives (see fix.diff)
Verdict
For zone names >= 13 chars, tmpname[len]=':' (vm_zone.c:841) overwrites the only NUL of the 14-byte stack buffer tmpname (clamp at :835 allows len==13), so ksnprintf's %s (:854) reads past the buffer, copying kernel-stack residue into the world-readable vm.zone sysctl output. Reproduced on the stock INVARIANTS guest with KLD-created 13/16-char zones: unprivileged reads showed >100 bytes of stack content per affected row; the bytes after tmpname are the previous execution's tmpbuf on the reused kernel stack, so the leak self-echoes and amplifies to ksnprintf's 127-byte cap (stable fixed point, identical md5 across 3 runs); rows also lose newlines and their stats columns get truncated. In-tree trigger: 'rfcomm_credit' (exactly 13 chars, rfcomm_session.c:162) on device-bluetooth kernels; stock GENERIC zone names are <= 9 chars, hence Medium. Fixed by clamping to 12 chars: patched kernel #1 renders clean padded rows with zero echo.
No comments yet.