* [PATCH] sched/mmcid: Bound the CID allocation busy wait
@ 2026-09-18 1:34 Jiakai Xu
2026-09-18 10:43 ` Peter Zijlstra
0 siblings, 1 reply; 6+ messages in thread
From: Jiakai Xu @ 2026-09-18 1:34 UTC (permalink / raw)
To: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot
Cc: Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Thomas Gleixner,
Mathieu Desnoyers, linux-kernel, Jiakai Xu
mm_get_cid() spins forever when no CID is available. All callers hold
either a runqueue lock or mm::mm_cid::lock with interrupts disabled,
so the loop relies on another CPU releasing a CID within a short
window.
That assumption fails in two ways:
1) In steady state per task mode CIDs are owned by their tasks for
their whole lifetime and are only released on task exit or
execve(). When the CID bitmap is exhausted, the spinning task
blocks everything on its CPU with interrupts disabled, which
escalates to RCU stalls and can lock up the machine when e.g. a
text_poke IPI targets the spinning CPU.
2) During a mode transition the fixup thread has to acquire the
runqueue lock of the spinning task's CPU to release per CPU owned
CIDs. That lock is held by the spinning task, so neither context
can make progress - a livelock.
Bound the retry loop and return MM_CID_UNSET on exhaustion. All call
sites cope with that:
- The schedule in paths (mm_cid_from_task()/mm_cid_from_cpu()) set
both the per CPU and the task storage to MM_CID_UNSET and retry on
the next schedule in. The plain per CPU value left behind by
mm_drop_cid_on_cpu() has no owner in the bitmap anymore, so the
task must not adopt it as its own CID. A task running with an
unset CID is an already established state for lazily assigned
tasks (see mm_cid_fixup_cpus_to_tasks()).
- sched_mm_cid_fork() stores the unset CID in the task and the per
CPU storage, which the schedule in path handles the same way.
This also prevents an exhausted allocation from feeding MM_CID_UNSET
into the transition bit handling, which would later hand MM_CID_UNSET
as bit number to clear_bit().
Fixes: 9a723ed7facff ("sched/mmcid: Provide new scheduler CID mechanism")
Signed-off-by: Jiakai Xu <xujiakai24@mails.ucas.ac.cn>
---
kernel/sched/sched.h | 51 ++++++++++++++++++++++++++++++++++++++------
1 file changed, 45 insertions(+), 6 deletions(-)
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index e656c7059bf86..6de25f546e3a6 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -3964,11 +3964,27 @@ static inline unsigned int __mm_get_cid(struct mm_struct *mm, unsigned int max_c
return cid;
}
+/*
+ * The retry loop covers the transient contention window where a CID is
+ * concurrently released. It must be bound because all callers hold a
+ * runqueue lock or mm::mm_cid::lock with interrupts disabled. An
+ * unbounded wait livelocks with the context which is expected to
+ * release a CID: in steady state per task mode CIDs are owned by their
+ * tasks until exit and during a mode transition the fixup thread needs
+ * the runqueue lock which the spinning task holds.
+ *
+ * On exhaustion MM_CID_UNSET is returned, which all callers handle by
+ * letting the task run without a CID. It retries on the next schedule
+ * in or fork.
+ */
+#define MM_CID_GET_RETRIES 32
+
static inline unsigned int mm_get_cid(struct mm_struct *mm)
{
unsigned int cid = __mm_get_cid(mm, READ_ONCE(mm->mm_cid.max_cids));
+ unsigned int tries = MM_CID_GET_RETRIES;
- while (cid == MM_CID_UNSET) {
+ while (cid == MM_CID_UNSET && tries--) {
cpu_relax();
cid = __mm_get_cid(mm, num_possible_cpus());
}
@@ -4030,9 +4046,22 @@ static __always_inline void mm_cid_from_cpu(struct task_struct *t, unsigned int
else
cpu_cid = cid_to_cpu_cid(tcid);
}
- /* Still nothing, allocate a new one */
- if (!cid_on_cpu(cpu_cid))
- cpu_cid = cid_to_cpu_cid(mm_get_cid(mm));
+ /* Still nothing, allocate a new one. On pool exhaustion
+ * set both storages to MM_CID_UNSET: the plain per CPU
+ * value left by mm_drop_cid_on_cpu() no longer has an
+ * owner in the bitmap and must not be adopted by the
+ * task. It will be retried on the next schedule in.
+ */
+ if (!cid_on_cpu(cpu_cid)) {
+ unsigned int ncid = mm_get_cid(mm);
+
+ if (ncid == MM_CID_UNSET) {
+ mm_cid_update_pcpu_cid(mm, MM_CID_UNSET);
+ mm_cid_update_task_cid(t, MM_CID_UNSET);
+ return;
+ }
+ cpu_cid = cid_to_cpu_cid(ncid);
+ }
/* Handle the transition mode flag if required */
if (mode & MM_CID_TRANSIT)
@@ -4065,9 +4094,19 @@ static __always_inline void mm_cid_from_task(struct task_struct *t, unsigned int
else
tcid = cpu_cid_to_cid(cpu_cid);
}
- /* Still nothing, allocate a new one */
- if (!cid_on_task(tcid))
+ /* Still nothing, allocate a new one. On pool exhaustion
+ * keep the CID unset. It will be retried on the next
+ * schedule in.
+ */
+ if (!cid_on_task(tcid)) {
tcid = mm_get_cid(mm);
+
+ if (tcid == MM_CID_UNSET) {
+ mm_cid_update_pcpu_cid(mm, tcid);
+ mm_cid_update_task_cid(t, tcid);
+ return;
+ }
+ }
/* Set the transition mode flag if required */
tcid |= mode & MM_CID_TRANSIT;
}
--
2.34.1
--
Below is the crash report:
rcu: INFO: rcu_preempt detected stalls on CPUs/tasks:
rcu: 0-...!: (1 GPs behind) idle=05ac/1/0x4000000000000000 softirq=640934/640935 fqs=18
rcu: (detected by 1, t=10005 jiffies, g=269757, q=11838 ncpus=2)
Sending NMI from CPU 1 to CPUs 0:
NMI backtrace for cpu 0
CPU: 0 UID: 32768 PID: 57790 Comm: syz.4.7239 Tainted: G W L 7.1.13 #1 PREEMPT(full)
Tainted: [W]=WARN, [L]=SOFTLOCKUP
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
RIP: 0010:num_possible_cpus home/zzzrrll/tmp/kf_src/linux-7.1.13/include/linux/cpumask.h:1222 [inline]
RIP: 0010:mm_get_cid home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/sched/sched.h:3880 [inline]
RIP: 0010:sched_mm_cid_fork+0x367/0x5c0 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/sched/core.c:10970
Code: 00 80 39 cd 76 da 89 c8 f0 48 0f ab 03 73 05 b9 00 00 00 80 89 c8 eb c8 41 89 87 6c 0b 00 00 eb 4b 3d 00 00 00 80 75 31 f3 90 <8b> 2d bb 05 8d 05 48 89 df 48 89 ee e8 08 e3 5f 01 48 89 c1 b8 00
RSP: 0018:ffffc9000e44fd88 EFLAGS: 00000046
RAX: 0000000080000000 RBX: ffff8880110b3ed0 RCX: 0000000000000002
RDX: 0000000000000001 RSI: 0000000000000002 RDI: ffff8880110b3ed0
RBP: 0000000000000002 R08: ffff8880f407a000 R09: 0000607e4b7877c0
R10: ffffc9000e44fc24 R11: ffffffff81aecab0 R12: ffff8880110b3910
R13: ffff8880110b3800 R14: ffffffff8999a020 R15: ffff888101201900
FS: 00007f4a95db7640(0000) GS:ffff8880f407a000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f4a95d74fe8 CR3: 000000001d284000 CR4: 0000000000752ef0
PKRU: 80000000
Call Trace:
<TASK>
bprm_execve+0x51d/0x7a0 home/zzzrrll/tmp/kf_src/linux-7.1.13/fs/exec.c:1775
do_execveat_common+0x884/0x900 home/zzzrrll/tmp/kf_src/linux-7.1.13/fs/exec.c:1850
__do_sys_execveat home/zzzrrll/tmp/kf_src/linux-7.1.13/fs/exec.c:1945 [inline]
__se_sys_execveat home/zzzrrll/tmp/kf_src/linux-7.1.13/fs/exec.c:1938 [inline]
__x64_sys_execveat+0x48/0x70 home/zzzrrll/tmp/kf_src/linux-7.1.13/fs/exec.c:1938
do_syscall_x64 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x187/0x520 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x590d6d
Code: 02 b8 ff ff ff ff c3 66 0f 1f 44 00 00 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 a8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f4a95db6fd8 EFLAGS: 00000216 ORIG_RAX: 0000000000000142
RAX: ffffffffffffffda RBX: 0000000000600b67 RCX: 0000000000590d6d
RDX: 0000200000000280 RSI: 0000200000000040 RDI: ffffffffffffff9c
RBP: 00007f4a95db7010 R08: 0000000000000000 R09: 0000000000000000
R10: 00002000000002c0 R11: 0000000000000216 R12: 00007f4a95db7640
R13: 000000000000004d R14: 0000000000528d40 R15: 00007f4a95d97000
</TASK>
rcu: rcu_preempt kthread starved for 9915 jiffies! g269757 f0x0 RCU_GP_WAIT_FQS(5) ->state=0x0 ->cpu=1
rcu: Unless rcu_preempt kthread gets sufficient CPU time, OOM is now expected behavior.
rcu: RCU grace-period kthread stack dump:
task:rcu_preempt state:R running task stack:14136 pid:15 tgid:15 ppid:2 task_flags:0x208040 flags:0x00080000
Call Trace:
<TASK>
context_switch home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/sched/core.c:5395 [inline]
__schedule+0x632/0x17e0 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/sched/core.c:7196
__schedule_loop home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/sched/core.c:7273 [inline]
schedule+0x5b/0xa0 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/sched/core.c:7288
schedule_timeout+0xce/0x150 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/time/sleep_timeout.c:99
rcu_gp_fqs_loop+0x17f/0x5d0 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/rcu/tree.c:2095
rcu_gp_kthread+0x1c/0x110 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/rcu/tree.c:2297
kthread+0x18d/0x1e0 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/kthread.c:436
ret_from_fork+0x191/0x450 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/entry/entry_64.S:245
</TASK>
rcu: Stack dump where RCU GP kthread last ran:
CPU: 1 UID: 0 PID: 10504 Comm: kworker/1:4 Tainted: G W L 7.1.13 #1 PREEMPT(full)
Tainted: [W]=WARN, [L]=SOFTLOCKUP
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
Workqueue: events jump_label_update_timeout
RIP: 0010:csd_lock_wait home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/smp.c:355 [inline]
RIP: 0010:smp_call_function_many_cond+0x63e/0x910 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/smp.c:912
Code: f8 48 8b 1c c5 50 47 df 86 44 8b 64 2b 08 44 89 e6 83 e6 01 31 ff e8 41 5d 05 00 41 83 e4 01 75 07 e8 f6 58 05 00 eb 18 f3 90 <f7> 44 1d 08 01 00 00 00 74 07 e8 e3 58 05 00 eb ed e8 dc 58 05 00
RSP: 0018:ffffc9000e06fca8 EFLAGS: 00000293
RAX: ffffffff81613f2d RBX: ffff8880f407a000 RCX: ffff8881078d8000
RDX: 0000000000000000 RSI: 0000000000000001 RDI: 0000000000000000
RBP: ffffffff899b86e0 R08: ffffffff81613f0f R09: 0000000000000001
R10: 0000000000000002 R11: ffffffff813a8df0 R12: 0000000000000001
R13: ffff88813de2d580 R14: 00000000fffffff8 R15: 0000000000000000
FS: 0000000000000000(0000) GS:ffff8881b447a000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000000000 CR3: 0000000007066000 CR4: 0000000000752ef0
PKRU: 55555554
Call Trace:
<TASK>
on_each_cpu_cond_mask+0x3c/0x90 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/smp.c:1077
on_each_cpu home/zzzrrll/tmp/kf_src/linux-7.1.13/include/linux/smp.h:72 [inline]
smp_text_poke_sync_each_cpu home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/kernel/alternative.c:2773 [inline]
smp_text_poke_batch_finish+0x163/0x590 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/kernel/alternative.c:2983
arch_jump_label_transform_apply+0x1a/0x30 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/kernel/jump_label.c:146
__static_key_slow_dec_cpuslocked+0xea/0x150 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/jump_label.c:315
__static_key_slow_dec home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/jump_label.c:321 [inline]
jump_label_update_timeout+0x1e/0x30 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/jump_label.c:329
process_one_work home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/workqueue.c:3314 [inline]
process_scheduled_works+0x2f9/0x690 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/workqueue.c:3397
worker_thread+0x31a/0x480 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/workqueue.c:3478
kthread+0x18d/0x1e0 home/zzzrrll/tmp/kf_src/linux-7.1.13/kernel/kthread.c:436
ret_from_fork+0x191/0x450 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 home/zzzrrll/tmp/kf_src/linux-7.1.13/arch/x86/entry/entry_64.S:245
</TASK>
---
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [PATCH] sched/mmcid: Bound the CID allocation busy wait 2026-09-18 1:34 [PATCH] sched/mmcid: Bound the CID allocation busy wait Jiakai Xu @ 2026-09-18 10:43 ` Peter Zijlstra 2026-10-15 20:37 ` Thomas Gleixner 0 siblings, 1 reply; 6+ messages in thread From: Peter Zijlstra @ 2026-09-18 10:43 UTC (permalink / raw) To: Jiakai Xu Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Thomas Gleixner, Mathieu Desnoyers, linux-kernel On Fri, Sep 18, 2026 at 01:34:54AM +0000, Jiakai Xu wrote: > mm_get_cid() spins forever when no CID is available. All callers hold > either a runqueue lock or mm::mm_cid::lock with interrupts disabled, > so the loop relies on another CPU releasing a CID within a short > window. > > That assumption fails in two ways: > > 1) In steady state per task mode CIDs are owned by their tasks for > their whole lifetime and are only released on task exit or > execve(). When the CID bitmap is exhausted, the spinning task > blocks everything on its CPU with interrupts disabled, which > escalates to RCU stalls and can lock up the machine when e.g. a > text_poke IPI targets the spinning CPU. > > 2) During a mode transition the fixup thread has to acquire the > runqueue lock of the spinning task's CPU to release per CPU owned > CIDs. That lock is held by the spinning task, so neither context > can make progress - a livelock. > > Bound the retry loop and return MM_CID_UNSET on exhaustion. All call > sites cope with that: > > - The schedule in paths (mm_cid_from_task()/mm_cid_from_cpu()) set > both the per CPU and the task storage to MM_CID_UNSET and retry on > the next schedule in. The plain per CPU value left behind by > mm_drop_cid_on_cpu() has no owner in the bitmap anymore, so the > task must not adopt it as its own CID. A task running with an > unset CID is an already established state for lazily assigned > tasks (see mm_cid_fixup_cpus_to_tasks()). > > - sched_mm_cid_fork() stores the unset CID in the task and the per > CPU storage, which the schedule in path handles the same way. > > This also prevents an exhausted allocation from feeding MM_CID_UNSET > into the transition bit handling, which would later hand MM_CID_UNSET > as bit number to clear_bit(). This all sounds horribly wrong. It fails to explain why the transition isn't happening, nor does it explain how it doesn't utterly violate/break user space. > Fixes: 9a723ed7facff ("sched/mmcid: Provide new scheduler CID mechanism") > Signed-off-by: Jiakai Xu <xujiakai24@mails.ucas.ac.cn> > --- > kernel/sched/sched.h | 51 ++++++++++++++++++++++++++++++++++++++------ > 1 file changed, 45 insertions(+), 6 deletions(-) > > diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h > index e656c7059bf86..6de25f546e3a6 100644 > --- a/kernel/sched/sched.h > +++ b/kernel/sched/sched.h > @@ -3964,11 +3964,27 @@ static inline unsigned int __mm_get_cid(struct mm_struct *mm, unsigned int max_c > return cid; > } > > +/* > + * The retry loop covers the transient contention window where a CID is > + * concurrently released. It must be bound because all callers hold a > + * runqueue lock or mm::mm_cid::lock with interrupts disabled. An > + * unbounded wait livelocks with the context which is expected to > + * release a CID: in steady state per task mode CIDs are owned by their > + * tasks until exit and during a mode transition the fixup thread needs > + * the runqueue lock which the spinning task holds. > + * > + * On exhaustion MM_CID_UNSET is returned, which all callers handle by > + * letting the task run without a CID. It retries on the next schedule > + * in or fork. > + */ > +#define MM_CID_GET_RETRIES 32 > + > static inline unsigned int mm_get_cid(struct mm_struct *mm) > { > unsigned int cid = __mm_get_cid(mm, READ_ONCE(mm->mm_cid.max_cids)); > + unsigned int tries = MM_CID_GET_RETRIES; > > - while (cid == MM_CID_UNSET) { > + while (cid == MM_CID_UNSET && tries--) { > cpu_relax(); > cid = __mm_get_cid(mm, num_possible_cpus()); > } > @@ -4030,9 +4046,22 @@ static __always_inline void mm_cid_from_cpu(struct task_struct *t, unsigned int > else > cpu_cid = cid_to_cpu_cid(tcid); > } > - /* Still nothing, allocate a new one */ > - if (!cid_on_cpu(cpu_cid)) > - cpu_cid = cid_to_cpu_cid(mm_get_cid(mm)); > + /* Still nothing, allocate a new one. On pool exhaustion > + * set both storages to MM_CID_UNSET: the plain per CPU > + * value left by mm_drop_cid_on_cpu() no longer has an > + * owner in the bitmap and must not be adopted by the > + * task. It will be retried on the next schedule in. > + */ This comment style is broken and inconsistent with your earlier comment. > + if (!cid_on_cpu(cpu_cid)) { > + unsigned int ncid = mm_get_cid(mm); > + > + if (ncid == MM_CID_UNSET) { > + mm_cid_update_pcpu_cid(mm, MM_CID_UNSET); > + mm_cid_update_task_cid(t, MM_CID_UNSET); > + return; > + } > + cpu_cid = cid_to_cpu_cid(ncid); > + } > > /* Handle the transition mode flag if required */ > if (mode & MM_CID_TRANSIT) > @@ -4065,9 +4094,19 @@ static __always_inline void mm_cid_from_task(struct task_struct *t, unsigned int > else > tcid = cpu_cid_to_cid(cpu_cid); > } > - /* Still nothing, allocate a new one */ > - if (!cid_on_task(tcid)) > + /* Still nothing, allocate a new one. On pool exhaustion > + * keep the CID unset. It will be retried on the next > + * schedule in. > + */ Again, broken comment style. > + if (!cid_on_task(tcid)) { > tcid = mm_get_cid(mm); > + > + if (tcid == MM_CID_UNSET) { > + mm_cid_update_pcpu_cid(mm, tcid); > + mm_cid_update_task_cid(t, tcid); > + return; > + } > + } > /* Set the transition mode flag if required */ > tcid |= mode & MM_CID_TRANSIT; > } ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] sched/mmcid: Bound the CID allocation busy wait 2026-09-18 10:43 ` Peter Zijlstra @ 2026-10-15 20:37 ` Thomas Gleixner 2026-10-20 13:54 ` Thomas Gleixner 0 siblings, 1 reply; 6+ messages in thread From: Thomas Gleixner @ 2026-10-15 20:37 UTC (permalink / raw) To: Peter Zijlstra, Jiakai Xu Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Mathieu Desnoyers, linux-kernel On Fri, Sep 18 2026 at 12:43, Peter Zijlstra wrote: >> This also prevents an exhausted allocation from feeding MM_CID_UNSET >> into the transition bit handling, which would later hand MM_CID_UNSET >> as bit number to clear_bit(). > > This all sounds horribly wrong. It fails to explain why the transition > isn't happening, nor does it explain how it doesn't utterly > violate/break user space. Correct. Going back to user space without a CID is broken. Looking at the stall splat the scenario is: CPU1 runs a jump label update and is stuck in csd_lock_wait(), which is invoked from on_each_cpu_cond_mask(), smp_text_poke_sync_each_cpu(). That does not make progress because CPU0 is looping in mm_get_cid() with interrupts disabled. CPU0 runs an execve() and loops in mm_get_cid(), which is invoked from sched_mm_cid_fork(), sched_mm_cid_after_execve(). RIP: 0010:mm_get_cid linux-7.1.13/kernel/sched/sched.h:3880 [inline] RIP: 0010:sched_mm_cid_fork linux-7.1.13/kernel/sched/core.c:10970 So yes, that's 7.1.13, which is EOL, but it has all the mmcid fixes and the same issue should reproduce on 7.3-rc7... Jiakai, can you please provide the reproducer for this issue and validate that it still persists on the lastest upstream kernel (7.3-rc7)? The stall makes no sense because mm_get_cid() is invoked from: 10967 /* First user ? */ 10968 if (!mm->mm_cid.users) { 10969 sched_mm_cid_add_user(t, mm); 10970 t->mm_cid.cid = mm_get_cid(mm); which means this is the first CID user on the MM. So how can the bitmap be exhausted? I have a suspicion but it does not add up (yet). This is a two CPU VM. Assume that the process which invokes execve() has more than two threads. That means the MMCID of the process is in per CPU mode. sched_mm_cid_exit() ignores a mode change when the last user goes away and with two CPUs the threshold to switch back is when the user count goes below _ONE_. So there is probably stale state there, but the last outgoing task should have reclaimed the per CPU CID and dropped it, but the other CPU still has it assigned to it. So that first allocation should succeed... I'll try to write a trivial test program tomorrow which creates enough threads and does an execve(). Thanks, tglx ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] sched/mmcid: Bound the CID allocation busy wait 2026-10-15 20:37 ` Thomas Gleixner @ 2026-10-20 13:54 ` Thomas Gleixner 2026-10-21 1:30 ` Jiakai Xu 2026-11-12 1:48 ` Mathieu Desnoyers 0 siblings, 2 replies; 6+ messages in thread From: Thomas Gleixner @ 2026-10-20 13:54 UTC (permalink / raw) To: Peter Zijlstra, Jiakai Xu Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Mathieu Desnoyers, linux-kernel On Thu, Oct 15 2026 at 22:37, Thomas Gleixner wrote: > On Fri, Sep 18 2026 at 12:43, Peter Zijlstra wrote: > The stall makes no sense because mm_get_cid() is invoked from: > > 10967 /* First user ? */ > 10968 if (!mm->mm_cid.users) { > 10969 sched_mm_cid_add_user(t, mm); > 10970 t->mm_cid.cid = mm_get_cid(mm); > > which means this is the first CID user on the MM. So how can the bitmap > be exhausted? > > I have a suspicion but it does not add up (yet). > > This is a two CPU VM. Assume that the process which invokes execve() has > more than two threads. That means the MMCID of the process is in per CPU > mode. sched_mm_cid_exit() ignores a mode change when the last user goes > away and with two CPUs the threshold to switch back is when the user > count goes below _ONE_. So there is probably stale state there, but the > last outgoing task should have reclaimed the per CPU CID and dropped it, > but the other CPU still has it assigned to it. So that first allocation > should succeed... > > I'll try to write a trivial test program tomorrow which creates enough > threads and does an execve(). Hmm. There is stale state, but there is always _ONE_ CID available because the last exiting thread drops the per CPU CID. As the mode is TRANSIT the whole scenario resolves when the new exec spawns threads or schedules around. That just works by chance, but I couldn't find a way to exhaust the bitmap for the initial allocation. So I really need to see the reproducer for this splat. The patch below cleans up the stale data and fixes a consistency issue vs. the allowed CPUs accounting. Thanks, tglx --- --- a/kernel/sched/core.c +++ b/kernel/sched/core.c @@ -10880,6 +10880,10 @@ static inline void mm_update_cpus_allowe */ mc = &mm->mm_cid; guard(raw_spinlock)(&mc->lock); + /* Might have raced with exit() */ + if (!mm->mm_cid.users) + return; + mm_allowed = mm_cpus_allowed(mm); weight = cpumask_weighted_or(mm_allowed, mm_allowed, affmsk); if (weight == mc->nr_cpus_allowed) @@ -11019,6 +11023,33 @@ static bool sched_mm_cid_add_user(struct return mm_update_max_cids(mm); } +/* + * sched_mm_cid_exit() does not handle the mode switch for the last user + * because for normal exit() that's a pointless exercise. But if the + * process which issued execve() was multithreaded and the last exiting + * thread switched back from per CPU mode then this leaves stale state + * around. The indicator for that is mm_cid::mode having MM_CID_TRANSIT + * set. + */ +static void sched_mm_cid_cleanup_execve(struct task_struct *t, struct mm_struct *mm) +{ + if (likely(!mm->mm_cid.mode)) + return; + + WARN_ON_ONCE(mm->mm_cid.mode & MM_CID_ONCPU); + mm->mm_cid.mode = 0; + + unsigned int cpu; + + for_each_possible_cpu(cpu) + per_cpu(mm->mm_cid.pcpu->cid, cpu) = 0; + + bitmap_zero(mm_cidmask(mm), num_possible_cpus()); + + mm->mm_cid.nr_cpus_allowed = t->nr_cpus_allowed; + cpumask_copy(mm_cpus_allowed(mm), &t->cpus_mask); +} + static void sched_mm_cid_fork(struct task_struct *t) { struct mm_struct *mm = t->mm; @@ -11035,6 +11066,7 @@ static void sched_mm_cid_fork(struct tas /* First user ? */ if (!mm->mm_cid.users) { + sched_mm_cid_cleanup_execve(t, mm); sched_mm_cid_add_user(t, mm); t->mm_cid.cid = mm_get_cid(mm); /* Required for execve() */ ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: Re: [PATCH] sched/mmcid: Bound the CID allocation busy wait 2026-10-20 13:54 ` Thomas Gleixner @ 2026-10-21 1:30 ` Jiakai Xu 2026-11-12 1:48 ` Mathieu Desnoyers 1 sibling, 0 replies; 6+ messages in thread From: Jiakai Xu @ 2026-10-21 1:30 UTC (permalink / raw) To: Thomas Gleixner Cc: Peter Zijlstra, Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, Mathieu Desnoyers, linux-kernel > On Thu, Oct 15 2026 at 22:37, Thomas Gleixner wrote: > > On Fri, Sep 18 2026 at 12:43, Peter Zijlstra wrote: > > The stall makes no sense because mm_get_cid() is invoked from: > > > > 10967 /* First user ? */ > > 10968 if (!mm->mm_cid.users) { > > 10969 sched_mm_cid_add_user(t, mm); > > 10970 t->mm_cid.cid = mm_get_cid(mm); > > > > which means this is the first CID user on the MM. So how can the bitmap > > be exhausted? > > > > I have a suspicion but it does not add up (yet). > > > > This is a two CPU VM. Assume that the process which invokes execve() has > > more than two threads. That means the MMCID of the process is in per CPU > > mode. sched_mm_cid_exit() ignores a mode change when the last user goes > > away and with two CPUs the threshold to switch back is when the user > > count goes below _ONE_. So there is probably stale state there, but the > > last outgoing task should have reclaimed the per CPU CID and dropped it, > > but the other CPU still has it assigned to it. So that first allocation > > should succeed... > > > > I'll try to write a trivial test program tomorrow which creates enough > > threads and does an execve(). > > Hmm. There is stale state, but there is always _ONE_ CID available > because the last exiting thread drops the per CPU CID. > > As the mode is TRANSIT the whole scenario resolves when the new exec > spawns threads or schedules around. That just works by chance, but I > couldn't find a way to exhaust the bitmap for the initial allocation. > > So I really need to see the reproducer for this splat. Hi Thomas, Unfortunately, I don't have a reproducer for this issue. Best regards, Jiakai > > The patch below cleans up the stale data and fixes a consistency issue > vs. the allowed CPUs accounting. > > Thanks, > > tglx > --- > --- a/kernel/sched/core.c > +++ b/kernel/sched/core.c > @@ -10880,6 +10880,10 @@ static inline void mm_update_cpus_allowe > */ > mc = &mm->mm_cid; > guard(raw_spinlock)(&mc->lock); > + /* Might have raced with exit() */ > + if (!mm->mm_cid.users) > + return; > + > mm_allowed = mm_cpus_allowed(mm); > weight = cpumask_weighted_or(mm_allowed, mm_allowed, affmsk); > if (weight == mc->nr_cpus_allowed) > @@ -11019,6 +11023,33 @@ static bool sched_mm_cid_add_user(struct > return mm_update_max_cids(mm); > } > > +/* > + * sched_mm_cid_exit() does not handle the mode switch for the last user > + * because for normal exit() that's a pointless exercise. But if the > + * process which issued execve() was multithreaded and the last exiting > + * thread switched back from per CPU mode then this leaves stale state > + * around. The indicator for that is mm_cid::mode having MM_CID_TRANSIT > + * set. > + */ > +static void sched_mm_cid_cleanup_execve(struct task_struct *t, struct mm_struct *mm) > +{ > + if (likely(!mm->mm_cid.mode)) > + return; > + > + WARN_ON_ONCE(mm->mm_cid.mode & MM_CID_ONCPU); > + mm->mm_cid.mode = 0; > + > + unsigned int cpu; > + > + for_each_possible_cpu(cpu) > + per_cpu(mm->mm_cid.pcpu->cid, cpu) = 0; > + > + bitmap_zero(mm_cidmask(mm), num_possible_cpus()); > + > + mm->mm_cid.nr_cpus_allowed = t->nr_cpus_allowed; > + cpumask_copy(mm_cpus_allowed(mm), &t->cpus_mask); > +} > + > static void sched_mm_cid_fork(struct task_struct *t) > { > struct mm_struct *mm = t->mm; > @@ -11035,6 +11066,7 @@ static void sched_mm_cid_fork(struct tas > > /* First user ? */ > if (!mm->mm_cid.users) { > + sched_mm_cid_cleanup_execve(t, mm); > sched_mm_cid_add_user(t, mm); > t->mm_cid.cid = mm_get_cid(mm); > /* Required for execve() */ > > ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] sched/mmcid: Bound the CID allocation busy wait 2026-10-20 13:54 ` Thomas Gleixner 2026-10-21 1:30 ` Jiakai Xu @ 2026-11-12 1:48 ` Mathieu Desnoyers 1 sibling, 0 replies; 6+ messages in thread From: Mathieu Desnoyers @ 2026-11-12 1:48 UTC (permalink / raw) To: Thomas Gleixner, Peter Zijlstra, Jiakai Xu Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak, linux-kernel [-- Attachment #1: Type: text/plain, Size: 6546 bytes --] On Tue, Oct 20 2026 at 15:54, Thomas Gleixner wrote: > Hmm. There is stale state, but there is always _ONE_ CID available > because the last exiting thread drops the per CPU CID. > > As the mode is TRANSIT the whole scenario resolves when the new exec > spawns threads or schedules around. That just works by chance, but I > couldn't find a way to exhaust the bitmap for the initial allocation. > > So I really need to see the reproducer for this splat. Hi Thomas, I have a reproducer for a lockup that starts from the stale state you describe. It needs two more things: the execve() fails, and the task is moved to another CPU during that execve(). The changelog of the patch below has the sequence as a diagram. A failed execve() rejoins the mm it left. The initial allocation succeeds, as you found. But a CID stays set with no owner, so the mm has lost one CID. A single thread should read mm_cid 0. This one reads 1: single thread, mm_cid 1 on CPU 0 after a failed execve moved from CPU 0 to CPU 1 On a guest with two CPUs both bits are now set. The next pthread_create() loops in mm_get_cid(), in sched_mm_cid_fork(). This is on tip sched/urgent (006991aa8997), on guests with two and three CPUs. This lockup is not the splat Jiakai reported. That one loops in the allocation of the first user, in the execve() path. Mine loops at the next clone. I could not show that both come from the same sequence. Your draft cleans up when the first user joins. My fix acts earlier, when the last user leaves. The reproducer is attached: mm_cid_execve_test.c, a selftest for tools/testing/selftests/rseq. Its second case is the sequence of the changelog. Without a fix it fails on guests with two and three CPUs, in six runs out of six. Its first case fails on tip too. That is another problem: after a failed execve() in per CPU mode, the task reads mm_cid 0x80000000. I will post both fixes and the tests as a series. I ran your draft against the reproducer. I applied it by hand on tip plus two other mm_cid fixes of that series, one of them for the 0x80000000 problem. With your draft the second case passes on the same guests, in six runs out of six. So the reset at the first user join also removes the CID without owner. I would like your opinion on the changelog and the comment before I post the series. I found this problem with the help of an LLM coding assistant. Thanks, Mathieu -- >8 -- From: Mathieu Desnoyers <mathieu.desnoyers@efficios.com> Subject: [PATCH] sched/mmcid: Complete the switch to per task mode when the last user leaves sched_mm_cid_exit() ignores the mode change when the last user of an mm leaves. Its comment says there is nothing to do. That holds for an exit: the mm goes away. It holds for a successful execve() too, which joins a new mm. A failed execve() is missed. It leaves the mm in sched_mm_cid_before_execve() and rejoins the same mm in sched_mm_cid_after_execve(). If the last user left in per CPU mode, MM_CID_TRANSIT stays set and the other CPUs still own their CIDs. In the diagram below, the machine has two CPUs. T is the main thread of a process. The four threads it creates only wait. H is another process. It stays on CPU 1. CPU 0 CPU 1 T: creates four threads the mm switches to per CPU mode T: runs on CPU 1 CPU 1 owns a CID T: runs on CPU 0 again CPU 0 owns a CID T: joins the four threads T is the only user left T: execve() sched_mm_cid_before_execve() T leaves as the last user drops the CID of CPU 0 MM_CID_TRANSIT is set CPU 1 still owns its CID H: sched_setaffinity(T, CPU 1) T: migrates to CPU 1 execve() fails sched_mm_cid_after_execve() sched_mm_cid_fork(T) T is the first user mm_get_cid() succeeds writes that CID to the storage of CPU 1 the CID CPU 1 owned keeps its bit, with no owner The mm has one CID fewer from then on. Once every remaining CID is taken, mm_get_cid() loops forever with interrupts disabled. On a guest with two CPUs, the next pthread_create() of T does not return: rcu: INFO: rcu_preempt detected stalls on CPUs/tasks: RIP: 0010:sched_mm_cid_fork+0x1dd/0x660 copy_process+0x2339/0x2ca0 kernel_clone+0xc3/0x3c0 __do_sys_clone3+0xe2/0x130 Per CPU mode lasts down to one user when mm_cid_calc_pcpu_thrs() returns 1. It does with two or three possible CPUs. Fix this by completing the switch when the last user leaves. Run mm_cid_fixup_cpus_to_tasks(), which drops the CIDs the CPUs own and clears MM_CID_TRANSIT. An exit and a successful execve() run the fixup too. They do not need it, but the outcome of an execve() is not known when it leaves the mm. This change was developed with assistance from an LLM. Fixes: 653fda7ae73d ("sched/mmcid: Switch over to the new mechanism") Signed-off-by: Mathieu Desnoyers <mathieu.desnoyers@efficios.com> Link: https://lore.kernel.org/all/87mrs8bhp4.ffs@fw13/ --- kernel/sched/core.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/kernel/sched/core.c b/kernel/sched/core.c index 7e213e88ac28..9ef086f58236 100644 --- a/kernel/sched/core.c +++ b/kernel/sched/core.c @@ -11132,13 +11132,21 @@ void sched_mm_cid_exit(struct task_struct *t) return; } /* Last user */ + bool flip; + scoped_guard(raw_spinlock_irq, &mm->mm_cid.lock) { /* Required across execve() */ if (t == current) mm_cid_transit_to_task(t, this_cpu_ptr(mm->mm_cid.pcpu)); - /* Ignore mode change. There is nothing to do. */ - sched_mm_cid_remove_user(t); + flip = sched_mm_cid_remove_user(t); } + /* + * The last user can get the mm back: a failed execve() + * rejoins it. Complete the switch to per task mode, so + * that no CPU keeps a CID. + */ + if (flip) + mm_cid_fixup_cpus_to_tasks(mm); } /* -- Mathieu Desnoyers EfficiOS Inc. https://www.efficios.com [-- Attachment #2: mm_cid_execve_test.c --] [-- Type: text/x-csrc, Size: 11828 bytes --] // SPDX-License-Identifier: LGPL-2.1 /* * The concurrency ID a thread reports after a failed execve(2). * * sched_mm_cid_fork() makes a task a user of the CIDs of its mm. In per * CPU mode a new user gets no CID there. It takes the CID of its CPU when * it is scheduled in. A failed execve(2) rejoins its mm while the task is * running and the other threads are still users. The task returns to user * space before any schedule in, so the join has to give it a CID. * * A second case covers the last user. A process that is back to a single * thread fails execve(2) while another process moves it to another CPU. * It leaves its mm as the last user on one CPU and rejoins it as the first * user on the other. A single thread must read 0 afterwards. That case * needs per CPU mode to last down to one thread, which the kernel only * does with at most three possible CPUs. The case is skipped when a single * thread reads 0 on both CPUs before the execve(2). * * Per CPU mode needs more threads than the mm has CIDs. The number of CIDs * follows the union of the allowed CPUs of the threads, which only grows. * The affinity is therefore narrowed before the fork(2) that creates the * process under test. A few threads are then enough on any machine. */ #define _GNU_SOURCE #include <errno.h> #include <limits.h> #include <pthread.h> #include <sched.h> #include <signal.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/wait.h> #include <unistd.h> #include "rseq.h" #include "kselftest.h" #define NARROW_CPUS 2 #define THREADS 4 #define ROUNDS 100 #define ATTEMPTS 50 #define MOVE_EXECS 300 /* Exit status of the process under test. */ #define CHILD_PASS 0 #define CHILD_FAIL 1 #define CHILD_SKIP 2 #define CHILD_ERROR 3 static int stop; /* * Stay alive as a user of the CIDs of the mm while the execve(2) below * leaves the mm and rejoins it. A thread needs no rseq area for that. */ static void *waiter(void *arg) { while (!RSEQ_READ_ONCE(stop)) (void)sched_yield(); return NULL; } /* The directory of the test's executable, "." when it cannot be read. */ static const char *test_dir(char *buf, size_t len) { ssize_t n = readlink("/proc/self/exe", buf, len - 1); char *slash; if (n <= 0) return "."; buf[n] = '\0'; slash = strrchr(buf, '/'); if (!slash) return "."; *slash = '\0'; return slash == buf ? "/" : buf; } /* * Create a file that no binfmt handler recognises and leave its path in * @path. execve(2) on it reaches sched_mm_cid_before_execve(). It then * fails with ENOEXEC before begin_new_exec() kills the other threads. A * path that does not exist would fail before bprm_execve(). * * The directory must allow execution. Try the directory of the test first, * then /tmp. Return 0 on success. One execve(2) is done as a check. */ static int make_unknown_format_file(char *path, size_t len) { char self[PATH_MAX]; const char *const dirs[] = { test_dir(self, sizeof(self)), "/tmp" }; static const char junk[] = "not a binary of any known format\n"; unsigned int i; for (i = 0; i < ARRAY_SIZE(dirs); i++) { char *const av[] = { path, NULL }; bool written; int fd; snprintf(path, len, "%s/mm_cid_execve_test.XXXXXX", dirs[i]); fd = mkstemp(path); if (fd < 0) continue; written = write(fd, junk, sizeof(junk) - 1) == (ssize_t)sizeof(junk) - 1 && !fchmod(fd, 0755); /* An open file would make execve(2) fail with ETXTBSY. */ if (close(fd) || !written) { unlink(path); continue; } if (execve(path, av, NULL) == -1 && errno == ENOEXEC) return 0; unlink(path); } return -1; } /* * The process under test. Start THREADS threads on a narrow set of CPUs. * Then fail execve(2) in a loop and read the mm_cid after each call. * Return a CHILD_* status. */ static int run_child(void) { pthread_t thread[THREADS]; char path[PATH_MAX]; unsigned int nr_cids, worst = 0; cpu_set_t mask; int i, err = 0, started = 0, ret = CHILD_PASS; /* The mm has no more IDs than CPUs its threads may run on. */ if (sched_getaffinity(0, sizeof(mask), &mask)) { fprintf(stderr, "sched_getaffinity: %s\n", strerror(errno)); return CHILD_ERROR; } nr_cids = CPU_COUNT(&mask); if (make_unknown_format_file(path, sizeof(path))) { fprintf(stderr, "no directory to run an unknown format file from: %s\n", strerror(errno)); return CHILD_SKIP; } for (i = 0; i < THREADS; i++) { err = pthread_create(&thread[i], NULL, waiter, NULL); if (err) break; started++; } if (started < THREADS) { fprintf(stderr, "pthread_create: %s\n", strerror(err)); ret = CHILD_ERROR; goto out; } for (i = 0; i < ROUNDS; i++) { char *const av[] = { path, NULL }; unsigned int cid; if (!execve(path, av, NULL)) { fprintf(stderr, "execve(%s) succeeded, expected ENOEXEC\n", path); ret = CHILD_ERROR; goto out; } if (errno != ENOEXEC) { fprintf(stderr, "execve(%s): %s, expected ENOEXEC\n", path, strerror(errno)); ret = CHILD_ERROR; goto out; } cid = rseq_current_mm_cid(); if (cid > worst) worst = cid; if (cid >= nr_cids) { fprintf(stderr, "mm_cid %u (0x%x) after a failed execve, round %d of %d\n", cid, cid, i, ROUNDS); ret = CHILD_FAIL; goto out; } } fprintf(stderr, "%d threads, %d failed execve, largest mm_cid %u, below %u\n", started + 1, ROUNDS, worst, nr_cids); out: RSEQ_WRITE_ONCE(stop, 1); for (i = 0; i < started; i++) pthread_join(thread[i], NULL); unlink(path); return ret; } /* Run on @cpu and return the mm_cid read there. */ static unsigned int cid_on_cpu(int cpu) { cpu_set_t one; CPU_ZERO(&one); CPU_SET(cpu, &one); if (sched_setaffinity(0, sizeof(one), &one)) return UINT_MAX; return rseq_current_mm_cid(); } /* * The second process under test. Its threads make the mm switch to per * CPU mode and exit. The last thread then fails execve(2) in a loop on the * CPU @from. A helper process moves it to the CPU @to, @delay_us after the * start of the loop. The thread then visits both CPUs and must read 0 on * each. * Return a CHILD_* status. */ static int run_child_moved(int delay_us) { pthread_t thread[THREADS]; char path[PATH_MAX]; int cpus[NARROW_CPUS], nr = 0, cpu, i, started = 0, ret = CHILD_PASS; int from, to, go[2], status; unsigned int cid0, cid1; cpu_set_t mask, one; pid_t helper, self = getpid(); char c = 0; if (sched_getaffinity(0, sizeof(mask), &mask)) return CHILD_ERROR; for (cpu = 0; cpu < CPU_SETSIZE && nr < NARROW_CPUS; cpu++) { if (CPU_ISSET(cpu, &mask)) cpus[nr++] = cpu; } if (nr < NARROW_CPUS) { fprintf(stderr, "fewer than %d allowed CPUs\n", NARROW_CPUS); return CHILD_SKIP; } if (make_unknown_format_file(path, sizeof(path))) { fprintf(stderr, "no directory to run an unknown format file from\n"); return CHILD_SKIP; } for (i = 0; i < THREADS; i++) { if (pthread_create(&thread[i], NULL, waiter, NULL)) break; started++; } /* Run on both CPUs while the threads exist: each CPU takes a CID. */ for (i = 0; i < 2 * NARROW_CPUS; i++) cid_on_cpu(cpus[i % NARROW_CPUS]); RSEQ_WRITE_ONCE(stop, 1); for (i = 0; i < started; i++) pthread_join(thread[i], NULL); if (started < THREADS) { ret = CHILD_ERROR; goto out; } cid0 = cid_on_cpu(cpus[0]); cid1 = cid_on_cpu(cpus[1]); if (cid0 == UINT_MAX || cid1 == UINT_MAX) { ret = CHILD_ERROR; goto out; } /* The CPUs own the CIDs only if one of them reads above 0. */ if (!cid0 && !cid1) { fprintf(stderr, "the mm is not in per CPU mode with a single thread\n"); ret = CHILD_SKIP; goto out; } /* Leave from the CPU that does not hold CID 0, rejoin on the other. */ from = cid0 ? cpus[0] : cpus[1]; to = from == cpus[0] ? cpus[1] : cpus[0]; cid_on_cpu(from); if (pipe(go)) { ret = CHILD_ERROR; goto out; } helper = fork(); if (helper < 0) { close(go[0]); close(go[1]); ret = CHILD_ERROR; goto out; } if (!helper) { /* Run beside the process under test, not on its CPU. */ CPU_ZERO(&one); CPU_SET(to, &one); if (sched_setaffinity(0, sizeof(one), &one) || read(go[0], &c, 1) != 1) _exit(1); usleep(delay_us); _exit(!!sched_setaffinity(self, sizeof(one), &one)); } if (write(go[1], &c, 1) != 1) { /* The helper waits for that byte. */ kill(helper, SIGKILL); ret = CHILD_ERROR; } for (i = 0; i < MOVE_EXECS; i++) { char *const av[] = { path, NULL }; execve(path, av, NULL); } if (waitpid(helper, &status, 0) != helper || !WIFEXITED(status) || WEXITSTATUS(status)) ret = CHILD_ERROR; close(go[0]); close(go[1]); /* The first visit lets the kernel settle, the second one is judged. */ for (i = 0; i < 2 * NARROW_CPUS && ret == CHILD_PASS; i++) { unsigned int cid = cid_on_cpu(cpus[i % NARROW_CPUS]); if (cid == UINT_MAX) { ret = CHILD_ERROR; } else if (i >= NARROW_CPUS && cid) { fprintf(stderr, "single thread, mm_cid %u on CPU %d after a failed execve moved from CPU %d to CPU %d\n", cid, cpus[i % NARROW_CPUS], from, to); ret = CHILD_FAIL; } } out: unlink(path); return ret; } /* Keep the first @nr CPUs of @src in @dst. */ static int narrow_cpus(cpu_set_t *dst, const cpu_set_t *src, int nr) { int cpu, taken = 0; CPU_ZERO(dst); for (cpu = 0; cpu < CPU_SETSIZE && taken < nr; cpu++) { if (!CPU_ISSET(cpu, src)) continue; CPU_SET(cpu, dst); taken++; } return taken; } /* * Run @fn(@arg) in a new process and return its exit status. The affinity * is @narrow when its mm is created. The union of allowed CPUs of an mm * starts there and only grows. */ static int run_under_test(int (*fn)(int), int arg, const cpu_set_t *narrow, const cpu_set_t *allowed) { int status = 0; pid_t child; if (sched_setaffinity(0, sizeof(*narrow), narrow)) ksft_exit_skip("sched_setaffinity: %s\n", strerror(errno)); child = fork(); if (!child) _exit(fn(arg)); if (child < 0) ksft_exit_fail_msg("fork: %s\n", strerror(errno)); if (sched_setaffinity(0, sizeof(*allowed), allowed)) ksft_exit_fail_msg("sched_setaffinity: %s\n", strerror(errno)); if (waitpid(child, &status, 0) != child) ksft_exit_fail_msg("waitpid: %s\n", strerror(errno)); if (!WIFEXITED(status)) ksft_exit_fail_msg("the process under test was killed by signal %d\n", WTERMSIG(status)); return WEXITSTATUS(status); } static void report(int status, const char *name) { switch (status) { case CHILD_PASS: ksft_test_result_pass("%s\n", name); break; case CHILD_SKIP: ksft_test_result_skip("%s\n", name); break; case CHILD_FAIL: ksft_test_result_fail("%s\n", name); break; default: ksft_exit_fail_msg("the process under test failed to run\n"); } } static int run_child_threads(int unused) { return run_child(); } int main(void) { cpu_set_t allowed, narrow; int status, i; ksft_print_header(); ksft_set_plan(2); if (!rseq_available()) ksft_exit_skip("rseq is unavailable\n"); /* glibc before 2.35 registers no rseq area. */ if (rseq_register_current_thread()) ksft_exit_fail_msg("rseq_register_current_thread: %s\n", strerror(errno)); if (!rseq_mm_cid_available()) ksft_exit_skip("the rseq mm_cid field is unavailable\n"); if (sched_getaffinity(0, sizeof(allowed), &allowed)) ksft_exit_fail_msg("sched_getaffinity: %s\n", strerror(errno)); if (!narrow_cpus(&narrow, &allowed, NARROW_CPUS)) ksft_exit_skip("no CPU to run on\n"); status = run_under_test(run_child_threads, 0, &narrow, &allowed); report(status, "mm_cid after a failed execve"); /* The helper's move has to land inside an execve: spread its delay. */ for (i = 0; i < ATTEMPTS; i++) { status = run_under_test(run_child_moved, (i * 37) % 1000, &narrow, &allowed); if (status != CHILD_PASS) break; } report(status, "mm_cid of a single thread after a failed execve on another CPU"); ksft_finished(); } ^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-11-12 1:48 UTC | newest] Thread overview: 6+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-09-18 1:34 [PATCH] sched/mmcid: Bound the CID allocation busy wait Jiakai Xu 2026-09-18 10:43 ` Peter Zijlstra 2026-10-15 20:37 ` Thomas Gleixner 2026-10-20 13:54 ` Thomas Gleixner 2026-10-21 1:30 ` Jiakai Xu 2026-11-12 1:48 ` Mathieu Desnoyers
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox