All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.