]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
arm64/fpsimd: Make clone() compatible with ZA lazy saving
authorMark Rutland <mark.rutland@arm.com>
Thu, 8 May 2025 13:26:33 +0000 (14:26 +0100)
committerWill Deacon <will@kernel.org>
Thu, 8 May 2025 14:29:10 +0000 (15:29 +0100)
Linux is intended to be compatible with userspace written to Arm's
AAPCS64 procedure call standard [1,2]. For the Scalable Matrix Extension
(SME), AAPCS64 was extended with a "ZA lazy saving scheme", where SME's
ZA tile is lazily callee-saved and caller-restored. In this scheme,
TPIDR2_EL0 indicates whether the ZA tile is live or has been saved by
pointing to a "TPIDR2 block" in memory, which has a "za_save_buffer"
pointer. This scheme has been implemented in GCC and LLVM, with
necessary runtime support implemented in glibc and bionic.

AAPCS64 does not specify how the ZA lazy saving scheme is expected to
interact with thread creation mechanisms such as fork() and
pthread_create(), which would be implemented in terms of the Linux clone
syscall. The behaviour implemented by Linux and glibc/bionic doesn't
always compose safely, as explained below.

Currently the clone syscall is implemented such that PSTATE.ZA and the
ZA tile are always inherited by the new task, and TPIDR2_EL0 is
inherited unless the 'flags' argument includes CLONE_SETTLS,
in which case TPIDR2_EL0 is set to 0/NULL. This doesn't make much sense:

(a) TPIDR2_EL0 is part of the calling convention, and changes as control
    is passed between functions. It is *NOT* used for thread local
    storage, despite superficial similarity to TPIDR_EL0, which is is
    used as the TLS register.

(b) TPIDR2_EL0 and PSTATE.ZA are tightly coupled in the procedure call
    standard, and some combinations of states are illegal. In general,
    manipulating the two independently is not guaranteed to be safe.

In practice, code which is compliant with the procedure call standard
may issue a clone syscall while in the "ZA dormant" state, where
PSTATE.ZA==1 and TPIDR2_EL0 is non-null and indicates that ZA needs to
be saved. This can cause a variety of problems, including:

* If the implementation of pthread_create() passes CLONE_SETTLS, the
  new thread will start with PSTATE.ZA==1 and TPIDR2==NULL. Per the
  procedure call standard this is not a legitimate state for most
  functions. This can cause data corruption (e.g. as code may rely on
  PSTATE.ZA being 0 to guarantee that an SMSTART ZA instruction will
  zero the ZA tile contents), and may result in other undefined
  behaviour.

* If the implementation of pthread_create() does not pass CLONE_SETTLS, the
  new thread will start with PSTATE.ZA==1 and TPIDR2 pointing to a
  TPIDR2 block on the parent thread's stack. This can result in a
  variety of problems, e.g.

  - The child may write back to the parent's za_save_buffer, corrupting
    its contents.

  - The child may read from the TPIDR2 block after the parent has reused
    this memory for something else, and consequently the child may abort
    or clobber arbitrary memory.

Ideally we'd require that userspace ensures that a task is in the "ZA
off" state (with PSTATE.ZA==0 and TPIDR2_EL0==NULL) prior to issuing a
clone syscall, and have the kernel force this state for new threads.
Unfortunately, contemporary C libraries do not do this, and simply
forcing this state within the implementation of clone would break
fork().

Instead, we can bodge around this by considering the CLONE_VM flag, and
manipulate PSTATE.ZA and TPIDR2_EL0 as a pair. CLONE_VM indicates that
the new task will run in the same address space as its parent, and in
that case it doesn't make sense to inherit a stale pointer to the
parent's TPIDR2 block:

* For fork(), CLONE_VM will not be set, and it is safe to inherit both
  PSTATE.ZA and TPIDR2_EL0 as the new task will have its own copy of the
  address space, and cannot clobber its parent's stack.

* For pthread_create() and vfork(), CLONE_VM will be set, and discarding
  PSTATE.ZA and TPIDR2_EL0 for the new task doesn't break any existing
  assumptions in userspace.

Implement this behaviour for clone(). We currently inherit PSTATE.ZA in
arch_dup_task_struct(), but this does not have access to the clone
flags, so move this logic under copy_thread(). Documentation is updated
to describe the new behaviour.

[1] https://github.com/ARM-software/abi-aa/releases/download/2025Q1/aapcs64.pdf
[2] https://github.com/ARM-software/abi-aa/blob/c51addc3dc03e73a016a1e4edf25440bcac76431/aapcs64/aapcs64.rst

Suggested-by: Catalin Marinas <catalin.marinas@arm.com>
Signed-off-by: Mark Rutland <mark.rutland@arm.com>
Cc: Catalin Marinas <catalin.marinas@arm.com>
Cc: Daniel Kiss <daniel.kiss@arm.com>
Cc: Marc Zyngier <maz@kernel.org>
Cc: Mark Brown <broonie@kernel.org>
Cc: Richard Sandiford <richard.sandiford@arm.com>
Cc: Sander De Smalen <sander.desmalen@arm.com>
Cc: Tamas Petz <tamas.petz@arm.com>
Cc: Will Deacon <will@kernel.org>
Cc: Yury Khrustalev <yury.khrustalev@arm.com>
Acked-by: Yury Khrustalev <yury.khrustalev@arm.com>
Link: https://lore.kernel.org/r/20250508132644.1395904-14-mark.rutland@arm.com
Signed-off-by: Will Deacon <will@kernel.org>
Documentation/arch/arm64/sme.rst
arch/arm64/kernel/process.c

index 3a98aed92732edf519a1c90ab19249636a7831a7..1c1e48d8bd1a58f9cdc72df3d5b41aa75802f513 100644 (file)
@@ -69,8 +69,8 @@ model features for SME is included in Appendix A.
   vectors from 0 to VL/8-1 stored in the same endianness invariant format as is
   used for SVE vectors.
 
-* On thread creation TPIDR2_EL0 is preserved unless CLONE_SETTLS is specified,
-  in which case it is set to 0.
+* On thread creation PSTATE.ZA and TPIDR2_EL0 are preserved unless CLONE_VM
+  is specified, in which case PSTATE.ZA is set to 0 and TPIDR2_EL0 is set to 0.
 
 2.  Vector lengths
 ------------------
index 27a5b0c7ec60b219c72c8f5c66a0f772c6ff4142..74bee78cc3fac2e993b20d6765eda7e2358bdda6 100644 (file)
@@ -364,31 +364,14 @@ int arch_dup_task_struct(struct task_struct *dst, struct task_struct *src)
        task_smstop_sm(dst);
 
        /*
-        * In the unlikely event that we create a new thread with ZA
-        * enabled we should retain the ZA and ZT state so duplicate
-        * it here.  This may be shortly freed if we exec() or if
-        * CLONE_SETTLS but it's simpler to do it here. To avoid
-        * confusing the rest of the code ensure that we have a
-        * sve_state allocated whenever sme_state is allocated.
+        * Drop stale reference to src's sme_state and ensure dst has ZA
+        * disabled.
+        *
+        * When necessary, ZA will be inherited later in copy_thread_za().
         */
-       if (thread_za_enabled(&src->thread)) {
-               dst->thread.sve_state = kzalloc(sve_state_size(src),
-                                               GFP_KERNEL);
-               if (!dst->thread.sve_state)
-                       return -ENOMEM;
-
-               dst->thread.sme_state = kmemdup(src->thread.sme_state,
-                                               sme_state_size(src),
-                                               GFP_KERNEL);
-               if (!dst->thread.sme_state) {
-                       kfree(dst->thread.sve_state);
-                       dst->thread.sve_state = NULL;
-                       return -ENOMEM;
-               }
-       } else {
-               dst->thread.sme_state = NULL;
-               clear_tsk_thread_flag(dst, TIF_SME);
-       }
+       dst->thread.sme_state = NULL;
+       clear_tsk_thread_flag(dst, TIF_SME);
+       dst->thread.svcr &= ~SVCR_ZA_MASK;
 
        /* clear any pending asynchronous tag fault raised by the parent */
        clear_tsk_thread_flag(dst, TIF_MTE_ASYNC_FAULT);
@@ -396,6 +379,31 @@ int arch_dup_task_struct(struct task_struct *dst, struct task_struct *src)
        return 0;
 }
 
+static int copy_thread_za(struct task_struct *dst, struct task_struct *src)
+{
+       if (!thread_za_enabled(&src->thread))
+               return 0;
+
+       dst->thread.sve_state = kzalloc(sve_state_size(src),
+                                       GFP_KERNEL);
+       if (!dst->thread.sve_state)
+               return -ENOMEM;
+
+       dst->thread.sme_state = kmemdup(src->thread.sme_state,
+                                       sme_state_size(src),
+                                       GFP_KERNEL);
+       if (!dst->thread.sme_state) {
+               kfree(dst->thread.sve_state);
+               dst->thread.sve_state = NULL;
+               return -ENOMEM;
+       }
+
+       set_tsk_thread_flag(dst, TIF_SME);
+       dst->thread.svcr |= SVCR_ZA_MASK;
+
+       return 0;
+}
+
 asmlinkage void ret_from_fork(void) asm("ret_from_fork");
 
 int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
@@ -428,8 +436,6 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
                 * out-of-sync with the saved value.
                 */
                *task_user_tls(p) = read_sysreg(tpidr_el0);
-               if (system_supports_tpidr2())
-                       p->thread.tpidr2_el0 = read_sysreg_s(SYS_TPIDR2_EL0);
 
                if (system_supports_poe())
                        p->thread.por_el0 = read_sysreg_s(SYS_POR_EL0);
@@ -441,14 +447,40 @@ int copy_thread(struct task_struct *p, const struct kernel_clone_args *args)
                                childregs->sp = stack_start;
                }
 
+               /*
+                * Due to the AAPCS64 "ZA lazy saving scheme", PSTATE.ZA and
+                * TPIDR2 need to be manipulated as a pair, and either both
+                * need to be inherited or both need to be reset.
+                *
+                * Within a process, child threads must not inherit their
+                * parent's TPIDR2 value or they may clobber their parent's
+                * stack at some later point.
+                *
+                * When a process is fork()'d, the child must inherit ZA and
+                * TPIDR2 from its parent in case there was dormant ZA state.
+                *
+                * Use CLONE_VM to determine when the child will share the
+                * address space with the parent, and cannot safely inherit the
+                * state.
+                */
+               if (system_supports_sme()) {
+                       if (!(clone_flags & CLONE_VM)) {
+                               p->thread.tpidr2_el0 = read_sysreg_s(SYS_TPIDR2_EL0);
+                               ret = copy_thread_za(p, current);
+                               if (ret)
+                                       return ret;
+                       } else {
+                               p->thread.tpidr2_el0 = 0;
+                               WARN_ON_ONCE(p->thread.svcr & SVCR_ZA_MASK);
+                       }
+               }
+
                /*
                 * If a TLS pointer was passed to clone, use it for the new
-                * thread.  We also reset TPIDR2 if it's in use.
+                * thread.
                 */
-               if (clone_flags & CLONE_SETTLS) {
+               if (clone_flags & CLONE_SETTLS)
                        p->thread.uw.tp_value = tls;
-                       p->thread.tpidr2_el0 = 0;
-               }
 
                ret = copy_thread_gcs(p, args);
                if (ret != 0)