timer: clamp future calendar base after clock jumps back

A calendar timer that is already waiting keeps the base timestamp it was
last armed from. When the wall clock is set backwards, that base can end
up in the future relative to the new realtime, and passing it to
calendar_spec_next_usec() schedules the next elapse relative to the old
future time instead of recalculating from now. systemctl list-timers
then keeps showing the stale pre-adjustment elapse (e.g. "3 years left"
after the clock moved back two years) and the timer never catches up.

Clamp the selected calendar base to the current realtime before asking
calendar_spec_next_usec() for the next occurrence. The clamp only kicks
in when the base is genuinely in the future, so Persistent=yes timers
whose last trigger is still in the past keep their catch-up behaviour and
we don't reintroduce the missed-run regression seen after suspend.

Fixes #6036.
This commit is contained in:
Ananth
2026-07-24 01:54:40 +09:00
committed by Yu Watanabe
parent af4c5730e5
commit 34c60f113e
2 changed files with 131 additions and 0 deletions
+15
View File
@@ -431,6 +431,21 @@ static void timer_enter_waiting(Timer *t, bool time_change) {
rebase_after_boot_time = true;
}
if (b > ts.realtime) {
/* The base we picked is in the future relative to the current realtime. This
* happens when the wall clock is set backwards while the timer is already
* waiting: the base was recorded from the old, later time. Feeding a future
* base to calendar_spec_next_usec() would schedule the next elapse relative to
* that stale time instead of now, so systemctl list-timers keeps showing the
* pre-adjustment elapse and the timer never catches up (see #6036). Clamp to
* now (minus the random offset, matching the "start from now" branch above) so
* we recalculate from the current time. */
log_unit_debug(UNIT(t),
"Calendar timer base time %s is in the future, recalculating from the current time.",
FORMAT_TIMESTAMP(b));
b = ts.realtime - random_offset;
}
r = calendar_spec_next_usec(v->calendar_spec, b, &v->next_elapse);
if (r < 0)
continue;
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
# Provides coverage for:
# - https://github.com/systemd/systemd/issues/6036
# - https://github.com/systemd/systemd/issues/24984
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
WAITING_UNIT="timer-clock-change-waiting-$RANDOM"
MISSED_UNIT="timer-clock-change-missed-$RANDOM"
MISSED_MARKER="/tmp/$MISSED_UNIT.ran"
MISSED_STAMP="/var/lib/systemd/timers/stamp-$MISSED_UNIT.timer"
START_REALTIME="$(date "+%s")"
START_MONOTONIC="$(cut -d . -f 1 /proc/uptime)"
at_exit() {
set +e
systemctl stop \
"$WAITING_UNIT.timer" \
"$WAITING_UNIT.service" \
"$MISSED_UNIT.timer" \
"$MISSED_UNIT.service"
systemctl clean --what=state "$MISSED_UNIT.timer"
rm -f \
"/run/systemd/system/$MISSED_UNIT.timer" \
"/run/systemd/system/$MISSED_UNIT.service" \
"$MISSED_MARKER" \
"$MISSED_STAMP"
systemctl daemon-reload
END_MONOTONIC="$(cut -d . -f 1 /proc/uptime)"
date --set="@$((START_REALTIME + END_MONOTONIC - START_MONOTONIC))"
}
trap at_exit EXIT
timer_delta_s() {
local next_elapse next_elapse_s now
next_elapse="$(systemctl show -P NextElapseUSecRealtime "$1")"
next_elapse_s="$(date --date="$next_elapse" "+%s")"
now="$(date "+%s")"
TIMER_NEXT_ELAPSE="$next_elapse"
TIMER_NOW="$now"
TIMER_DELTA_S="$((next_elapse_s - now))"
}
assert_timer_due_within() {
local ok=0 unit="${1:?}" max_delta_s="${2:?}"
for _ in {1..50}; do
timer_delta_s "$unit"
if ((TIMER_DELTA_S >= 0 && TIMER_DELTA_S <= max_delta_s)); then
ok=1
break
fi
sleep .2
done
if ((ok == 0)); then
echo "Timer elapse outside of the expected window."
echo " unit=$unit"
echo " next_elapse=$TIMER_NEXT_ELAPSE"
echo " now=$TIMER_NOW"
echo " time_delta=$TIMER_DELTA_S"
exit 1
fi
}
: "A waiting calendar timer is recalculated after the clock is set backwards"
date --set="+3 days"
systemd-run --unit "$WAITING_UNIT" --on-calendar "*:0/15:0" true
systemctl status "$WAITING_UNIT.timer"
date --set="-3 days"
assert_timer_due_within "$WAITING_UNIT.timer" 1200
systemctl stop "$WAITING_UNIT.timer" "$WAITING_UNIT.service"
: "A persistent calendar timer still catches up after time advances past a missed elapse"
date --set="19:00:00"
cat >"/run/systemd/system/$MISSED_UNIT.timer" <<EOF
[Timer]
OnCalendar=*-*-* 20:30:00
Persistent=true
AccuracySec=1ms
EOF
cat >"/run/systemd/system/$MISSED_UNIT.service" <<EOF
[Service]
Type=oneshot
ExecStart=touch $MISSED_MARKER
EOF
systemctl daemon-reload
mkdir -p "$(dirname "$MISSED_STAMP")"
touch -d "yesterday 20:30:00" "$MISSED_STAMP"
systemctl start "$MISSED_UNIT.timer"
systemctl status "$MISSED_UNIT.timer"
test ! -e "$MISSED_MARKER"
date --set="tomorrow 09:00:00"
timeout 30 bash -xec "until [[ -e '$MISSED_MARKER' ]]; do sleep .5; done"