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
next prev parent 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