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

Uninitialized st_padding1 leaked to userspace via every stat syscall

Field Value
ID DF-0034
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE CWE-908 Use of Uninitialized Resource
File sys/kern/vfs_vnops.c
Lines 852-853 (vn_stat)
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

vn_stat() zeros the spare stat fields st_lspare and st_qspare2 (:852-853) but never initializes the explicitly-declared __uint16_t st_padding1 (sys/sys/stat.h:105). All five stat syscall handlers declare struct stat on the kernel stack without zeroing it and copyout() the whole struct, leaking 2 bytes of uninitialized kernel stack to unprivileged userspace on every fstat/stat/lstat/fstatat/fhstat.

Root cause

sys/kern/vfs_vnops.c:852-853:

/* Zero the spare stat fields */
sb->st_lspare = 0;
sb->st_qspare2 = 0;
/* sb->st_padding1 (stat.h:105) is never written */

Callers pass a stack-local, unzeroed struct stat and copyout sizeof(struct stat): sys_fstat (kern_descrip.c:1574,1580), sys_stat (vfs_syscalls.c:3071,3078), sys_lstat (:3093,3100), sys_fstatat (:3115,3130), sys_fhstat (:5004,5030).

Threat model & preconditions

  • Attacker position: any local unprivileged user.
  • Privileges gained or impact: information disclosure β€” 2 bytes of uninitialized kernel stack per call (pointer fragments, prior-syscall residue), samplable in a tight loop. Low-grade KASLR-assist / info-scrape.
  • Required config or capabilities: none; default kernel.
  • Reachability: any stat(2) family call on any vnode.

Proof of concept

PoC source: findings/poc/DF-0034/leak_stpad.c

Build & run (unprivileged)

cc -o leak_stpad findings/poc/DF-0034/leak_stpad.c
./leak_stpad

Expected output

Non-zero, varying byte pairs at offsetof(struct stat, st_padding1).

Impact

Low β€” 2-byte kernel-stack info leak, samplable in a loop. Same class as DF-0007/DF-0010/DF-0027.

Zero st_padding1 in vn_stat (and defense-in-depth: bzero the struct stat in each stat syscall handler before vn_stat/fo_stat):

--- a/sys/kern/vfs_vnops.c
+++ b/sys/kern/vfs_vnops.c
@@ -851,6 +851,7 @@
     */
+   sb->st_padding1 = 0;
    sb->st_lspare = 0;
    sb->st_qspare2 = 0;

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/vfs_vnops.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0034 Β· 15 files
FileTypeDescriptionSize
leak_stpad.c trigger-source minimal PoC: fstat() x20000, counts non-zero st_padding1 bytes 1.6 KB view raw
leak_sharp.c trigger-source sharper variant: first 16 non-zero samples + byte-value histogram (shows run-to-run variance) 1.4 KB view raw
build.sh build-script cc -o leak_stpad leak_stpad.c; cc -o leak_sharp leak_sharp.c 328 B view raw
run.sh run-script unprivileged invocation of both PoCs 324 B view raw
run.log run-log decisive unpatched-kernel run (10218/20000 non-zero) 321 B view raw
run.sharp.log run-log 3 sharp runs on unpatched kernel showing varying residue 2.1 KB view raw
leak_sample.txt leak-sample st_padding1[0] byte distributions across 3 runs (genuine variance) 461 B view raw
fix.diff suggested-fix git-apply-able: add sb->st_padding1 = 0; in vn_stat (vfs_vnops.c:852) 242 B view raw
fix_build.log build-log full nativekernel build output of the single-fix kernel (rc=0) 5.6 MB ↓ download
fix_run.log run-log 3 runs on patched #1 kernel: 0/20000 every run (fix validated) 1.3 KB view raw
env.txt environment uname, cc version, kern.version 247 B view raw
VERDICT.md verdict full narrative: mechanism, evidence, fix, validation 4.3 KB ↓ raw
README.md readme original PoC readme 784 B ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original PoC readme
↓ download raw

DF-0034 β€” PoC

leak_stpad.c β€” unprivileged leak of 2 bytes of kernel stack via the uninitialized st_padding1 of struct stat.

The bug

vn_stat (sys/kern/vfs_vnops.c:833-977) zeroes st_lspare and st_qspare2 (:852-853) but never writes st_padding1 (sys/sys/stat.h:105, __uint16_t). Every stat syscall handler declares struct stat st; on the kernel stack unzeroed and copyout(sizeof(struct stat)), leaking 2 bytes of uninitialized kernel stack per fstat/stat/lstat/fstatat/fhstat.

Build & run (unprivileged)

cc -o leak_stpad findings/poc/DF-0034/leak_stpad.c
./leak_stpad

Expected output (bug present)

sample 0: st_padding1 = <..> <..>
...
samples with non-marker/non-zero st_padding1 byte: N
result: LEAK CONFIRMED
VERDICT.md verdict full narrative: mechanism, evidence, fix, validation
↓ download raw

DF-0034 β€” VERDICT

Verdict: REPRODUCED (info leak). Fix VALIDATED.

st_padding1 (__uint16_t, sys/sys/stat.h:105) is never written by vn_stat() (sys/kern/vfs_vnops.c:832-977). vn_stat deliberately zeros the other two spare fields at :852-853 (st_lspare, st_qspare2) but omits st_padding1. Every stat-family syscall handler (sys_fstat/sys_stat/sys_lstat/sys_fstatat/sys_fhstat) declares struct stat st; on the kernel stack unzeroed and copyout()s sizeof(struct stat), so the 2 uninitialized bytes at offsetof(struct stat, st_padding1) (offset 18 on amd64, between st_mode and st_uid) reach userspace containing stale kernel-stack residue.

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger β€” any unprivileged fstat()/stat()/lstat()/fstatat()/ fhstat() on any vnode. No setup, no privilege, default GENERIC kernel.
  2. Primitive β€” 2-byte info leak of uninitialized kernel stack at a fixed offset in the returned struct stat. The handler's stack-local struct stat st; is not zeroed (sys/kern/kern_descrip.c:1574, sys/kern/vfs_syscalls.c:3071,3093,3115), vn_stat fills every field except st_padding1, and copyout(&st, ub, sizeof(st)) ships the residue.
  3. Effect β€” per-call leak of 2 bytes of kernel stack (pointer fragments, prior-syscall residue). Samplable in a tight loop; Low-grade info-scrape / KASLR-assist. No write primitive, no escalation.

Confirmed by

  • Source trace β€” sys/kern/vfs_vnops.c:852-853 zeros st_lspare and st_qspare2 but never assigns st_padding1; sys/sys/stat.h:105 declares it __uint16_t; the five handlers at kern_descrip.c:1574, vfs_syscalls.c:3071/3093/3115 all stack-allocate struct stat st; unzeroed and copyout sizeof.
  • Runtime (unpatched #0 kernel) β€” ./leak_sharp over 20000 fstat() calls showed 7402–17844 samples with non-zero st_padding1[0] across three runs, with run-to-run varying dominant bytes:
  • Run 1: 0xfe (10350), 0x69 (7494)
  • Run 2: 0x19 (9364), 0x68 (1733), 0x1c (1)
  • Run 3: 0x50 (5437), 0x42 (1) The distribution shifts each run and occasional wild values appear β€” the signature of genuine uninitialized stack data, not cosmetic output.

Exploit chain

Not applicable β€” this is a read-only info leak (Phase 6 valid hard blocker: primitive is genuinely read-only, no write/corruption). Impact ceiling is 2 bytes of kernel-stack residue per stat call, samplable in a loop for low-grade KASLR-assist / info-scrape. No uid=0 derivable.

Fix

fix.diff adds sb->st_padding1 = 0; in vn_stat() next to the existing st_lspare/st_qspare2 zeroing (sys/kern/vfs_vnops.c:852). This is the root-cause fix β€” vn_stat is the single function that fills the struct stat for every vnode stat path, so one line closes all five syscall handlers. Matches the finding markdown's ## Recommended fix proposal.

(Defense-in-depth alternative: bzero(&st, sizeof(st)) in each handler before the vn_stat/fo_stat call. The minimal single-point fix in vn_stat is sufficient and is what was validated.)

Fix validation (Phase 8)

Kernel kern.version leak_stpad result leak_sharp result
unpatched baseline 6.5-DEVELOPMENT #0 (Jul 2 06:02) 10218/20000 non-zero 7402–17844/20000, varying bytes
single-fix 6.5-DEVELOPMENT #1 (Jul 12 23:02) 0/20000 (00 00 every sample) 0/20000

Built with make -j6 nativekernel KERNCONF=X86_64_GENERIC from /usr/src with only fix.diff applied; installed kernel.stripped β†’ /boot/kernel/kernel + kernel.debug; rebooted; kern.version bumped #0 β†’ #1. Re-ran both PoCs 3Γ— β€” zero non-zero st_padding1 bytes on every run. Fix is deterministic, closes the leak completely.

PoC changes

  • leak_stpad.c β€” unchanged (the original reviewer-written minimal PoC; it builds and runs as-is on DragonFly 6.5-DEVELOPMENT, gcc 8.3).
  • leak_sharp.c β€” added: sharper variant that prints the first 16 non-zero samples + a byte-value histogram, making the run-to-run variance of the leaked residue visible (proof it is real stack data, not cosmetic).
  • build.sh / run.sh β€” added: exact, runnable repro scripts.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline 10218/20000 non-zero; patched 0/20000 all zero.

BEFORE #0: 10218/20000 leaked, varying values. AFTER #1: 0/20000 all zero x4 runs.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 23:02:13 UTC 2026 (sha256 d68878dce5cd6973fd2257c94cfbc02056b619751e7f03315cc9390af0de20d1)

Confirmed kernel references

Detail

Exploit chain

none (read-only info leak, 2 bytes/call). No escalation.

Evidence (decisive lines)

baseline #0: 10218/20000 non-zero st_padding1, run-to-run variance. patched #1: 0/20000, all zero, x3+1 runs.

PoC changes

leak_stpad.c unchanged. Added leak_sharp.c (histogram + first-16-samples). Added build.sh, run.sh, VERDICT.md, manifest.json, fix.diff.

Verified recommended fix

In sys/kern/vfs_vnops.c:852, add sb->st_padding1 = 0; before existing sb->st_lspare = 0;. Matches finding markdown. Full git-apply-able diff in findings/poc/DF-0034/fix.diff.

Verdict

REPRODUCED. vn_stat() zeros st_lspare/st_qspare2 but never st_padding1 (__uint16_t, sys/sys/stat.h:105). All five stat syscall handlers stack-allocate struct stat unzeroed. 10218/20000 fstat samples leaked non-zero st_padding1 with run-to-run varying values (0xfe/0x69, 0x19/0x68, 0x50) -- genuine stack residue.