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
/* 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.
Recommended fix
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
sys/kern/vfs_vnops.c:852-853β spares zeroed exceptst_padding1.sys/sys/stat.h:105β__uint16_t st_padding1.- CWE-908 Use of Uninitialized Resource.
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)
PoC verification
Evidence pack
findings/poc/DF-0034 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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)
- Trigger β any unprivileged
fstat()/stat()/lstat()/fstatat()/fhstat()on any vnode. No setup, no privilege, default GENERIC kernel. - Primitive β 2-byte info leak of uninitialized kernel stack at a fixed
offset in the returned
struct stat. The handler's stack-localstruct stat st;is not zeroed (sys/kern/kern_descrip.c:1574,sys/kern/vfs_syscalls.c:3071,3093,3115),vn_statfills every field exceptst_padding1, andcopyout(&st, ub, sizeof(st))ships the residue. - 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-853zerosst_lspareandst_qspare2but never assignsst_padding1;sys/sys/stat.h:105declares it__uint16_t; the five handlers atkern_descrip.c:1574,vfs_syscalls.c:3071/3093/3115all stack-allocatestruct stat st;unzeroed andcopyout sizeof. - Runtime (unpatched
#0kernel) β./leak_sharpover 20000fstat()calls showed 7402β17844 samples with non-zerost_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
fixedVALIDATED: 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.
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.
No comments yet.