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

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/fpregs is always permitted by p_trespass because cr1==cr2 returns 0 at kern_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.

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

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0938 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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).

VERDICT.md verdict full narrative: mechanism, evidence, fix, validation
↓ download raw

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)

  1. Stack-declared, unzeroed buffer. sys/vfs/procfs/procfs_fpregs.c:54 β€” struct fpreg r; with no initializer. struct fpreg is 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).

  2. Partial fill on the default path. procfs_fpregs.c:63 calls procfs_read_fpregs(lp, &r) β†’ fill_fpregs(lp, &r) (sys/platform/pc64/x86_64/machdep.c:3078). On cpu_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_fpregs calls fill_fpregs_xmm(&pcb_save.sv_xmm, (struct save87 *)fpregs) (machdep.c:3082-3085).

  3. fill_fpregs_xmm writes only 108 bytes. machdep.c:3034-3053 writes only env87 (28 B: en_cw/sw/tw/fip/fcs/opcode/foo/fos) + sv_ac[8] (8 Γ— fpacc87 = 80 B) = 108 B. It never touches sv_pad0[4] nor sv_pad[64] of the save87 overlay (sys/cpu/x86_64/include/npx.h:67-80), nor the 336-byte fpr_xacc+fpr_spare tail of struct fpreg that lies beyond the 176-byte save87 overlay.

  4. Full-buffer copyout. procfs_fpregs.c:65 β€” uiomove_frombuf(&r, sizeof(r), uio) copies the full sizeof(r)=512 bytes to userspace. Net: ~404 bytes (4 + 64 + 336) of the buffer are uninitialized kernel stack β†’ leaked to the reader.

  5. Reachability / privilege. procfs_fpregs.c:59 checks p_trespass(curp->p_ucred, p->p_ucred); reading one's own /proc/self/fpregs returns 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)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 12:25:37 UTC 2026 (sha256 /boot/kernel/kernel = b97a7729c4309033e2d9e719793c5702612ac59ac21f3230a3ff628b2a9aeb2a)

Confirmed kernel references

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 and call bzero(&r, sizeof(r)) immediately after the 'struct fpreg r;' declaration (line 54). Supersedes finding proposal (memset -> bzero + systm.h for -Werror cleanliness). Full git-apply-able diff in findings/poc/DF-0938/fix.diff.

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.