Linux RTC
 help / color / mirror / Atom feed
From: "Alperen Kılıç" <sabri.alperen03@gmail.com>
To: Alexandre Belloni <alexandre.belloni@bootlin.com>
Cc: linux-rtc@vger.kernel.org, "John Stultz" <jstultz@google.com>,
	"Thomas Gleixner" <tglx@kernel.org>,
	"Stephen Boyd" <sboyd@kernel.org>,
	"Miroslav Lichvar" <mlichvar@redhat.com>,
	linux-kernel@vger.kernel.org,
	"Alperen Kılıç" <sabri.alperen03@gmail.com>,
	stable@vger.kernel.org
Subject: [PATCH] rtc: class: Do not inject sleep time without a suspend snapshot
Date: Sat,  3 Oct 2026 19:31:38 +0300	[thread overview]
Message-ID: <20261003163138.14221-1-sabri.alperen03@gmail.com> (raw)

rtc_suspend() returns early when timekeeping_rtc_skipsuspend() is true,
which is the case on every system with a persistent clock. old_rtc and
old_system are then never written and stay zero.

rtc_resume() is skipped under a different condition,
timekeeping_rtc_skipresume(), which is only true once
timekeeping_resume() has injected sleep time. timekeeping_resume() does
that if a nonstop clocksource reports elapsed time or if the persistent
clock is ahead of timekeeping_suspend_time. If neither holds, it injects
nothing and rtc_resume() runs. With old_rtc and old_system being zero,
rtc_resume() computes

	sleep_time = new_rtc - new_system

and injects it when it is not negative. For an RTC kept in UTC this is
about zero. For an RTC kept in local time east of UTC it is the UTC
offset, so CLOCK_REALTIME and CLOCK_BOOTTIME jump ahead by that amount.

This happens with suspend-to-idle on x86. The persistent clock is the
CMOS RTC with a resolution of one second, and the TSC is only flagged
CLOCK_SOURCE_SUSPEND_NONSTOP on CPUs with X86_FEATURE_NONSTOP_TSC_S3.
timekeeping_suspend() and timekeeping_resume() are called for every pass
through the s2idle loop, and when the last pass freezes timekeeping for
less than a second, the persistent clock may not have advanced past
timekeeping_suspend_time.

Seen on a Lenovo ThinkBook 16p G5 (Core i9-14900HX, s2idle only) with the
RTC in local time at UTC+3: on some resumes the system clock jumped
three hours ahead and chronyd then slewed it back for more than a day.
A kprobe on timekeeping_inject_sleeptime64() shows the call coming from
rtc_resume() with a delta of 10798 seconds.

Record in rtc_suspend() whether the snapshot was taken and return early
from rtc_resume() when it was not. This also covers the case where
rtc_suspend() failed to read the RTC. The device name check moves in
front of timekeeping_rtc_skipsuspend() so that the flag is cleared on
every suspend of the hctosys device.

A freeze too short to be seen by the persistent clock is not accounted
after this change. With the RTC in UTC the injected value used to be the
sub-second difference between the RTC and the system time, not the time
spent suspended.

Tested on v7.3-rc5 with a script that shifts the system clock by less
than a second relative to the RTC and then suspends to idle with a wake
alarm a few seconds ahead, 30 times. Unpatched, 4 cycles jumped by the
UTC offset, each of them with timekeeping frozen for less than 0.9
seconds. Patched, no cycle jumped although 6 of them met that condition.

Fixes: 0fa88cb4b82b ("time, drivers/rtc: Don't bother with rtc_resume() for the nonstop clocksource")
Link: https://bugzilla.redhat.com/show_bug.cgi?id=2543517
Cc: stable@vger.kernel.org
Signed-off-by: Alperen Kılıç <sabri.alperen03@gmail.com>
---

Notes:
    The jump was found and measured on my own laptop (suspend hook log in
    the Bugzilla report). An LLM assisted in analyzing the RTC and timekeeping
    resume path, writing the reproducer script and drafting this patch and
    its changelog. I reviewed the change and ran the reproducer on unpatched
    and patched v7.3-rc5 builds myself.

 drivers/rtc/class.c | 21 +++++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/drivers/rtc/class.c b/drivers/rtc/class.c
index 01ba04028..c1a949252 100644
--- a/drivers/rtc/class.c
+++ b/drivers/rtc/class.c
@@ -96,6 +96,8 @@ static void rtc_hctosys(struct rtc_device *rtc)
  */
 
 static struct timespec64 old_rtc, old_system, old_delta;
+/* Set when rtc_suspend() took the snapshot rtc_resume() relies on */
+static bool old_rtc_valid;
 
 static int rtc_suspend(struct device *dev)
 {
@@ -104,10 +106,12 @@ static int rtc_suspend(struct device *dev)
 	struct timespec64	delta, delta_delta;
 	int err;
 
-	if (timekeeping_rtc_skipsuspend())
+	if (strcmp(dev_name(&rtc->dev), CONFIG_RTC_HCTOSYS_DEVICE) != 0)
 		return 0;
 
-	if (strcmp(dev_name(&rtc->dev), CONFIG_RTC_HCTOSYS_DEVICE) != 0)
+	old_rtc_valid = false;
+
+	if (timekeeping_rtc_skipsuspend())
 		return 0;
 
 	/* snapshot the current RTC and system time at suspend*/
@@ -119,6 +123,7 @@ static int rtc_suspend(struct device *dev)
 
 	ktime_get_real_ts64(&old_system);
 	old_rtc.tv_sec = rtc_tm_to_time64(&tm);
+	old_rtc_valid = true;
 
 	/*
 	 * To avoid drift caused by repeated suspend/resumes,
@@ -153,6 +158,18 @@ static int rtc_resume(struct device *dev)
 	if (timekeeping_rtc_skipresume())
 		return 0;
 
+	/*
+	 * rtc_suspend() takes no snapshot when the persistent clock is in
+	 * charge of the sleep time or when the RTC could not be read. The
+	 * timekeeping core still asks for sleep time if the persistent clock
+	 * did not advance, which happens when the system was suspended for
+	 * less than a second. Without a snapshot there is nothing to compute
+	 * the sleep time from. Using the stale old_rtc and old_system would
+	 * inject the offset between the RTC and system time.
+	 */
+	if (!old_rtc_valid)
+		return 0;
+
 	rtc_hctosys_ret = -ENODEV;
 	if (strcmp(dev_name(&rtc->dev), CONFIG_RTC_HCTOSYS_DEVICE) != 0)
 		return 0;

base-commit: e767a4ea70a3992c37ed604157d32f0dfbf9b1e3
-- 
2.55.0


             reply	other threads:[~2026-10-03 16:32 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-03 16:31 Alperen Kılıç [this message]
2026-10-03 17:56 ` [PATCH] rtc: class: Do not inject sleep time without a suspend snapshot sashiko-bot

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=20261003163138.14221-1-sabri.alperen03@gmail.com \
    --to=sabri.alperen03@gmail.com \
    --cc=alexandre.belloni@bootlin.com \
    --cc=jstultz@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-rtc@vger.kernel.org \
    --cc=mlichvar@redhat.com \
    --cc=sboyd@kernel.org \
    --cc=stable@vger.kernel.org \
    --cc=tglx@kernel.org \
    /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