From 643b64025650d0ac5d57bd522d2eb0996a278081 Mon Sep 17 00:00:00 2001 From: Ping Li Date: Tue, 20 Oct 2026 19:48:07 +0800 Subject: [PATCH 1/4] arm_mpam: Release the mon_sel lock on mpam_save_mbwu_state() errors mpam_save_mbwu_state() takes the mon_sel lock at the top of each loop iteration, but the four error paths that return after failed MPAMF_MON_SEL register accesses do so without releasing it. Take the lock with the conditional mon_sel_lock guard class instead, so all exit paths release it. Fixes: a3a94c13c442 ("arm_mpam: propagate MSC access errors for state saving function") Signed-off-by: Ping Li Reviewed-by: Ben Horgan Reviewed-by: Andre Przywara Signed-off-by: Catalin Marinas --- drivers/resctrl/mpam_devices.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/drivers/resctrl/mpam_devices.c b/drivers/resctrl/mpam_devices.c index 48adc49b833c..c067b6d57d71 100644 --- a/drivers/resctrl/mpam_devices.c +++ b/drivers/resctrl/mpam_devices.c @@ -2037,8 +2037,10 @@ static int mpam_save_mbwu_state(void *arg) struct mpam_class *class = ris->vmsc->comp->class; for (i = 0; i < ris->props.num_mbwu_mon; i++) { - if (WARN_ON_ONCE(!mpam_mon_sel_lock(msc))) - return -EIO; + ACQUIRE(mon_sel_lock, guard)(msc); + ret = ACQUIRE_ERR(mon_sel_lock, &guard); + if (ret) + return ret; mbwu_state = &ris->mbwu_state[i]; cfg = &mbwu_state->cfg; @@ -2061,10 +2063,8 @@ static int mpam_save_mbwu_state(void *arg) cfg->partid = FIELD_GET(MSMON_CFG_x_FLT_PARTID, cur_flt); mbwu_state->enabled = FIELD_GET(MSMON_CFG_x_CTL_EN, cur_ctl); - if (!mbwu_state->enabled) { - mpam_mon_sel_unlock(msc); + if (!mbwu_state->enabled) continue; - } val = 0; mbwu_arg = (struct mon_read) { @@ -2088,8 +2088,6 @@ static int mpam_save_mbwu_state(void *arg) ret = mpam_write_monsel_reg(msc, CFG_MBWU_CTL, 0); if (ret) return ret; - - mpam_mon_sel_unlock(msc); } return 0; From 486d396b19e93f6e56e0a0ba96c14a9ccc1a5977 Mon Sep 17 00:00:00 2001 From: Zhao Gongyi Date: Fri, 23 Oct 2026 20:12:33 +0800 Subject: [PATCH 2/4] arm64/mm: restrict transparent contpte mappings to valid PTEs The transparent contpte layer treats PTE_CONT as software grouping state. When a valid PTE has PTE_CONT set, the contpte helpers may align an operation to a CONT_PTE_SIZE block and operate on the whole block. The current single-PTE helpers use pte_valid_cont() to identify such groups; this is a helper contract, not an architectural restriction on using PTE_CONT with present-invalid descriptors. Explicit users such as hugetlb manage their contiguous encodings separately. contpte_set_ptes() currently sets PTE_CONT from alignment alone. Batched set_ptes() users can therefore install present-invalid PTEs with PTE_CONT set, for example mprotect(PROT_NONE), NUMA hinting through modify_prot_commit_ptes(), fork() copying a PROT_NONE large folio, or a PMD split installing PAGE_NONE PTEs. This is not a hardware programming problem: invalid descriptors are not translated, so the hardware ignores descriptor bits such as PTE_CONT. It is a software-state problem. Some arm64 fast paths, such as test_and_clear_young_ptes(), test pte_cont() directly instead of pte_valid_cont(). In the observed failure, mprotect(PROT_NONE) installed a full CONT_PTE_SIZE range of present-invalid PTEs with PTE_CONT set. MADV_GUARD_INSTALL then zapped one PTE. The nr == 1 clear_full_ptes() path only unfolds pte_valid_cont() entries, so the neighbouring present-invalid entries kept PTE_CONT while the target entry became a guard marker. clear_refs_pte_range() then reached the marker through: ptep_test_and_clear_young() test_and_clear_young_ptes() contpte_test_and_clear_young_ptes() contpte_align_addr_ptep() __ptep_test_and_clear_young() The marker changed from old_pte=0x00000000000047c0 to new_pte=0x00000000000043c0 by clearing PTE_AF. For swap PTEs, bit 10 is part of the swap type field, bits 6-10: 0x47c0 is SWP_PTE_MARKER with offset 4, while 0x43c0 has type 15 and is decoded as an ordinary swap entry. Zap later warned in swap_put_entries_direct() with a bad swap entry. Present-invalid PTEs could retain PTE_CONT as software grouping state, but their single-PTE modification paths would then need to recognize and unfold those groups consistently. Since current helpers mostly use pte_valid_cont() for membership, avoid creating present-invalid transparent contpte groups in contpte_set_ptes(). They bring no hardware benefit while invalid, and eligible valid ranges can be grouped again when they are installed. The public set_ptes() wrapper already clears PTE_CONT before calling contpte_set_ptes(). Return invalid PTEs through __set_ptes() directly, and warn if a future direct caller unexpectedly passes an invalid PTE with PTE_CONT set. This is triggerable by a user process against its own address space. On systems running with panic_on_warn, the warning becomes a denial of service. Fixes: 4602e5757bcc ("arm64/mm: wire up PTE_CONT for user mappings") Cc: stable@vger.kernel.org Reviewed-by: Baolin Wang Tested-by: Lance Yang Signed-off-by: Zhao Gongyi Signed-off-by: Catalin Marinas --- arch/arm64/include/asm/pgtable.h | 3 +++ arch/arm64/mm/contpte.c | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h index 6000905a2e86..6c81a3d22d94 100644 --- a/arch/arm64/include/asm/pgtable.h +++ b/arch/arm64/include/asm/pgtable.h @@ -1651,6 +1651,9 @@ extern void modify_prot_commit_ptes(struct vm_area_struct *vma, unsigned long ad * The contpte APIs are used to transparently manage the contiguous bit in ptes * where it is possible and makes sense to do so. The PTE_CONT bit is considered * a private implementation detail of the public ptep API (see below). + * Single-PTE transparent contpte helpers use pte_valid_cont() to identify + * grouped PTEs; explicit contiguous-encoding users such as hugetlb manage + * their entries separately. */ extern void __contpte_try_fold(struct mm_struct *mm, unsigned long addr, pte_t *ptep, pte_t pte); diff --git a/arch/arm64/mm/contpte.c b/arch/arm64/mm/contpte.c index 0acab179fc1a..be78d8602e51 100644 --- a/arch/arm64/mm/contpte.c +++ b/arch/arm64/mm/contpte.c @@ -460,8 +460,10 @@ void contpte_set_ptes(struct mm_struct *mm, unsigned long addr, */ VM_WARN_ON(nr == 1); - if (!mm_is_user(mm)) + if (!mm_is_user(mm) || !pte_valid(pte)) { + VM_WARN_ON_ONCE(!pte_valid(pte) && pte_cont(pte)); return __set_ptes(mm, addr, ptep, pte, nr); + } end = addr + (nr << PAGE_SHIFT); pfn = pte_pfn(pte); From 078e122f3bf000dd45965997b66deac10ee580fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ezequiel=20Rodriguez?= Date: Wed, 28 Oct 2026 12:43:12 -0300 Subject: [PATCH 3/4] arm64/gcs: release GCS on failed fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copy_thread() can map a CLONE_VM GCS and still return 0. A later copy_process() failure calls exit_thread(), but arm64 did not implement it, so the VMA stayed in the parent's mm. Wire HAVE_EXIT_THREAD to gcs_free(). Kernel threads created with CLONE_VM (io_uring and vhost workers) inherit the parent's gcs_base/gcs_size from arch_dup_task_struct() without allocating their own GCS. When such a thread exits, exit_mm() -> mm_release() -> deactivate_mm() -> gcs_free() unmaps the parent's GCS. With the new exit_thread(), a failed copy_process() for one of these threads would do the same. Clear these fields in copy_thread(). Fixes: 506496bcbb42 ("arm64/gcs: Ensure that new threads have a GCS") Cc: stable@vger.kernel.org Cc: Mark Brown Assisted-by: LLM Signed-off-by: Iván Ezequiel Rodriguez Reviewed-by: Mark Brown Tested-by: Mark Brown Signed-off-by: Catalin Marinas --- arch/arm64/Kconfig | 1 + arch/arm64/kernel/process.c | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig index 8593ad10a9c4..7587cf86d941 100644 --- a/arch/arm64/Kconfig +++ b/arch/arm64/Kconfig @@ -199,6 +199,7 @@ config ARM64 select HAVE_SAMPLE_FTRACE_DIRECT_MULTI select HAVE_BUILDTIME_MCOUNT_SORT select HAVE_EFFICIENT_UNALIGNED_ACCESS + select HAVE_EXIT_THREAD select HAVE_GUP_FAST select HAVE_FTRACE_GRAPH_FUNC select HAVE_FUNCTION_TRACER diff --git a/arch/arm64/kernel/process.c b/arch/arm64/kernel/process.c index 581f80e9b9b7..42b15b0c069f 100644 --- a/arch/arm64/kernel/process.c +++ b/arch/arm64/kernel/process.c @@ -320,9 +320,18 @@ static int copy_thread_gcs(struct task_struct *p, return 0; } +static void gcs_reset_thread(struct task_struct *task) +{ + task->thread.gcs_base = 0; + task->thread.gcs_size = 0; + task->thread.gcspr_el0 = 0; +} + #else static void flush_gcs(void) { } +static void gcs_reset_thread(struct task_struct *task) { } + static int copy_thread_gcs(struct task_struct *p, const struct kernel_clone_args *args) { @@ -341,6 +350,12 @@ void flush_thread(void) flush_gcs(); } +void exit_thread(struct task_struct *tsk) +{ + /* Failed fork after copy_thread(); deactivate_mm() does not run. */ + gcs_free(tsk); +} + void arch_release_task_struct(struct task_struct *tsk) { fpsimd_release_task(tsk); @@ -503,6 +518,8 @@ 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; + /* Kernel threads sharing the parent's mm must not free its GCS. */ + gcs_reset_thread(p); if (system_supports_poe()) p->thread.por_el0 = POR_EL0_INIT; From f46be4da2cbb17574b5369517a9a1e1b414063b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ezequiel=20Rodriguez?= Date: Wed, 28 Oct 2026 12:43:14 -0300 Subject: [PATCH 4/4] selftests/arm64: GCS must not leak on a failed clone3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KTAP coverage for clone3(CLONE_VM|CLONE_PIDFD) with a bad pidfd pointer: sixteen failures must not grow the parent's GCS VMA count. Assisted-by: LLM Signed-off-by: Iván Ezequiel Rodriguez Reviewed-by: Mark Brown Tested-by: Mark Brown Signed-off-by: Catalin Marinas --- tools/testing/selftests/arm64/gcs/.gitignore | 1 + tools/testing/selftests/arm64/gcs/Makefile | 3 +- .../selftests/arm64/gcs/gcs-clone-pidfd.c | 168 ++++++++++++++++++ .../testing/selftests/arm64/gcs/gcs-locking.c | 20 --- tools/testing/selftests/arm64/gcs/gcs-util.h | 24 +++ tools/testing/selftests/arm64/gcs/libc-gcs.c | 20 --- 6 files changed, 195 insertions(+), 41 deletions(-) create mode 100644 tools/testing/selftests/arm64/gcs/gcs-clone-pidfd.c diff --git a/tools/testing/selftests/arm64/gcs/.gitignore b/tools/testing/selftests/arm64/gcs/.gitignore index bbb8e40a7e52..063780ab5779 100644 --- a/tools/testing/selftests/arm64/gcs/.gitignore +++ b/tools/testing/selftests/arm64/gcs/.gitignore @@ -5,3 +5,4 @@ gcs-stress gcs-stress-thread gcspushm gcsstr +gcs-clone-pidfd diff --git a/tools/testing/selftests/arm64/gcs/Makefile b/tools/testing/selftests/arm64/gcs/Makefile index 1fbbf0ca1f02..f1365eea31c9 100644 --- a/tools/testing/selftests/arm64/gcs/Makefile +++ b/tools/testing/selftests/arm64/gcs/Makefile @@ -6,7 +6,8 @@ # nolibc. # -TEST_GEN_PROGS := basic-gcs libc-gcs gcs-locking gcs-stress gcspushm gcsstr +TEST_GEN_PROGS := basic-gcs libc-gcs gcs-locking gcs-stress gcspushm gcsstr \ + gcs-clone-pidfd TEST_GEN_PROGS_EXTENDED := gcs-stress-thread LDLIBS+=-lpthread diff --git a/tools/testing/selftests/arm64/gcs/gcs-clone-pidfd.c b/tools/testing/selftests/arm64/gcs/gcs-clone-pidfd.c new file mode 100644 index 000000000000..96400ea376c0 --- /dev/null +++ b/tools/testing/selftests/arm64/gcs/gcs-clone-pidfd.c @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * clone3(CLONE_VM|CLONE_PIDFD) with an unwritable pidfd pointer must + * not leak a GCS VMA. copy_thread() maps one, then put_user() fails + * and copy_process() calls exit_thread(). Without HAVE_EXIT_THREAD + * that free is a no-op and the VMA stays in the parent's mm. + * + * After enabling GCS, do not return from main(): libc frames above + * that point have no GCS entries. + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "kselftest.h" +#include "gcs-util.h" + +#ifndef HWCAP_GCS +#define HWCAP_GCS (1UL << 32) +#endif + +#ifndef __NR_clone3 +#define __NR_clone3 435 +#endif + +#define NCLONE 16 +#define STACK_SIZE (64 * 1024) + +static int count_ss_vmas(void) +{ + FILE *f; + char line[256]; + int vmas = 0; + + f = fopen("/proc/self/smaps", "r"); + if (!f) + ksft_exit_fail_msg("fopen /proc/self/smaps: %s\n", + strerror(errno)); + + while (fgets(line, sizeof(line), f)) { + if (!strncmp(line, "VmFlags:", 8) && strstr(line, " ss")) + vmas++; + } + fclose(f); + return vmas; +} + +/* + * CLONE_VM maps a new GCS. CLONE_PIDFD lets put_user() fail after that. + * Use a raw svc: the child resumes on an empty GCS, so a libc return + * after clone3 would fault; exit the same way. + */ +static long clone_pidfd(void *stack, size_t stack_size, int *pidfd_ptr) +{ + struct clone_args args = { + .flags = CLONE_VM | CLONE_PIDFD, + .pidfd = (uint64_t)(uintptr_t)pidfd_ptr, + .exit_signal = SIGCHLD, + .stack = (uint64_t)(uintptr_t)stack, + .stack_size = stack_size, + }; + register long num __asm__("x8") = __NR_clone3; + register long ret __asm__("x0") = (long)&args; + register long sz __asm__("x1") = sizeof(args); + int st; + + asm volatile("svc #0" + : "+r"(ret) + : "r"(num), "r"(sz) + : "memory", "cc"); + if (ret == 0) { + register long n __asm__("x8") = __NR_exit; + register long e __asm__("x0") = 0; + + asm volatile("svc #0" : : "r"(n), "r"(e) : "memory"); + __builtin_unreachable(); + } + if (ret < 0) + return ret; + if (waitpid((pid_t)ret, &st, 0) < 0) + return -errno; + if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) + return 1; + return 0; +} + +int main(void) +{ + void *stack; + void *bad_pidfd; + unsigned long gcs_mode; + long rc; + int pidfd = -1; + int vmas0, vmas1; + int i, n_efault = 0; + + ksft_print_header(); + + if (!(getauxval(AT_HWCAP) & HWCAP_GCS)) + ksft_exit_skip("GCS not supported\n"); + + stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); + if (stack == MAP_FAILED) + ksft_exit_fail_msg("mmap stack: %s\n", strerror(errno)); + + bad_pidfd = mmap(NULL, 4096, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (bad_pidfd == MAP_FAILED) + ksft_exit_fail_msg("mmap pidfd page: %s\n", strerror(errno)); + + rc = my_syscall2(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, &gcs_mode); + if (rc) + ksft_exit_fail_msg("PR_GET_SHADOW_STACK_STATUS: %ld\n", rc); + + if (!(gcs_mode & PR_SHADOW_STACK_ENABLE)) { + rc = my_syscall2(__NR_prctl, PR_SET_SHADOW_STACK_STATUS, + PR_SHADOW_STACK_ENABLE); + if (rc == -EOPNOTSUPP || rc == -EINVAL || rc == -EBUSY) + ksft_exit_skip("cannot enable GCS: %ld\n", rc); + if (rc) + ksft_exit_fail_msg("PR_SET_SHADOW_STACK_STATUS: %ld\n", + rc); + } + + rc = clone_pidfd(stack, STACK_SIZE, &pidfd); + if (rc == -ENOSYS || rc == -EINVAL || rc == -EPERM) + ksft_exit_skip("clone3 valid pidfd unavailable: %s\n", + strerror(-rc)); + + ksft_set_plan(3); + + ksft_test_result(rc == 0 && pidfd >= 0, + "clone3 valid pidfd (rc=%ld pidfd=%d)\n", + rc, pidfd); + if (pidfd >= 0) + close(pidfd); + + vmas0 = count_ss_vmas(); + + for (i = 0; i < NCLONE; i++) { + rc = clone_pidfd(stack, STACK_SIZE, bad_pidfd); + if (rc == -EFAULT) + n_efault++; + } + ksft_test_result(n_efault == NCLONE, + "clone3 bad pidfd returns -EFAULT x%d\n", NCLONE); + + vmas1 = count_ss_vmas(); + ksft_test_result(vmas1 == vmas0, + "no GCS VMA leak (%d -> %d)\n", vmas0, vmas1); + + ksft_finished(); +} diff --git a/tools/testing/selftests/arm64/gcs/gcs-locking.c b/tools/testing/selftests/arm64/gcs/gcs-locking.c index 80a1317f6097..8877fd2d2eab 100644 --- a/tools/testing/selftests/arm64/gcs/gcs-locking.c +++ b/tools/testing/selftests/arm64/gcs/gcs-locking.c @@ -18,26 +18,6 @@ #include "gcs-util.h" -#define my_syscall2(num, arg1, arg2) \ -({ \ - register long _num __asm__ ("x8") = (num); \ - register long _arg1 __asm__ ("x0") = (long)(arg1); \ - register long _arg2 __asm__ ("x1") = (long)(arg2); \ - register long _arg3 __asm__ ("x2") = 0; \ - register long _arg4 __asm__ ("x3") = 0; \ - register long _arg5 __asm__ ("x4") = 0; \ - \ - __asm__ volatile ( \ - "svc #0\n" \ - : "=r"(_arg1) \ - : "r"(_arg1), "r"(_arg2), \ - "r"(_arg3), "r"(_arg4), \ - "r"(_arg5), "r"(_num) \ - : "memory", "cc" \ - ); \ - _arg1; \ -}) - /* No mode bits are rejected for locking */ TEST(lock_all_modes) { diff --git a/tools/testing/selftests/arm64/gcs/gcs-util.h b/tools/testing/selftests/arm64/gcs/gcs-util.h index 7a81bb07ed4b..46f6ed9cf54d 100644 --- a/tools/testing/selftests/arm64/gcs/gcs-util.h +++ b/tools/testing/selftests/arm64/gcs/gcs-util.h @@ -43,6 +43,30 @@ #define GCS_CAP(x) (((unsigned long)(x) & GCS_CAP_ADDR_MASK) | \ GCS_CAP_VALID_TOKEN) +/* + * prctl(..., arg1, arg2) with unused registers forced to zero. The + * kernel rejects non-zero unused args; libc may leave garbage there. + */ +#define my_syscall2(num, arg1, arg2) \ +({ \ + register long _num __asm__ ("x8") = (num); \ + register long _arg1 __asm__ ("x0") = (long)(arg1); \ + register long _arg2 __asm__ ("x1") = (long)(arg2); \ + register long _arg3 __asm__ ("x2") = 0; \ + register long _arg4 __asm__ ("x3") = 0; \ + register long _arg5 __asm__ ("x4") = 0; \ + \ + __asm__ volatile ( \ + "svc #0\n" \ + : "=r"(_arg1) \ + : "r"(_arg1), "r"(_arg2), \ + "r"(_arg3), "r"(_arg4), \ + "r"(_arg5), "r"(_num) \ + : "memory", "cc" \ + ); \ + _arg1; \ +}) + static inline unsigned long *get_gcspr(void) { unsigned long *gcspr; diff --git a/tools/testing/selftests/arm64/gcs/libc-gcs.c b/tools/testing/selftests/arm64/gcs/libc-gcs.c index ddb0b2b20155..4e9fa9b82f0f 100644 --- a/tools/testing/selftests/arm64/gcs/libc-gcs.c +++ b/tools/testing/selftests/arm64/gcs/libc-gcs.c @@ -24,26 +24,6 @@ #include "gcs-util.h" -#define my_syscall2(num, arg1, arg2) \ -({ \ - register long _num __asm__ ("x8") = (num); \ - register long _arg1 __asm__ ("x0") = (long)(arg1); \ - register long _arg2 __asm__ ("x1") = (long)(arg2); \ - register long _arg3 __asm__ ("x2") = 0; \ - register long _arg4 __asm__ ("x3") = 0; \ - register long _arg5 __asm__ ("x4") = 0; \ - \ - __asm__ volatile ( \ - "svc #0\n" \ - : "=r"(_arg1) \ - : "r"(_arg1), "r"(_arg2), \ - "r"(_arg3), "r"(_arg4), \ - "r"(_arg5), "r"(_num) \ - : "memory", "cc" \ - ); \ - _arg1; \ -}) - static noinline void gcs_recurse(int depth) { if (depth)