The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Guopeng Zhang <guopeng.zhang@linux.dev>
To: Suren Baghdasaryan <surenb@google.com>
Cc: Johannes Weiner <hannes@cmpxchg.org>,
	Peter Zijlstra <peterz@infradead.org>,
	Ingo Molnar <mingo@redhat.com>,
	Juri Lelli <juri.lelli@redhat.com>,
	Vincent Guittot <vincent.guittot@linaro.org>,
	Dietmar Eggemann <dietmar.eggemann@arm.com>,
	Steven Rostedt <rostedt@goodmis.org>,
	Ben Segall <bsegall@google.com>, Mel Gorman <mgorman@suse.de>,
	Valentin Schneider <vschneid@redhat.com>,
	K Prateek Nayak <kprateek.nayak@amd.com>,
	Tejun Heo <tj@kernel.org>,
	Zhaoyang Huang <zhaoyang.huang@unisoc.com>,
	"ziwei.dai" <ziwei.dai@unisoc.com>,
	Chengming Zhou <zhouchengming@bytedance.com>,
	linux-kernel@vger.kernel.org
Subject: Re: [PATCH 2/3] sched/psi: Prevent stale timer rearm after rtpoll teardown
Date: Fri, 7 Aug 2026 17:52:26 +0800	[thread overview]
Message-ID: <73b7d519-e815-4439-8470-d3f67b4c8177@linux.dev> (raw)
In-Reply-To: <CAJuCfpGgzBcSzNQ93wF0DatSQc-SApj4xBPC7anrKpKMHZQ_zQ@mail.gmail.com>



在 2026/7/27 12:49, Suren Baghdasaryan 写道:
> On Fri, Jul 17, 2026 at 2:14 AM Guopeng Zhang <guopeng.zhang@linux.dev> wrote:
>>
>> From: Guopeng Zhang <zhangguopeng@kylinos.cn>
>>
>> psi_schedule_rtpoll_work() reads rtpoll_task under RCU before calling
>> mod_timer(). Last-trigger teardown clears the pointer and deletes the
>> timer before waiting for existing readers. A reader that saw the old task
>> can therefore rearm the timer after timer_delete(), leaving a stale timer
>> pending after trigger teardown.
>>
>> psi_cgroup_free() shuts down rtpoll_timer before freeing the group, so the
>> pending timer cannot outlive the psi_group. It can still fire after the
>> last trigger has been removed and wake the waitqueue when no worker is
>> published, and trigger teardown does not leave the timer quiesced.
> 
> quiesced? Don't you just love these AI generated changelogs?
> 

Yes, I do rely on LLMs a bit for help with my English :) The downside is that 
they occasionally sneak words like "quiesced" into the changelog when I'm not 
looking. I'll proofread them more carefully next time.

>>
>> After publishing NULL, wait for existing readers while holding
>> rtpoll_trigger_lock, then use timer_delete_sync() to drain the callback.
>> Holding the lock also prevents a new trigger from reusing the timer until
>> teardown has finished with it.
> 
> I've seen a report of this problem generated by AI and I think it's
> legitimate; however, so far I could not reproduce it even after
> injecting delays to increase the possibility of this race. Have you
> been able to reproduce it? If so, could you please share the
> reproducer?
> 

Yes, I was able to reproduce it.

I used the following debug instrumentation to widen the race window and
count stale timer rearms:

diff --git a/include/linux/psi_types.h b/include/linux/psi_types.h
index dd10c22299ab..02760a014136 100644
--- a/include/linux/psi_types.h
+++ b/include/linux/psi_types.h
@@ -203,6 +203,11 @@ struct psi_group {
 	u64 rtpoll_total[NR_PSI_STATES - 1];
 	u64 rtpoll_next_update;
 	u64 rtpoll_until;
+
+	atomic_t rtpoll_dbg_stale_rearm;
+	atomic_t rtpoll_dbg_hotpath_arm;
+	struct task_struct *rtpoll_dbg_deleted_task;
+	bool rtpoll_dbg_timer_deleted;
 };
 
 #else /* CONFIG_PSI */
diff --git a/kernel/sched/psi.c b/kernel/sched/psi.c
index b9e2a93a757b..bf7d2eaf2196 100644
--- a/kernel/sched/psi.c
+++ b/kernel/sched/psi.c
@@ -140,6 +140,10 @@
 #include <linux/workqueue.h>
 #include <linux/psi.h>
 #include "sched.h"
+#include <linux/debugfs.h>
+#include <linux/delay.h>
+
+#define PSI_DBG_RACE_UDELAY_US	5000
 
 static int psi_bug __read_mostly;
 
@@ -223,6 +227,11 @@ static void group_init(struct psi_group *group)
 	init_waitqueue_head(&group->rtpoll_wait);
 	timer_setup(&group->rtpoll_timer, poll_timer_fn, 0);
 	rcu_assign_pointer(group->rtpoll_task, NULL);
+
+	atomic_set(&group->rtpoll_dbg_stale_rearm, 0);
+	atomic_set(&group->rtpoll_dbg_hotpath_arm, 0);
+	group->rtpoll_dbg_deleted_task = NULL;
+	group->rtpoll_dbg_timer_deleted = false;
 }
 
 void __init psi_init(void)
@@ -634,18 +643,28 @@ static void psi_schedule_rtpoll_work(struct psi_group *group, unsigned long dela
 	 */
 	if (atomic_xchg(&group->rtpoll_scheduled, 1) && !force)
 		return;
+	if (!force)
+		atomic_inc(&group->rtpoll_dbg_hotpath_arm);
 
 	rcu_read_lock();
 
 	task = rcu_dereference(group->rtpoll_task);
+	if (task && PSI_DBG_RACE_UDELAY_US)
+		udelay(PSI_DBG_RACE_UDELAY_US);
+	bool stale_rearm = task &&
+		smp_load_acquire(&group->rtpoll_dbg_timer_deleted) &&
+		task == READ_ONCE(group->rtpoll_dbg_deleted_task);
 	/*
 	 * kworker might be NULL in case psi_trigger_destroy races with
 	 * psi_task_change (hotpath) which can't use locks
 	 */
-	if (likely(task))
+	if (likely(task)) {
 		mod_timer(&group->rtpoll_timer, jiffies + delay);
-	else
+		if (stale_rearm)
+			atomic_inc(&group->rtpoll_dbg_stale_rearm);
+	} else {
 		atomic_set(&group->rtpoll_scheduled, 0);
+	}
 
 	rcu_read_unlock();
 }
@@ -1323,6 +1342,8 @@ int psi_trigger_create_rtpoll_worker(struct psi_group *group)
 		if (!rcu_access_pointer(group->rtpoll_task)) {
 			atomic_set(&group->rtpoll_wakeup, 0);
 			wake_up_process(task);
+			WRITE_ONCE(group->rtpoll_dbg_deleted_task, NULL);
+			smp_store_release(&group->rtpoll_dbg_timer_deleted, false);
 			rcu_assign_pointer(group->rtpoll_task, task);
 
 			/*
@@ -1489,6 +1510,8 @@ void psi_trigger_destroy(struct psi_trigger *t)
 						lockdep_is_held(&group->rtpoll_trigger_lock));
 				rcu_assign_pointer(group->rtpoll_task, NULL);
 				timer_delete(&group->rtpoll_timer);
+				WRITE_ONCE(group->rtpoll_dbg_deleted_task, task_to_destroy);
+				smp_store_release(&group->rtpoll_dbg_timer_deleted, true);
 			}
 		}
 		mutex_unlock(&group->rtpoll_trigger_lock);
@@ -1717,6 +1740,10 @@ static int __init psi_proc_init(void)
 {
 	if (psi_enable) {
 		proc_mkdir("pressure", NULL);
+		debugfs_create_atomic_t("rtpoll_stale_rearm", 0444, NULL,
+					&psi_system.rtpoll_dbg_stale_rearm);
+		debugfs_create_atomic_t("rtpoll_dbg_hotpath_arm", 0444, NULL,
+					&psi_system.rtpoll_dbg_hotpath_arm);
 		proc_create("pressure/io", 0666, NULL, &psi_io_proc_ops);
 		proc_create("pressure/memory", 0666, NULL, &psi_memory_proc_ops);
 		proc_create("pressure/cpu", 0666, NULL, &psi_cpu_proc_ops);

I used the following test script, also with some LLM help:

#!/bin/bash
# Stress driver for the PSI rtpoll stale-timer-rearm race (coordinated mode).
# Single loop: open RT trigger -> IO burst (polling) -> idle (psi_rtpoll_work
# winds down, scheduled=0) -> resume IO (reader fresh-arms, parks in udelay) ->
# close trigger (teardown lands inside the udelay -> stale rearm).
# Env: DURATION BURST IDLE RESUME_DELAY TMPFILE  (root, instrumented kernel)
set -u
set +m

DURATION=${DURATION:-60}
BURST=${BURST:-64}                    # dd reads per active burst
IDLE=${IDLE:-0.15}                    # idle gap (s); must exceed the polling window (~100ms)
RESUME_DELAY=${RESUME_DELAY:-0.003}   # delay before close; lands it inside the udelay window
THRESHOLD_US=${THRESHOLD_US:-1000}
WINDOW_US=${WINDOW_US:-100000}
TMPFILE=${TMPFILE:-/var/tmp/psi_race_buf}
REARM=/sys/kernel/debug/rtpoll_stale_rearm
HOTARM=/sys/kernel/debug/rtpoll_dbg_hotpath_arm
PIDS=/tmp/psi_race.pids

log() { printf '%s\n' "$*"; }

[ "$(id -u)" -eq 0 ] || { log "ERROR: must be root"; exit 1; }
mountpoint -q /sys/kernel/debug 2>/dev/null || mount -t debugfs none /sys/kernel/debug 2>/dev/null
[ -f "$REARM" ] || { log "ERROR: $REARM missing (boot the instrumented kernel)"; exit 1; }

if ! exec 9<>/proc/pressure/io; then log "ERROR: cannot open /proc/pressure/io"; exit 1; fi
if ! printf 'some %s %s\n' "$THRESHOLD_US" "$WINDOW_US" >&9; then
	log "ERROR: failed to create PSI trigger"; exec 9>&-; exit 1
fi
exec 9>&-

cleanup() {
	[ -f "$PIDS" ] && kill -9 $(cat "$PIDS" 2>/dev/null) 2>/dev/null
	pkill -9 -f "if=$TMPFILE" 2>/dev/null
	wait 2>/dev/null
	rm -f "$PIDS"
}
trap cleanup EXIT

if [ -b "$TMPFILE" ]; then
	:
elif [ ! -e "$TMPFILE" ]; then
	log "preparing $TMPFILE (512M) ..."
	dd if=/dev/zero of="$TMPFILE" bs=1M count=512 oflag=direct 2>/dev/null \
		|| dd if=/dev/zero of="$TMPFILE" bs=1M count=512 2>/dev/null
	sync
fi

before_r=$(cat "$REARM" 2>/dev/null || echo 0)
before_h=$(cat "$HOTARM" 2>/dev/null || echo 0)
log "=== PSI rtpoll stale-rearm stress (coordinated) ==="
log "duration=${DURATION}s  burst=${BURST}  idle=${IDLE}s  resume_delay=${RESUME_DELAY}s"
log "stale_rearm before: $before_r   hotpath_arm before: $before_h"

: > "$PIDS"
end=$((SECONDS + DURATION))
while [ "$SECONDS" -lt "$end" ]; do
	exec 3<>/proc/pressure/io 2>/dev/null || break
	echo "some $THRESHOLD_US $WINDOW_US" >&3 2>/dev/null || { exec 3>&-; break; }
	dd if="$TMPFILE" of=/dev/null bs=64k iflag=direct count="$BURST" 2>/dev/null \
		|| dd if="$TMPFILE" of=/dev/null bs=64k count="$BURST" 2>/dev/null
	sleep "$IDLE"
	taskset -c 1 dd if="$TMPFILE" of=/dev/null bs=64k iflag=direct count=4 2>/dev/null &
	echo $! >> "$PIDS"
	sleep "$RESUME_DELAY"
	exec 3>&-
	wait 2>/dev/null
done

cleanup

after_r=$(cat "$REARM" 2>/dev/null || echo 0)
after_h=$(cat "$HOTARM" 2>/dev/null || echo 0)
dr=$((after_r - before_r))
dh=$((after_h - before_h))
log "stale_rearm after:  $after_r   (delta $dr)"
log "hotpath_arm after:  $after_h   (delta $dh)"
log ">>> stale rearm this run: $dr   (hotpath fresh-arms: $dh) <<<"
if [ "$dr" -gt 0 ]; then log "VERDICT: stale rearm OBSERVED."
else log "VERDICT: stale rearm not observed."
fi
if [ "$dh" -eq 0 ]; then
	log "NOTE: hotpath_arm=0 -- reader never fresh-armed (scheduled never 0)."
	log "      Increase IDLE or confirm PSI polling is active."
fi

Run it as root on the instrumented kernel:

sudo taskset -c 0 bash rtpoll_race_stress.sh

This pins the trigger close path to CPU 0, while the resume dd is pinned
to CPU 1 by the script, so the two paths can overlap across CPUs.

Here are the results from 5-minute runs (DURATION=300), with the same
load and a fresh boot before each run:

without patch 2:  rtpoll_stale_rearm = 17   (fresh-arms: 3570)
with patch 2:     rtpoll_stale_rearm =  0   (fresh-arms: 3497)

To test with patch 2 applied: move the two lines in psi_trigger_destroy() 
(WRITE_ONCE(rtpoll_dbg_deleted_task, ...) + smp_store_release(..., true)) 
to after timer_delete_sync() — the rest of the instrumentation is identical.

Thanks,
Guopeng
>>
>> Fixes: 8f91efd870ea ("psi: Fix race between psi_trigger_create/destroy")
>> Signed-off-by: Guopeng Zhang <zhangguopeng@kylinos.cn>
>> ---
>>  kernel/sched/psi.c | 18 +++++++++++-------
>>  1 file changed, 11 insertions(+), 7 deletions(-)
>>
>> diff --git a/kernel/sched/psi.c b/kernel/sched/psi.c
>> index b9e2a93a757b..db9c56fa8923 100644
>> --- a/kernel/sched/psi.c
>> +++ b/kernel/sched/psi.c
>> @@ -1488,18 +1488,22 @@ void psi_trigger_destroy(struct psi_trigger *t)
>>                                                 group->rtpoll_task,
>>                                                 lockdep_is_held(&group->rtpoll_trigger_lock));
>>                                 rcu_assign_pointer(group->rtpoll_task, NULL);
>> -                               timer_delete(&group->rtpoll_timer);
>> +                               /*
>> +                                * Wait for psi_schedule_rtpoll_work() to either
>> +                                * observe the NULL task or finish rearming the timer.
>> +                                * Keeping the mutex held also prevents a new trigger
>> +                                * from installing a task before the old timer is gone.
>> +                                */
>> +                               synchronize_rcu();
>> +                               timer_delete_sync(&group->rtpoll_timer);
> 
> Ok, poll_timer_fn() does not take rtpoll_trigger_lock, so I think this
> is safe. I would like to double-check the code and run some tests
> before approving this fix.
> 
>>                         }
>>                 }
>>                 mutex_unlock(&group->rtpoll_trigger_lock);
>>         }
>>
>> -       /*
>> -        * Wait for psi_schedule_rtpoll_work RCU to complete its read-side
>> -        * critical section before destroying the trigger and optionally the
>> -        * rtpoll_task.
>> -        */
>> -       synchronize_rcu();
>> +       /* The last-trigger path has already waited for RCU readers above. */
>> +       if (!task_to_destroy)
>> +               synchronize_rcu();
>>         /*
>>          * Stop kthread 'psimon' after releasing rtpoll_trigger_lock to prevent
>>          * a deadlock while waiting for psi_rtpoll_work to acquire
>> --
>> 2.43.0


  reply	other threads:[~2026-08-07  9:52 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-17  9:14 [PATCH 0/3] sched/psi: Fix rtpoll teardown races Guopeng Zhang
2026-07-17  9:14 ` [PATCH 1/3] sched/psi: Avoid losing wakeups during rtpoll worker replacement Guopeng Zhang
2026-07-27  4:03   ` Suren Baghdasaryan
2026-08-07  9:51     ` Guopeng Zhang
2026-07-17  9:14 ` [PATCH 2/3] sched/psi: Prevent stale timer rearm after rtpoll teardown Guopeng Zhang
2026-07-27  4:49   ` Suren Baghdasaryan
2026-08-07  9:52     ` Guopeng Zhang [this message]
2026-07-17  9:14 ` [PATCH 3/3] sched/psi: Avoid clobbering rtpoll_scheduled during teardown Guopeng Zhang
2026-07-27  5:53   ` Suren Baghdasaryan

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=73b7d519-e815-4439-8470-d3f67b4c8177@linux.dev \
    --to=guopeng.zhang@linux.dev \
    --cc=bsegall@google.com \
    --cc=dietmar.eggemann@arm.com \
    --cc=hannes@cmpxchg.org \
    --cc=juri.lelli@redhat.com \
    --cc=kprateek.nayak@amd.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mgorman@suse.de \
    --cc=mingo@redhat.com \
    --cc=peterz@infradead.org \
    --cc=rostedt@goodmis.org \
    --cc=surenb@google.com \
    --cc=tj@kernel.org \
    --cc=vincent.guittot@linaro.org \
    --cc=vschneid@redhat.com \
    --cc=zhaoyang.huang@unisoc.com \
    --cc=zhouchengming@bytedance.com \
    --cc=ziwei.dai@unisoc.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox