From: Ananth Date: Wed, 22 Jul 2026 17:49:34 +0000 (+0000) Subject: timer: clamp future calendar base after clock jumps back X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=34c60f113e78db7554c5521e856265feb53f1096;p=thirdparty%2Fsystemd.git 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. --- diff --git a/src/core/timer.c b/src/core/timer.c index 5716c4f81d5..70bcb56b83d 100644 --- a/src/core/timer.c +++ b/src/core/timer.c @@ -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; diff --git a/test/units/TEST-53-TIMER.clock-change.sh b/test/units/TEST-53-TIMER.clock-change.sh new file mode 100755 index 00000000000..9a79494c134 --- /dev/null +++ b/test/units/TEST-53-TIMER.clock-change.sh @@ -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" <"/run/systemd/system/$MISSED_UNIT.service" <