[PATCH v2 1/2] x86/fpu: Fix memory leak with dynamic fpstate and exec()
From: Guixiong Wei
Date: Thu Oct 08 2026 - 02:08:31 EST
The fpstate embedded in struct fpu only accommodates the default
xfeatures. After a task obtains permission for an XFD-controlled
xfeature, its first use raises #NM. The handler calls
__xfd_enable_feature(), which uses fpstate_realloc() to install a larger
fpstate allocated with vzalloc().
On exec(), fpu_flush_thread() calls fpstate_reset(), which overwrites
fpu->fpstate with the embedded fpstate. This leaks the dynamic
allocation because arch_release_task_struct() cannot free it when the
task exits.
Make fpstate_reset() own the complete lifetime transition. Preserve the
old pointer, install and initialize the embedded fpstate, and then free
the old allocation. This detaches the allocation from fpu->fpstate
before freeing it and makes future reset callers release dynamic state
automatically.
Since fpstate_reset() now releases the old state, initialize
dst_fpu->fpstate to NULL before invoking it from fpu_clone().
Make fpstate_free() operate on the fpstate pointer itself so it can free
the detached allocation. Use it for the existing reallocation and task
release paths as well.
On an AMX-capable KVM guest, 1000 iterations left 1000 dynamic fpstate
allocations on an unfixed kernel and none with this change.
Fixes: 500afbf645a0 ("x86/fpu/xstate: Add fpstate_realloc()/free()")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Guixiong Wei <weiguixiong@xxxxxxxxxxxxx>
---
arch/x86/include/asm/fpu/api.h | 6 +++---
arch/x86/kernel/fpu/core.c | 5 +++++
arch/x86/kernel/fpu/xstate.c | 10 ++++------
arch/x86/kernel/process.c | 2 +-
4 files changed, 13 insertions(+), 10 deletions(-)
diff --git a/arch/x86/include/asm/fpu/api.h b/arch/x86/include/asm/fpu/api.h
index 90c63fe19c0fb..3e0c3b7dce73b 100644
--- a/arch/x86/include/asm/fpu/api.h
+++ b/arch/x86/include/asm/fpu/api.h
@@ -123,11 +123,11 @@ extern void fpu__resume_cpu(void);
DECLARE_PER_CPU(bool, kernel_fpu_allowed);
DECLARE_PER_CPU(struct fpu *, fpu_fpregs_owner_ctx);
-/* Process cleanup */
+/* Dynamic fpstate cleanup */
#ifdef CONFIG_X86_64
-extern void fpstate_free(struct fpu *fpu);
+extern void fpstate_free(struct fpstate *fpstate);
#else
-static inline void fpstate_free(struct fpu *fpu) { }
+static inline void fpstate_free(struct fpstate *fpstate) { }
#endif
/* fpstate-related functions which are exported to KVM */
diff --git a/arch/x86/kernel/fpu/core.c b/arch/x86/kernel/fpu/core.c
index d1aeecd57f5ed..bcebe1f0d82b7 100644
--- a/arch/x86/kernel/fpu/core.c
+++ b/arch/x86/kernel/fpu/core.c
@@ -594,6 +594,8 @@ static void __fpstate_reset(struct fpstate *fpstate)
void fpstate_reset(struct fpu *fpu)
{
+ struct fpstate *oldfpstate = fpu->fpstate;
+
/* Set the fpstate pointer to the default fpstate */
fpu->fpstate = &fpu->__fpstate;
__fpstate_reset(fpu->fpstate);
@@ -610,6 +612,8 @@ void fpstate_reset(struct fpu *fpu)
* guest FPUs, which allows for common guest and userspace ABI.
*/
fpu->guest_perm.__user_state_size = fpu_user_cfg.default_size;
+
+ fpstate_free(oldfpstate);
}
static inline void fpu_inherit_perms(struct fpu *dst_fpu)
@@ -670,6 +674,7 @@ int fpu_clone(struct task_struct *dst, u64 clone_flags, bool minimal,
/* The new task's FPU state cannot be valid in the hardware. */
dst_fpu->last_cpu = -1;
+ dst_fpu->fpstate = NULL;
fpstate_reset(dst_fpu);
if (!cpu_feature_enabled(X86_FEATURE_FPU))
diff --git a/arch/x86/kernel/fpu/xstate.c b/arch/x86/kernel/fpu/xstate.c
index a7b6524a9dea2..ea3736a74fd43 100644
--- a/arch/x86/kernel/fpu/xstate.c
+++ b/arch/x86/kernel/fpu/xstate.c
@@ -1556,10 +1556,10 @@ static int __init xfd_update_static_branch(void)
}
arch_initcall(xfd_update_static_branch)
-void fpstate_free(struct fpu *fpu)
+void fpstate_free(struct fpstate *fpstate)
{
- if (fpu->fpstate && fpu->fpstate != &fpu->__fpstate)
- vfree(fpu->fpstate);
+ if (fpstate && fpstate->is_valloc)
+ vfree(fpstate);
}
/**
@@ -1640,9 +1640,7 @@ static int fpstate_realloc(u64 xfeatures, unsigned int ksize,
xfd_update_state(fpu->fpstate);
fpregs_unlock();
- /* Only free valloc'ed state */
- if (curfps && curfps->is_valloc)
- vfree(curfps);
+ fpstate_free(curfps);
return 0;
}
diff --git a/arch/x86/kernel/process.c b/arch/x86/kernel/process.c
index 346c438ac8801..b7306f257f4b0 100644
--- a/arch/x86/kernel/process.c
+++ b/arch/x86/kernel/process.c
@@ -118,7 +118,7 @@ int arch_dup_task_struct(struct task_struct *dst, struct task_struct *src)
void arch_release_task_struct(struct task_struct *tsk)
{
if (fpu_state_size_dynamic() && !(tsk->flags & (PF_KTHREAD | PF_USER_WORKER)))
- fpstate_free(x86_task_fpu(tsk));
+ fpstate_free(x86_task_fpu(tsk)->fpstate);
}
#endif
--
2.50.1 (Apple Git-155)