Uninitialized kernel stack leaked via /proc/<pid>/fpregs read
| Field | Value |
|---|---|
| ID | DF-0938 |
| Status | new |
| Severity | Medium |
| 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; CWE-200 Exposure of Sensitive Information to an Unauthorized Actor |
| File | sys/vfs/procfs/procfs_fpregs.c |
| Lines | 54, 63, 65 |
| Area | vfs |
| Confidence | certain |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none (same class as FreeBSD SA-18:01.fdread) |
| CVE match | dfly_specific |
Summary
procfs_dofpregs declares struct fpreg r on the stack without
zeroing it, then on the default cpu_fxsr=true amd64 path
fill_fpregs only initializes 108 of the 512 bytes via
fill_fpregs_xmm. uiomove_frombuf(&r, sizeof(r), uio) then copies
the full 512 bytes to userspace, leaking ~404 bytes of uninitialized
kernel stack (function pointers, return addresses, leftover credential
pointers, etc.) to any local user who reads /proc/self/fpregs. This
is a direct KASLR-bypass / kernel-address-disclosure primitive.
Root cause
procfs_fpregs.c:54 declares struct fpreg r; with no initializer.
procfs_fpregs.c:63 calls procfs_read_fpregs(lp, &r) β
fill_fpregs(lp, &r) (machdep.c:3077). On cpu_fxsr==true (the
default on every modern x86), fill_fpregs branches to
fill_fpregs_xmm(&pcb_save.sv_xmm, (struct save87 *)fpregs)
(machdep.c:3082-3085), which writes ONLY:
env87(28 bytes:cw/sw/tw/fip/fcs/opcode/foo/fos)sv_ac[8](8 Γfpacc87= 80 bytes)
= 108 bytes total (machdep.c:3033-3053). It does NOT touch
sv_pad0[4] nor sv_pad[64] inside the save87 overlay, and
struct fpreg (cpu/x86_64/include/reg.h:74-84) is 512 bytes
(fpr_env[4]=32, fpr_acc[8][16]=128, fpr_xacc[16][16]=256,
fpr_spare[12]=96) β far larger than the 176-byte save87 overlay,
so the entire 336-byte fpr_xacc+fpr_spare tail of r is untouched.
procfs_fpregs.c:65 then does uiomove_frombuf(&r, sizeof(r), uio)
which copies sizeof(r)=512 bytes from the partially-initialized stack
buffer out to the reading process. Net result: ~404 bytes
(4 + 64 + 336) of stale kernel-stack content are disclosed to the
reader.
Threat model & preconditions
- Attacker position: Any local user (no privilege required β
reading
/proc/self/fpregsis always permitted byp_trespassbecausecr1==cr2returns0atkern_prot.c:1025-1026). - Privileges gained or impact: KASLR bypass (kernel text/data/stack pointers are routinely left on the thread's kernel stack by prior syscall/trap handling). Can expose other sensitive kernel pointers (credential structs, vmspace pointers, etc.). Combined with a separate kernel memory-corruption primitive, this typically defeats KASLR deterministically.
- Required config or capabilities: None beyond a local account.
- Reachability:
read(open("/proc/self/fpregs", O_RDONLY), buf, 512). The leak is repeatable: each read returns fresh leftovers from whatever syscall executed previously on the same kernel stack, so an attacker can aggregate pointers across reads.
Proof of concept
PoC source: findings/poc/DF-0938/poc.c
Build & run
cc -O2 -o poc poc.c ./poc | hexdump -C | sed -n '7,40p'
Expected output
Bytes 0..107 are the legitimately populated env87 + sv_ac region.
Bytes 108..511 (with the exception of any coincidental zeros) contain
residual kernel-stack data: kernel .text/.rodata pointers, struct
proc/ucred/vmspace pointers, frame pointers, etc. Repeating the
read after issuing different syscalls (open/stat/exec) yields
different leaked pointers, confirming it is uninitialized stack rather
than deterministic FPU state.
Sample: pointer-aligned 8-byte values in 0xffff80xxxxxxxxxx range
(DFly kernel VA range) visible in the leaked tail.
Impact
Local kernel stack info leak (~404 bytes per read). Pure confidentiality impact (no panic, no corruption). Useful as a KASLR defeat enabler for a separate kernel exploitation primitive.
Recommended fix
Zero-initialize r before fill_fpregs touches it:
--- a/sys/vfs/procfs/procfs_fpregs.c
+++ b/sys/vfs/procfs/procfs_fpregs.c
@@ -52,6 +52,7 @@ procfs_dofpregs(struct proc *curp, struct lwp *lp, struct pfsnode *pfs,
struct proc *p = lp->lwp_proc;
int error;
struct fpreg r;
+ memset(&r, 0, sizeof(r));
/* Can't trace a process that's currently exec'ing. */
if ((p->p_flags & P_INEXEC) != 0)
Equivalent defensive alternative: have fill_fpregs_xmm
(machdep.c:3033) explicitly zero sv_pad0/sv_pad and have
fill_fpregs zero the full struct fpreg before the cpu_fxsr
branch β but the procfs-side fix is the minimal, self-contained one and
matches the historical FreeBSD fix (FreeBSD r331844 / SA-18:01.fdread)
for the same class of bug.
Apply the same pattern defensively to procfs_doregs and
procfs_dodbregs which share the bug (their struct reg/struct
dbreg are also stack-declared without zeroing, though on the amd64
fill paths those structs happen to be fully populated today β a
fragile invariant the memset makes explicit).
References
sys/platform/pc64/x86_64/machdep.c:3033-3053βfill_fpregs_xmmonly writes 108 bytes.sys/cpu/x86_64/include/reg.h:74-84βstruct fpregis 512 bytes.- FreeBSD SA-18:01.fdread / r331844 β historical fix for the same bug class in FreeBSD.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0938 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | reads /proc/self/fpregs and reports leaked kernel-stack bytes in the unpopulated tail | 1.5 KB | view raw |
| build.sh | build-script | cc -O2 -o poc poc.c | 138 B | view raw |
| run.sh | run-script | ./poc + hexdump of the 512-byte read | 377 B | view raw |
| build.log | build-log | PoC build output (cc 8.3, exit 0) | 292 B | view raw |
| run.log | run-log | decisive unpatched run: 287 non-zero tail bytes + full hexdump + variance proof | 4.1 KB | view raw |
| run1.bin | leak-sample | raw 512-byte /proc/self/fpregs buffer from run 1 (binary) | 512 B | β download |
| leak_sample.txt | leak-sample | annotated leaked kernel pointers across 3 runs + SHA256 variance proof | 2.4 KB | view raw |
| env.txt | environment | uname, cc version, procfs mount, cpu_fxsr source refs | 453 B | view raw |
| fix.diff | suggested-fix | git-apply-able: add sys/systm.h + bzero(&r,sizeof(r)) before fill_fpregs | 771 B | view raw |
| fix_build.log | build-log | full make -j6 nativekernel output for the single-fix kernel (rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched #1 kernel PoC run: 0 non-zero tail bytes (3x), hexdump all-zero | 2.2 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, evidence, fix, validation | 7.2 KB | β raw |
| manifest.json | manifest | this catalog | 3.4 KB | view raw |
| README.md | readme | human reproduce doc | 1.8 KB | β 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-0938 β /proc/<pid>/fpregs uninitialized kernel stack leak
Summary
procfs_dofpregs (sys/vfs/procfs/procfs_fpregs.c:54) declares struct fpreg r
(512 bytes) on the kernel stack without zeroing. On the default cpu_fxsr=true
amd64 path, fill_fpregs β fill_fpregs_xmm writes only 108 of those 512 bytes
(env87 + sv_ac[8]). uiomove_frombuf(&r, sizeof(r), uio) then copies the full
512 bytes to userspace, leaking ~404 bytes of uninitialized kernel stack β
including kernel .text/.data/heap pointers β to any local user who reads
/proc/self/fpregs. Pure info leak (KASLR-defeat primitive); no corruption.
Build & run (as unprivileged user maxx)
./build.sh && ./run.sh
Equivalent manual invocation:
cc -O2 -o poc poc.c ./poc | hexdump -C | sed -n '1,40p'
Expected output
BUGGY kernel (6.5-DEVELOPMENT #0, unpatched):
read 512 bytes ~300 non-zero non-0xAA bytes in tail [108..512) (leaked kernel stack)
Hexdump offsets 0x70..0x1ff contain kernel-virtual pointers in
0xfffff8008dxxxxxx, 0xfffff80116xxxxxx, 0xffffffff80xxxxxx ranges.
Variance across runs confirms uninitialized stack (not deterministic FPU state).
FIXED kernel (6.5-DEVELOPMENT #1, with fix.diff applied):
read 512 bytes 0 non-zero non-0xAA bytes in tail [108..512) (leaked kernel stack)
Hexdump shows 00 00 00 ... from offset 0x6c onward (deterministic, 3 runs).
Fix
fix.diff β adds #include <sys/systm.h> and bzero(&r, sizeof(r)) immediately
after the struct fpreg r; declaration in procfs_dofpregs. Validated by
building a single-fix kernel and confirming the leak disappears (see VERDICT.md).
This supersedes the finding markdown's proposed memset (which omits the
required systm.h include and fails to compile under -Werror).
DF-0938 β VERDICT
Status: REPRODUCED (info leak)
Impact: leak:~300 bytes of uninitialized kernel stack per /proc/self/fpregs read (includes multiple kernel-virtual pointers β direct KASLR-defeat / kernel-address disclosure).
Confidence: certain
Class: CWE-908 (Use of Uninitialized Resource) / CWE-200 (Info Exposure)
Verdict (one line)
REPRODUCED: reading /proc/self/fpregs as an unprivileged user leaks ~300 bytes of uninitialized kernel stack (including kernel .text/.data/heap pointers) because procfs_dofpregs declares struct fpreg r on the stack without zeroing and fill_fpregs (on the default cpu_fxsr=true amd64 path) only initializes 108 of the 512 bytes.
Mechanism (every hop cited path:line)
-
Stack-declared, unzeroed buffer.
sys/vfs/procfs/procfs_fpregs.c:54βstruct fpreg r;with no initializer.struct fpregis 512 bytes (sys/cpu/x86_64/include/reg.h:74-84:fpr_env[4]=32 + fpr_acc[8][16]=128 + fpr_xacc[16][16]=256 + fpr_spare[12]=96). -
Partial fill on the default path.
procfs_fpregs.c:63callsprocfs_read_fpregs(lp, &r)βfill_fpregs(lp, &r)(sys/platform/pc64/x86_64/machdep.c:3078). Oncpu_fxsr==trueβ which is set unconditionally on every SSE+FXSR CPU at boot (sys/platform/pc64/x86_64/initcpu.c:227-230, i.e. every modern x86_64 including this KVM guest) βfill_fpregscallsfill_fpregs_xmm(&pcb_save.sv_xmm, (struct save87 *)fpregs)(machdep.c:3082-3085). -
fill_fpregs_xmmwrites only 108 bytes.machdep.c:3034-3053writes onlyenv87(28 B:en_cw/sw/tw/fip/fcs/opcode/foo/fos) +sv_ac[8](8 Γfpacc87= 80 B) = 108 B. It never touchessv_pad0[4]norsv_pad[64]of thesave87overlay (sys/cpu/x86_64/include/npx.h:67-80), nor the 336-bytefpr_xacc+fpr_sparetail ofstruct fpregthat lies beyond the 176-bytesave87overlay. -
Full-buffer copyout.
procfs_fpregs.c:65βuiomove_frombuf(&r, sizeof(r), uio)copies the fullsizeof(r)=512bytes to userspace. Net: ~404 bytes (4 + 64 + 336) of the buffer are uninitialized kernel stack β leaked to the reader. -
Reachability / privilege.
procfs_fpregs.c:59checksp_trespass(curp->p_ucred, p->p_ucred); reading one's own/proc/self/fpregsreturns 0 (cr1==cr2,sys/kern/kern_prot.c:1025-1026), so the read is always permitted for any local user with no privilege.
Evidence (decisive)
Unprivileged user maxx (uid 1001, not in wheel), unpatched 6.5-DEVELOPMENT #0 kernel:
$ cc -O2 -o poc poc.c && ./poc read 512 bytes 297 non-zero non-0xAA bytes in tail [108..512) (leaked kernel stack)
Hexdump of the 512-byte read (offset 0x70 onward β the unzeroed tail β is full of kernel pointers):
00000070 88 38 49 18 01 f8 ff ff ... fffff80118493888
00000080 78 35 49 18 01 f8 ff ff 53 4f 9d 80 ff ff ff ff fffff80118493578 ffffffff809d4f53
000000a0 80 01 06 4f e9 03 00 00 00 8c cb 8d 00 f8 ff ff ... fffff8008dcb8c00
000000d0 0a 64 cb 8d 00 f8 ff ff 80 1d d1 16 01 f8 ff ff fffff8008dcb640a fffff80116d11d80
00000100 f0 95 93 16 01 f8 ff ff f0 95 93 16 01 f8 ff ff fffff801169395f0 (Γ2)
00000168 c0 db 0e 81 ff ff ff ff ffffffff810edbc0 β kernel .text
(proc0 = 0xffffffff81176920)
Variance proof (3 runs) β confirms uninitialized stack, not deterministic FPU state:
| run | non-zero tail bytes | qword @ 0x100 | SHA256(512B) |
|---|---|---|---|
| 1 | 300 | fffff801169395f0 |
c3c54806... |
| 2 | 299 | fffff80116939d20 |
81660cce... |
| 3 | 298 | fffff80116939c80 |
4b338660... |
The pointer at 0x100 changes every run; the buffer hash changes every run β stale stack residue.
Exploit chain
None β this is a pure info leak (no write/corruption primitive). Impact ceiling is KASLR defeat / kernel address disclosure: ~404 bytes per read, including kernel .text/.data/heap pointers (e.g. 0xffffffff810xxxxx near proc0, 0xfffff80116xxxxxx heap, 0xfffff8008dxxxxxx text). Repeatable per-read with fresh residue β an attacker can aggregate pointers to deterministically resolve kernel symbols, enabling a separate kernel memory-corruption primitive that requires a known kernel VA. No uid=0 derivable from this bug alone.
PoC changes
PoC (findings/poc/DF-0938/poc.c) was correct as delivered by the reviewer β it compiled and ran first try, no source edits needed. The only changes I made were to the evidence pack: added build.sh, run.sh, VERDICT.md, manifest.json, full build.log/run.log, leak_sample.txt, env.txt, run1.bin, and the validated fix.diff.
Fix (fix.diff)
Zero-initialize struct fpreg r before fill_fpregs touches it, so the unpopulated regions cannot leak stack bytes. Adds #include <sys/systm.h> (for bzero, matching the idiom in the sibling procfs_vfsops.c:86) and bzero(&r, sizeof(r)) immediately after the declaration:
struct fpreg r;
bzero(&r, sizeof(r)); /* zero unpopulated tail so uiomove_frombuf can't leak stack */
This supersedes the finding markdown's proposal (which used memset without the required #include <sys/systm.h> β the kernel builds with -Werror=implicit-function-declaration, so the literal markdown fix fails to compile; switching to bzero + the systm.h include compiles clean). The defensive suggestion in the markdown (also zero in procfs_doregs/procfs_dodbregs) is sound but out of scope for this single finding.
Fix validation (Phase 8)
Built a single-fix kernel (make -j6 nativekernel, .c-only change β fast incremental) on the with-src base, installed kernel.stripped β /boot/kernel/kernel, rebooted into 6.5-DEVELOPMENT #1: Sun Jul 12 12:25:37 UTC 2026 (sha256 b97a7729c4309033e2d9e719793c5702612ac59ac21f3230a3ff628b2a9aeb2a).
| kernel | PoC result |
|---|---|
#0 unpatched (baseline) |
297 non-zero bytes in tail; hexdump full of fffff8../ffffffff810.. kernel pointers |
#1 single-fix |
0 non-zero bytes in tail; hexdump shows 00 00 ... from offset 0x6c onward (deterministic across 3 runs) |
β fixed: the leak is gone on the patched kernel and present on the unpatched baseline. Clean before/after.
Kernel references (verified during this run)
sys/vfs/procfs/procfs_fpregs.c:54βstruct fpreg r;declared on stack, unzeroed.sys/vfs/procfs/procfs_fpregs.c:63βprocfs_read_fpregs(lp, &r)βfill_fpregs.sys/vfs/procfs/procfs_fpregs.c:65βuiomove_frombuf(&r, sizeof(r), uio)copies full 512 B.sys/platform/pc64/x86_64/machdep.c:3078-3085βfill_fpregsbranches tofill_fpregs_xmmwhencpu_fxsr.sys/platform/pc64/x86_64/machdep.c:3034-3053βfill_fpregs_xmmwrites only 108 B (env87 + sv_ac[8]).sys/platform/pc64/x86_64/initcpu.c:227-230βcpu_fxsr=1on every SSE+FXSR CPU (default on modern x86_64).sys/cpu/x86_64/include/reg.h:74-84βstruct fpregis 512 bytes.sys/cpu/x86_64/include/npx.h:67-80βstruct save87(176 B) overlay;sv_pad0[4]+sv_pad[64]never written byfill_fpregs_xmm.sys/kern/kern_prot.c:1025-1026βp_trespassreturns 0 when reading own cred (unprivileged reachability).
Fix verification
fixedVALIDATED. baseline #0 leaked 297 non-zero bytes with kernel pointers; patched #1 leaked 0 non-zero bytes across 3 runs. Fix closes the leak deterministically.
BEFORE (#0): 297 non-zero bytes in tail with kernel pointers fffff80118493888, ffffffff810edbc0. AFTER (#1): 0 non-zero bytes in tail, hexdump all-zero past 0x6c, x3 runs.
Confirmed kernel references
- sys/vfs/procfs/procfs_fpregs.c:54
- sys/vfs/procfs/procfs_fpregs.c:63
- sys/vfs/procfs/procfs_fpregs.c:65
- sys/platform/pc64/x86_64/machdep.c:3078
- sys/platform/pc64/x86_64/machdep.c:3082
- sys/platform/pc64/x86_64/machdep.c:3034
- sys/platform/pc64/x86_64/initcpu.c:227
- sys/cpu/x86_64/include/reg.h:74
- sys/cpu/x86_64/include/npx.h:67
- sys/kern/kern_prot.c:1025
Detail
Exploit chain
none -- pure info leak, no memory-corruption primitive. Impact ceiling is KASLR defeat / kernel address disclosure (~404 bytes per read, repeatable with fresh stack residue).
Evidence (decisive lines)
baseline #0: 297 non-zero bytes in tail [108..512) with kernel pointers fffff80118493888, ffffffff810edbc0 (near proc0). Variance across 3 runs (different SHA256). Patched #1: 0 non-zero bytes, hexdump all-zero past offset 0x6c.
PoC changes
PoC source was correct as delivered. Added evidence pack: build.sh, run.sh, VERDICT.md, manifest.json, README.md, build.log, run.log, leak_sample.txt, env.txt, run1.bin, fix.diff (bzero + systm.h include).
Verified recommended fix
In sys/vfs/procfs/procfs_fpregs.c, add #include
Verdict
REPRODUCED (info leak). The bug is real and on the default path: procfs_dofpregs (sys/vfs/procfs/procfs_fpregs.c:54) declares struct fpreg r (512 B) on the kernel stack unzeroed; fill_fpregs writes only 108 B, leaving ~404 B of stale kernel stack shipped to userspace via uiomove_frombuf. Confirmed by reading /proc/self/fpregs as unprivileged maxx (uid 1001): ~300 non-zero bytes in tail containing kernel .text/.data/heap pointers.
No comments yet.