copy_thread() maps a GCS for a CLONE_VM child and can still return
0. A later copy_process() failure then goes through
bad_fork_cleanup_thread, which calls exit_thread(). The hole this
series hits is clone3(CLONE_VM|CLONE_PIDFD) when put_user() of the
pidfd fails after that mapping. arm64 did not implement
exit_thread(). deactivate_mm() is what normally calls gcs_free(),
and it does not run on this path. Each rejected clone leaves one
GCS VMA in the parent's mm until the mm dies.
Select HAVE_EXIT_THREAD and free the child's GCS from
exit_thread(). gcs_free() already ignores a cleared base and a
different mm, so the usual exit path (deactivate_mm() then this) is
a no-op the second time. copy_thread_gcs() zeros the inherited
tracking before a user-thread allocation, so an allocation error
still skips exit_thread() with nothing to unmap.
A kthread copies the caller's GCS fields via arch_dup_task_struct().
kernel_thread() uses CLONE_VM, so during a failed copy_process()
the child still shares the caller's mm and gcs_free()'s mm check
does not save us. Clear the inherited tracking as part of kthread
setup.
Tested with CONFIG_ARM64_GCS on QEMU 11.1.2 -machine virt -cpu max.
Sixteen clone3() calls with CLONE_VM|CLONE_PIDFD and an unwritable
pidfd pointer leaked 16 GCS VMAs without the fix. With it, the VMA
count was unchanged and call/return on the parent still used the
original GCS. A successful clone3 child exit with the same flags
runs deactivate_mm() and then exit_thread().
Fixes: 506496bcbb42 ("arm64/gcs: Ensure that new threads have a GCS")
Cc: [email protected]
Cc: Mark Brown <[email protected]>
Assisted-by: LLM
Signed-off-by: Iván Ezequiel Rodriguez <[email protected]>
---
arch/arm64/Kconfig | 1 +
arch/arm64/kernel/process.c | 21 +++++++++++++++++++++
2 files changed, 22 insertions(+)
diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig
index cab741a695f5..c7fc79409f98 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -177,6 +177,7 @@ config ARM64
select HAVE_ARM_SMCCC
select HAVE_ASM_MODVERSIONS
select HAVE_EBPF_JIT
+ select HAVE_EXIT_THREAD
select HAVE_C_RECORDMCOUNT
select HAVE_CMPXCHG_DOUBLE
select HAVE_CMPXCHG_LOCAL
diff --git a/arch/arm64/kernel/process.c b/arch/arm64/kernel/process.c
index 581f80e9b9b7..eb5314b4b42b 100644
--- a/arch/arm64/kernel/process.c
+++ b/arch/arm64/kernel/process.c
@@ -341,6 +341,16 @@ void flush_thread(void)
flush_gcs();
}
+void exit_thread(struct task_struct *tsk)
+{
+ /*
+ * copy_process() calls this after copy_thread() returns 0.
+ * deactivate_mm() does not run on that failure path.
+ * gcs_free() is a no-op once gcs_base is cleared.
+ */
+ gcs_free(tsk);
+}
+
void arch_release_task_struct(struct task_struct *tsk)
{
fpsimd_release_task(tsk);
@@ -503,6 +513,17 @@ int copy_thread(struct task_struct *p, const struct
kernel_clone_args *args)
p->thread.cpu_context.x19 = (unsigned long)args->fn;
p->thread.cpu_context.x20 = (unsigned long)args->fn_arg;
+#ifdef CONFIG_ARM64_GCS
+ /*
+ * kernel_thread() uses CLONE_VM, so a failed
+ * copy_process() still shares the caller's mm.
+ * Inherited GCS tracking would then look like a
+ * child mapping to gcs_free().
+ */
+ p->thread.gcs_base = 0;
+ p->thread.gcs_size = 0;
+ p->thread.gcspr_el0 = 0;
+#endif
if (system_supports_poe())
p->thread.por_el0 = POR_EL0_INIT;
--
2.43.0