diff --git a/src/core/timer.c b/src/core/timer.c index 5716c4f81d..70bcb56b83 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 0000000000..9a79494c13 --- /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" <