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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2830 Β· 14 files
FileTypeDescriptionSize
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):

  1. build.sh β€” build df2830.ko (creates zones named AAAAAAAAAAAAA (13) and BBBBBBBBBBBBBBBB (16)).
  2. run.sh as 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 zones
  • build.sh / run.sh
  • build.log / run.log / run.hexdump.log β€” decisive unpriv reads
  • leak_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
VERDICT.md
↓ download raw

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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 20:57:06 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.