From a8b66ca9af811148b67ee952ab32748f88b8bba3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= <zbyszek@in.waw.pl>
Date: Mon, 22 Mar 2021 12:51:47 +0100
Subject: [PATCH] shared/calendarspec: when mktime() moves us backwards, jump
 forward
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When trying to calculate the next firing of 'Sun *-*-* 01:00:00', we'd fall
into an infinite loop, because mktime() moves us "backwards":

Before this patch:
tm_within_bounds: good=0 2021-03-29 01:00:00 → 2021-03-29 00:00:00
tm_within_bounds: good=0 2021-03-29 01:00:00 → 2021-03-29 00:00:00
tm_within_bounds: good=0 2021-03-29 01:00:00 → 2021-03-29 00:00:00
...

We rely on mktime() normalizing the time. The man page does not say that it'll
move the time forward, but our algorithm relies on this. So let's catch this
case explicitly.

With this patch:
$ TZ=Europe/Dublin faketime 2021-03-21 build/systemd-analyze calendar --iterations=5 'Sun *-*-* 01:00:00'
Normalized form: Sun *-*-* 01:00:00
    Next elapse: Sun 2021-03-21 01:00:00 GMT
       (in UTC): Sun 2021-03-21 01:00:00 UTC
       From now: 59min left
       Iter. #2: Sun 2021-04-04 01:00:00 IST
       (in UTC): Sun 2021-04-04 00:00:00 UTC
       From now: 1 weeks 6 days left           <---- note the 2 week jump here
       Iter. #3: Sun 2021-04-11 01:00:00 IST
       (in UTC): Sun 2021-04-11 00:00:00 UTC
       From now: 2 weeks 6 days left
       Iter. #4: Sun 2021-04-18 01:00:00 IST
       (in UTC): Sun 2021-04-18 00:00:00 UTC
       From now: 3 weeks 6 days left
       Iter. #5: Sun 2021-04-25 01:00:00 IST
       (in UTC): Sun 2021-04-25 00:00:00 UTC
       From now: 1 months 4 days left

Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1941335.

(cherry picked from commit 129cb6e249bef30dc33e08f98f0b27a6de976f6f)
(cherry picked from commit e5cf86ff98a21b427e1439a001d8e6b81c07b19c)
---
 src/shared/calendarspec.c    | 19 +++++++++++--------
 src/test/test-calendarspec.c |  3 +++
 test/test-functions          |  1 +
 3 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/src/shared/calendarspec.c b/src/shared/calendarspec.c
index d83e0fbded..48041bdfa1 100644
--- a/src/shared/calendarspec.c
+++ b/src/shared/calendarspec.c
@@ -1185,15 +1185,18 @@ static int tm_within_bounds(struct tm *tm, bool utc) {
                 return negative_errno();
 
         /* Did any normalization take place? If so, it was out of bounds before */
-        bool good = t.tm_year == tm->tm_year &&
-                    t.tm_mon  == tm->tm_mon  &&
-                    t.tm_mday == tm->tm_mday &&
-                    t.tm_hour == tm->tm_hour &&
-                    t.tm_min  == tm->tm_min  &&
-                    t.tm_sec  == tm->tm_sec;
-        if (!good)
+        int cmp = CMP(t.tm_year, tm->tm_year) ?:
+                  CMP(t.tm_mon, tm->tm_mon) ?:
+                  CMP(t.tm_mday, tm->tm_mday) ?:
+                  CMP(t.tm_hour, tm->tm_hour) ?:
+                  CMP(t.tm_min, tm->tm_min) ?:
+                  CMP(t.tm_sec, tm->tm_sec);
+
+        if (cmp < 0)
+                return -EDEADLK; /* Refuse to go backward */
+        if (cmp > 0)
                 *tm = t;
-        return good;
+        return cmp == 0;
 }
 
 static bool matches_weekday(int weekdays_bits, const struct tm *tm, bool utc) {
diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c
index 9e2ae55ab6..080aa1f012 100644
--- a/src/test/test-calendarspec.c
+++ b/src/test/test-calendarspec.c
@@ -218,6 +218,9 @@ int main(int argc, char* argv[]) {
         // Confirm that timezones in the Spec work regardless of current timezone
         test_next("2017-09-09 20:42:00 Pacific/Auckland", "", 12345, 1504946520000000);
         test_next("2017-09-09 20:42:00 Pacific/Auckland", "EET", 12345, 1504946520000000);
+        /* Check that we don't start looping if mktime() moves us backwards */
+        test_next("Sun *-*-* 01:00:00 Europe/Dublin", "", 1616412478000000, 1617494400000000);
+        test_next("Sun *-*-* 01:00:00 Europe/Dublin", "IST", 1616412478000000, 1617494400000000);
 
         assert_se(calendar_spec_from_string("test", &c) < 0);
         assert_se(calendar_spec_from_string(" utc", &c) < 0);
diff --git a/test/test-functions b/test/test-functions
index 984643c37f..cc817925f0 100644
--- a/test/test-functions
+++ b/test/test-functions
@@ -1121,6 +1121,7 @@ install_zoneinfo() {
     inst_any /usr/share/zoneinfo/Asia/Vladivostok
     inst_any /usr/share/zoneinfo/Australia/Sydney
     inst_any /usr/share/zoneinfo/Europe/Berlin
+    inst_any /usr/share/zoneinfo/Europe/Dublin
     inst_any /usr/share/zoneinfo/Europe/Kiev
     inst_any /usr/share/zoneinfo/Pacific/Auckland
     inst_any /usr/share/zoneinfo/Pacific/Honolulu