Bug 2543517 - With RTC in local time, clock is off by the UTC offset after s2idle resume and chronyd slews it for ~36 h, making the desktop laggy
Summary: With RTC in local time, clock is off by the UTC offset after s2idle resume an...
Keywords:
Status: NEW
Alias: None
Product: Fedora
Classification: Fedora
Component: kernel
Version: 44
Hardware: x86_64
OS: Linux
unspecified
medium
Target Milestone: ---
Assignee: Justin M. Forbes
QA Contact: Fedora Extras Quality Assurance
URL: https://github.com/EOLO03/kde-suspend...
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-09-29 19:09 UTC by Alperen Kilic
Modified: 2026-10-05 07:21 UTC (History)
15 users (show)

Fixed In Version:
Clone Of:
Environment:
Last Closed:
Type: ---
Embargoed:


Attachments (Terms of Use)
Raw measurements from the affected machine (3.68 KB, text/plain)
2026-09-29 19:15 UTC, Alperen Kilic
no flags Details
systemd-sleep hook log: system time, raw RTC and boottime before/after each suspend (46.46 KB, text/plain)
2026-10-03 14:35 UTC, Alperen Kilic
no flags Details

Description Alperen Kilic 2026-09-29 19:09:06 UTC
On a dual-boot machine with the RTC kept in local time (timedatectl set-local-rtc 1, the usual Windows dual-boot setup), the system clock is sometimes off by exactly the UTC offset (3 hours here) after resuming from s2idle. chronyd detects the offset. Because the default Fedora config only allows stepping during the first 3 updates (makestep 1.0 3), chronyd slews the clock instead.

The slew slows the kernel clock by about 8.3% (adjtimex tick = 9167 instead of 10000). CLOCK_MONOTONIC then runs at 0.917x real speed for about 36 hours, until the offset is corrected. Everything timed against CLOCK_MONOTONIC runs slow in that period. The most visible effect is that the KDE Plasma (KWin, Wayland) desktop becomes slow and choppy: animations, window moves and the cursor are all affected. A reboot fixes it because chronyd is allowed to step the clock again at startup.

A second, unrelated laptop (Huawei, 13th-gen Intel, no dGPU) on Fedora 44 KDE shows the same symptom.

Versions:
chrony-4.9-1.fc44.x86_64
systemd-259.9-1.fc44.x86_64
kernel-7.2.7-200.fc44.x86_64
kwin / plasma-workspace 6.7.5-1.fc44

Reproducible: Sometimes

Steps to Reproduce:
1. Keep the RTC in local time with a non-UTC timezone (here Asia/Nicosia, UTC+3): sudo timedatectl set-local-rtc 1
2. Boot, wait for chronyd to synchronise, then suspend (s2idle) and resume, repeatedly and with longer sleeps.
3. After a bad resume, run: journalctl | grep "System clock wrong by" and chronyc tracking
Actual Results:
Journal after resume:
  14:45:04 systemd-logind: Lid opened.
  14:45:05 kernel: PM: suspend exit
  14:45:15 chronyd[1511]: System clock wrong by -10799.997000 seconds

chronyc tracking a few minutes later:
  System time : 10681.260742188 seconds fast of NTP time

adjtimex() shows tick 9167. CLOCK_MONOTONIC / CLOCK_MONOTONIC_RAW measured over 5 s = 0.91668.

The desktop stays laggy until "sudo chronyc makestep" or a reboot. After makestep the ratio returns to 1.00001 and the lag disappears immediately.

Expected Results:
The clock should not be off by the UTC offset after resume. If an offset of several hours does appear after resume, chronyd should step it immediately instead of slewing it over ~36 hours while the whole system runs 8% slow.

Additional Information:
- The offset is exactly the UTC offset (10800 s). This suggests the local-time RTC is being read as UTC somewhere on the resume path, or local time is being written to the RTC while the kernel assumes UTC. I could not confirm which. If this belongs to the kernel (timekeeping / rtc-cmos) rather than chrony, please reassign.
- Workarounds: "sudo timedatectl set-local-rtc 0" plus RealTimeIsUniversal=1 on Windows, or "makestep 1.0 -1" in /etc/chrony.conf.
- Possible improvements: have chronyd step large offsets that appear right after resume; document the local-RTC interaction in the Fedora chrony.conf comments.
- Ruled out while the desktop was lagging: CPU/GPU load, thermal/PROCHOT throttling, i915 errors, PSR (not supported by the panel), timer latency on all CPUs (median < 0.2 ms), clocksource (tsc), KWin renderer (hardware OpenGL).
- Hardware: Lenovo ThinkBook 16p G5 (i9-14900HX, Intel UHD iGPU, NVIDIA dGPU disabled), mem_sleep = s2idle only.
- Full write-up, raw measurements and a read-only check script: https://github.com/EOLO03/kde-suspend-lag-rtc-fix
- Upstream discussion: https://discussion.fedoraproject.org/t/203245

Comment 1 Alperen Kilic 2026-09-29 19:15:55 UTC
Created attachment 2159072 [details]
Raw measurements from the affected machine

Attaching the raw measurements referenced in the description: timedatectl/chronyc/adjtimex output before and after the fix, and the list of ruled-out causes with measured values.

Comment 2 Alperen Kilic 2026-09-29 19:41:24 UTC
Correction: the second laptop runs GNOME, not KDE, and I also saw this earlier on my own machine under GNOME. So it's not KDE-specific.

Comment 3 Miroslav Lichvar 2026-09-30 09:57:31 UTC
The default chrony configuration doesn't allow steps after the first three update to prevent upsetting services and applications sensitive to clock steps.

The problem is with the RTC and/or the kernel. Either the RTC is not being set by the kernel (11-minute mode) at all or with an incorrect timezone offset, or the reads it back with an incorrect offset.

Please check that your /etc/chrony.conf includes the "rtcsync" directive.

Before suspending, please run timedatectl to verify if the RTC is set to the correct time.

Comment 4 Alperen Kilic 2026-09-30 11:41:17 UTC
Thanks for the explanation.

1. Yes, /etc/chrony.conf includes "rtcsync" (unmodified Fedora default).

2. I had switched to RTC in UTC as a workaround, so I've now switched back to local time (timedatectl set-local-rtc 1) to reproduce. Right after switching, timedatectl shows the RTC set correctly:

   Local time: Wed 2026-09-30 14:38:33 EEST
   Universal time: Wed 2026-09-30 11:38:33 UTC
   RTC time: Wed 2026-09-30 14:38:33
   RTC in local TZ: yes

3. I installed a systemd-sleep hook that logs, right before suspend and right after resume: system time (UTC and local), the raw RTC (/sys/class/rtc/rtc0/date, time, since_epoch), chronyc tracking and the adjtimex tick/status. I'll post the log after a few suspend/resume cycles, including at least one long sleep.

One thing I noticed: with RTC in local time, timedatectl warns "The RTC time is never updated, it relies on external facilities to maintain it", and earlier adjtimex showed status 0x40 (STA_UNSYNC). Could this mean the kernel 11-minute mode doesn't update the RTC in this configuration?

Comment 5 Miroslav Lichvar 2026-09-30 12:02:19 UTC
It seems timedated always logs that message when the RTC is in local time. adjtimex should not have 0x40 (STA_UNSYNC) when chronyd is running and updating the clock with the rtcsync directive.

Comment 6 Alperen Kilic 2026-09-30 12:07:04 UTC
Thanks. To clarify when I saw STA_UNSYNC: it was during the bad state, about 30 minutes after the resume, while chronyd was running with rtcsync and slewing the ~10681 s offset. At that time:

  adjtimex: status 0x40, tick 9167
  timedatectl: System clock synchronized: no
  chronyc tracking: Reference ID A29FC801 (time.cloudflare.com), Stratum 4, System time 10681.26 seconds fast of NTP time, Leap status Normal

Right now, with the RTC back in local time and no suspend since, everything looks normal: status 0x0, tick 10000, "System clock synchronized: yes".

The sleep hook also records the adjtimex status right before suspend and right after resume, so the next log will show whether STA_UNSYNC was already set before suspending or only appeared after resume.

Comment 7 Miroslav Lichvar 2026-09-30 12:47:39 UTC
Ok, with a large uncorrected offset it is expected to see the STA_UNSYNC flag. The maximum value of the adjtimex maxerror is 16 seconds and with chronyd that includes the uncorrected offset.

Comment 8 Alperen Kilic 2026-10-01 18:06:38 UTC
Reproduced on Oct 1 with RTC in local time. My sleep hook did not run (I had put it in /etc/systemd/system-sleep instead of /usr/lib/systemd/system-sleep), so I don't have the pre-suspend RTC reading yet. It is fixed now and I'll post that log next. Here is what the journal and the state after resume show.

Timeline (real times derived from NTP; lid closed the whole time):

  20:21:32  suspend entry (s2idle)
  20:43:35  resume, clock OK            "PM: suspend devices took 0.115 seconds"
  20:43:47  suspend entry again (KDE re-suspends 11 s later, lid still closed)
  ~20:48:29 resume, journal shows 23:48:29 -> clock jumped +3 h here
            "PM: suspend devices took 1.568 seconds", "PM: resume devices took 2.292 seconds"
  23:48:39  suspend entry
  23:59:47  resume
  00:00:13  chronyd: Forward time jump detected!
  00:00:18  chronyd: System clock wrong by -10799.704534 seconds

State about one minute later:

  Local time: Fri 2026-10-02 00:01:05 EEST
  Universal time: Thu 2026-10-01 21:01:05 UTC
  RTC time: Thu 2026-10-01 21:01:09
  RTC in local TZ: yes
  /sys/class/rtc/rtc0: 2026-10-01 21:01:09
  chronyc tracking: System time 10795.439 seconds fast of NTP time
  adjtimex: status 0x2040, tick 9167
  /etc/adjtime: LOCAL

The real local time at that moment was 21:01, so the RTC held the correct local time, and the system clock was equal to the RTC value interpreted as UTC. So the RTC itself looks fine; the system time after that resume is what the RTC would give if read without the local-time offset.

The first resume (22 min sleep) was fine; the jump happened on the resume that followed a re-suspend 11 s after the previous resume. The bad cycle also has much longer device suspend/resume times (1.57 s / 2.29 s vs 0.115 s).

Other details that may matter:
- At boot the kernel does the usual: "rtc_cmos rtc_cmos: setting system clock to ... UTC", then systemd "RTC configured in localtime, applying delta of 180 minutes to system time".
- Kernel 7.2.7 also registers a second RTC: "acpi-tad ACPI000E:00: registered as rtc1". /sys/bus/platform/devices/ACPI000E:00/time reads 2026:10:1:21:4:26:2047:0 (same local time, TZ field 2047 = unspecified).
- Clocksource is tsc. Boot log has "[Firmware Bug]: TSC ADJUST differs within socket(s), fixing all errors".

Side observation, possibly a separate issue: on Sep 30 I ran "timedatectl set-local-rtc 1" at runtime (RTC was correct local time right after). After about 7 hours of uptime I rebooted, and at that boot the RTC held UTC ("PM: RTC time: 18:15:18" while local time was 21:15), so systemd's 180-minute delta made the clock 3 h slow and chronyd logged "System clock wrong by 10800.307774 seconds" and stepped it.

Given that the RTC is correct and the jump happens inside the kernel resume path, should this be reassigned to the kernel?

Comment 9 Alperen Kilic 2026-10-03 14:35:29 UTC
Created attachment 2159642 [details]
systemd-sleep hook log: system time, raw RTC and boottime before/after each suspend

Comment 10 Alperen Kilic 2026-10-03 14:35:54 UTC
I now have before/after data from the sleep hook for two occurrences (Oct 1 on kernel 7.2.7, Oct 3 on 7.2.8). The full log is attached (35 suspend cycles).

Columns: system time (UTC), raw rtc0 time, their difference, and the change across the suspend in system time, RTC time and CLOCK_BOOTTIME (/proc/uptime), all in seconds.

  phase  system UTC           rtc0 raw             rtc-sys   d_sys   d_rtc  d_boottime
  pre    2026-10-01 19:00:38  2026-10-01 22:00:37   10799
  post   2026-10-01 19:09:34  2026-10-01 22:09:33   10799      536     536     536.0
  pre    2026-10-01 19:09:44  2026-10-01 22:09:43   10799
  post   2026-10-01 23:29:56  2026-10-01 23:29:56       0    15612    4813   15611.9   <- jump
  pre    2026-10-01 23:30:07  2026-10-01 23:30:07       0
  post   2026-10-02 00:58:35  2026-10-02 00:58:35       0     5308    5308    5308.6
  ...
  pre    2026-10-03 13:22:59  2026-10-03 16:22:59   10800
  post   2026-10-03 13:23:06  2026-10-03 16:23:05   10799        7       6       6.4
  pre    2026-10-03 13:23:16  2026-10-03 16:23:16   10800
  post   2026-10-03 16:23:20  2026-10-03 16:23:21       1    10804       5   10803.8   <- jump
  pre    2026-10-03 16:23:31  2026-10-03 16:23:32       1
  post   2026-10-03 16:23:38  2026-10-03 16:23:39       1        7       7       7.2

What this shows:

1. Before the bad suspend the RTC is correct: it holds local time, 10800 s ahead of system UTC.
2. The RTC is not changed across the suspend; it advances by the real sleep time (4813 s and 5 s).
3. System time AND CLOCK_BOOTTIME both advance by the sleep time plus 10800 s. So the extra 3 h was injected by the kernel as sleep time; it was not set from userspace.
4. Afterwards system UTC equals the raw RTC value, and no further jump is visible while they are equal.

It happened in 2 of 35 logged cycles (9 of those were while the clock was already shifted, where it could not show). Before the Oct 3 jump the machine was waking and re-suspending every ~17 s with the lid closed.

This looks like rtc_resume() in drivers/rtc/class.c injecting (RTC - system time) as sleep time. As far as I can tell from the source:

- On x86 rtc_suspend() returns early because timekeeping_rtc_skipsuspend() is true (persistent clock exists), so old_rtc and old_system stay zero.
- In s2idle, timekeeping_suspend()/timekeeping_resume() run for each wakeup inside the s2idle loop. If the last iteration is shorter than the 1 s resolution of the CMOS clock, timekeeping_resume() injects nothing (no nonstop clocksource, persistent clock not strictly greater), so suspend_timing_needed stays true.
- rtc_resume() is then not skipped and computes (new_rtc - old_rtc) - (new_system - old_system) with the old values at zero, i.e. new_rtc - new_system, and injects it when it is >= 0.

With the RTC in UTC that value is about zero and invisible. With the RTC in local time east of UTC it is the UTC offset. If this reading is right, it would not happen west of UTC (negative value, not injected). I have not confirmed it with tracing, but it matches all three numbers above.

So this does not look like a chrony problem. Could you reassign it to the kernel, or should I file a new bug there?

Comment 11 Alperen Kilic 2026-10-03 16:57:25 UTC
Following up on the earlier comment: the jump is reproducible on demand now, the cause is in the kernel, and a patch is on the list.

Patch: https://lore.kernel.org/all/20261003163138.14221-1-sabri.alperen03@gmail.com/
("rtc: class: Do not inject sleep time without a suspend snapshot", sent to linux-rtc with the timekeeping maintainers in Cc)

Cause, in short:

- rtc_suspend() returns early when timekeeping_rtc_skipsuspend() is true, i.e. whenever a persistent clock exists. On x86 that is always, so old_rtc and old_system stay zero.
- rtc_resume() is skipped under a different condition, timekeeping_rtc_skipresume(), which is only true after timekeeping_resume() has injected sleep time.
- With s2idle, timekeeping_suspend()/timekeeping_resume() run for every pass through the idle loop. When the last pass freezes timekeeping for less than a second, the CMOS persistent clock has not advanced past timekeeping_suspend_time, nothing is injected, and rtc_resume() runs.
- With the old values being zero it injects new_rtc - new_system. With the RTC in UTC that is about zero. With the RTC in local time east of UTC it is the UTC offset.

Reproducer: the script stops chronyd, shifts the system clock by less than a second relative to the RTC, suspends to idle with a wake alarm 3 to 6 seconds ahead, and puts a kprobe with a stack trace on timekeeping_inject_sleeptime64(). 8 control cycles without the shift, then 30 test cycles. Same machine as before (ThinkBook 16p G5, i9-14900HX, s2idle only, RTC in local time, UTC+3).

  kernel                 control jumps   test jumps
  7.2.8-200.fc44         0 of 8          7 of 30
  7.3.0-rc5 unpatched    0 of 8          4 of 30
  7.3.0-rc5 patched      0 of 8          0 of 30

Every jump is 10797 to 10799 seconds and the kprobe stack is always

  timekeeping_inject_sleeptime64
  rtc_resume
  dpm_run_callback
  device_resume
  dpm_resume
  dpm_resume_end
  suspend_devices_and_enter

The trace clock was set to x86-tsc, which keeps counting in s2idle, so the trace shows how long timekeeping was really frozen. In every cycle that jumped, the longest freeze was below 0.9 s. In every cycle that did not jump, there was either a freeze of 0.99 s or more, or no freeze at all. On the patched kernel 6 cycles had only sub-0.9 s freezes and none of them jumped.

The script, the three logs and the traces are at
https://github.com/EOLO03/kde-suspend-lag-rtc-fix/tree/main/kernel-repro

So chrony is not at fault here. It only reacts to a clock that the kernel moved. Could the component be changed to kernel?

Comment 12 Miroslav Lichvar 2026-10-05 07:21:48 UTC
Thanks for the detailed report and upstream patch submission.

I'm reassigning this bug to the kernel component.


Note You need to log in before you can comment on or make changes to this bug.