From 169615c9a8cdc54d748d4dfc8279be9b3c2bec44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sun, 21 Mar 2021 20:59:32 +0100 Subject: [PATCH 1/5] shared/calendarspec: abort calculation after 1000 iterations We have a bug where we seem to enter an infinite loop when running in the Europe/Dublin timezone. The timezone is "special" because it has negative SAVE values. The handling of this should obviously be fixed, but let's use a belt-and-suspenders approach, and gracefully fail if we fail to find an answer within a specific number of attempts. The code in this function is rather complex, and it's hard to rule out another bug in the future. --- src/shared/calendarspec.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/shared/calendarspec.c b/src/shared/calendarspec.c index 4f68a570b52..feb43efdcda 100644 --- a/src/shared/calendarspec.c +++ b/src/shared/calendarspec.c @@ -1210,6 +1210,10 @@ static bool matches_weekday(int weekdays_bits, const struct tm *tm, bool utc) { return (weekdays_bits & (1 << k)); } +/* A safety valve: if we get stuck in the calculation, return an error. + * C.f. https://bugzilla.redhat.com/show_bug.cgi?id=1941335. */ +#define MAX_CALENDAR_ITERATIONS 1000 + static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) { struct tm c; int tm_usec; @@ -1223,7 +1227,7 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) { c = *tm; tm_usec = *usec; - for (;;) { + for (unsigned iteration = 0; iteration < MAX_CALENDAR_ITERATIONS; iteration++) { /* Normalize the current date */ (void) mktime_or_timegm(&c, spec->utc); c.tm_isdst = spec->dst; @@ -1320,6 +1324,14 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) { *usec = tm_usec; return 0; } + + /* It seems we entered an infinite loop. Let's gracefully return an error instead of hanging or + * aborting. This code is also exercised when timers.target is brought up during early boot, so + * aborting here is problematic and hard to diagnose for users. */ + _cleanup_free_ char *s = NULL; + (void) calendar_spec_to_string(spec, &s); + return log_warning_errno(SYNTHETIC_ERRNO(EDEADLK), + "Infinite loop in calendar calculation: %s", strna(s)); } static int calendar_spec_next_usec_impl(const CalendarSpec *spec, usec_t usec, usec_t *ret_next) { From 462f15d92d35f812d7d77edd486ca63236cffe83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 22 Mar 2021 09:20:47 +0100 Subject: [PATCH 2/5] shared/calendarspec: constify parameter and simplify assignments to variable The scope of start & stop is narrowed down, and they are assigned only once. No functional change, but I think the code is easier to read this way. Also add a comment to make the code easier to read. --- src/shared/calendarspec.c | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/shared/calendarspec.c b/src/shared/calendarspec.c index feb43efdcda..5c666412946 100644 --- a/src/shared/calendarspec.c +++ b/src/shared/calendarspec.c @@ -1101,7 +1101,7 @@ int calendar_spec_from_string(const char *p, CalendarSpec **spec) { return 0; } -static int find_end_of_month(struct tm *tm, bool utc, int day) { +static int find_end_of_month(const struct tm *tm, bool utc, int day) { struct tm t = *tm; t.tm_mon++; @@ -1114,28 +1114,39 @@ static int find_end_of_month(struct tm *tm, bool utc, int day) { return t.tm_mday; } -static int find_matching_component(const CalendarSpec *spec, const CalendarComponent *c, - struct tm *tm, int *val) { - const CalendarComponent *p = c; - int start, stop, d = -1; +static int find_matching_component( + const CalendarSpec *spec, + const CalendarComponent *c, + const struct tm *tm, /* tm is only used for end-of-month calculations */ + int *val) { + + int d = -1, r; bool d_set = false; - int r; assert(val); + /* Finds the *earliest* matching time specified by one of the CalendarCompoment items in chain c. + * If no matches can be found, returns -ENOENT. + * Otherwise, updates *val to the matching time. 1 is returned if *val was changed, 0 otherwise. + */ + if (!c) return 0; + bool end_of_month = spec->end_of_month && c == spec->day; + while (c) { - start = c->start; - stop = c->stop; + int start, stop; - if (spec->end_of_month && p == spec->day) { - start = find_end_of_month(tm, spec->utc, start); - stop = find_end_of_month(tm, spec->utc, stop); + if (end_of_month) { + start = find_end_of_month(tm, spec->utc, c->start); + stop = find_end_of_month(tm, spec->utc, c->stop); if (stop > 0) SWAP_TWO(start, stop); + } else { + start = c->start; + stop = c->stop; } if (start >= *val) { From f035bb1b7a5900439640f267db881c60d042e450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 22 Mar 2021 11:10:22 +0100 Subject: [PATCH 3/5] test-calendarspec: print offending line in output The output is rather long at this makes it easier to jump to the right place. Also use normal output routines and set_unset_env() to make things more compact. --- src/test/test-calendarspec.c | 48 +++++++++++++++++------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c index 01ec7f87704..152ce879f8a 100644 --- a/src/test/test-calendarspec.c +++ b/src/test/test-calendarspec.c @@ -2,11 +2,11 @@ #include "alloc-util.h" #include "calendarspec.h" +#include "env-util.h" #include "errno-util.h" #include "string-util.h" -#include "util.h" -static void test_one(const char *input, const char *output) { +static void _test_one(int line, const char *input, const char *output) { CalendarSpec *c; _cleanup_free_ char *p = NULL, *q = NULL; usec_t u; @@ -16,13 +16,13 @@ static void test_one(const char *input, const char *output) { assert_se(calendar_spec_from_string(input, &c) >= 0); assert_se(calendar_spec_to_string(c, &p) >= 0); - printf("\"%s\" → \"%s\"\n", input, p); + log_info("line %d: \"%s\" → \"%s\"", line, input, p); assert_se(streq(p, output)); u = now(CLOCK_REALTIME); r = calendar_spec_next_usec(c, u, &u); - printf("Next: %s\n", r < 0 ? strerror_safe(r) : format_timestamp(buf, sizeof(buf), u)); + log_info("Next: %s", r < 0 ? strerror_safe(r) : format_timestamp(buf, sizeof buf, u)); calendar_spec_free(c); assert_se(calendar_spec_from_string(p, &c) >= 0); @@ -31,8 +31,9 @@ static void test_one(const char *input, const char *output) { assert_se(streq(q, p)); } +#define test_one(input, output) _test_one(__LINE__, input, output) -static void test_next(const char *input, const char *new_tz, usec_t after, usec_t expect) { +static void _test_next(int line, const char *input, const char *new_tz, usec_t after, usec_t expect) { CalendarSpec *c; usec_t u; char *old_tz; @@ -43,22 +44,19 @@ static void test_next(const char *input, const char *new_tz, usec_t after, usec_ if (old_tz) old_tz = strdupa(old_tz); - if (new_tz) { - char *colon_tz; + if (new_tz) + new_tz = strjoina(":", new_tz); - colon_tz = strjoina(":", new_tz); - assert_se(setenv("TZ", colon_tz, 1) >= 0); - } else - assert_se(unsetenv("TZ") >= 0); + assert_se(set_unset_env("TZ", new_tz, true) == 0); tzset(); assert_se(calendar_spec_from_string(input, &c) >= 0); - printf("\"%s\"\n", input); + log_info("line %d: \"%s\" new_tz=%s", line, input, strnull(new_tz)); u = after; r = calendar_spec_next_usec(c, after, &u); - printf("At: %s\n", r < 0 ? strerror_safe(r) : format_timestamp_style(buf, sizeof buf, u, TIMESTAMP_US)); + log_info("At: %s", r < 0 ? strerror_safe(r) : format_timestamp_style(buf, sizeof buf, u, TIMESTAMP_US)); if (expect != USEC_INFINITY) assert_se(r >= 0 && u == expect); else @@ -66,12 +64,10 @@ static void test_next(const char *input, const char *new_tz, usec_t after, usec_ calendar_spec_free(c); - if (old_tz) - assert_se(setenv("TZ", old_tz, 1) >= 0); - else - assert_se(unsetenv("TZ") >= 0); + assert_se(set_unset_env("TZ", old_tz, true) == 0); tzset(); } +#define test_next(input, new_tz, after, expect) _test_next(__LINE__, input,new_tz,after,expect) static void test_timestamp(void) { char buf[FORMAT_TIMESTAMP_MAX]; @@ -83,12 +79,12 @@ static void test_timestamp(void) { x = now(CLOCK_REALTIME); - assert_se(format_timestamp_style(buf, sizeof(buf), x, TIMESTAMP_US)); - printf("%s\n", buf); + assert_se(format_timestamp_style(buf, sizeof buf, x, TIMESTAMP_US)); + log_info("%s", buf); assert_se(calendar_spec_from_string(buf, &c) >= 0); assert_se(calendar_spec_to_string(c, &t) >= 0); calendar_spec_free(c); - printf("%s\n", t); + log_info("%s", t); assert_se(parse_timestamp(t, &y) >= 0); assert_se(y == x); @@ -104,11 +100,11 @@ static void test_hourly_bug_4031(void) { n = now(CLOCK_REALTIME); assert_se((r = calendar_spec_next_usec(c, n, &u)) >= 0); - printf("Now: %s (%"PRIu64")\n", format_timestamp_style(buf, sizeof buf, n, TIMESTAMP_US), n); - printf("Next hourly: %s (%"PRIu64")\n", r < 0 ? strerror_safe(r) : format_timestamp_style(buf, sizeof buf, u, TIMESTAMP_US), u); + log_info("Now: %s (%"PRIu64")", format_timestamp_style(buf, sizeof buf, n, TIMESTAMP_US), n); + log_info("Next hourly: %s (%"PRIu64")", r < 0 ? strerror_safe(r) : format_timestamp_style(buf, sizeof buf, u, TIMESTAMP_US), u); assert_se((r = calendar_spec_next_usec(c, u, &w)) >= 0); - printf("Next hourly: %s (%"PRIu64")\n", r < 0 ? strerror_safe(r) : format_timestamp_style(zaf, sizeof zaf, w, TIMESTAMP_US), w); + log_info("Next hourly: %s (%"PRIu64")", r < 0 ? strerror_safe(r) : format_timestamp_style(zaf, sizeof zaf, w, TIMESTAMP_US), w); assert_se(n < u); assert_se(u <= n + USEC_PER_HOUR); @@ -209,13 +205,13 @@ int main(int argc, char* argv[]) { test_next("2017-08-06 9..17/2:00 UTC", "", 1502029800000000, 1502031600000000); test_next("2016-12-* 3..21/6:00 UTC", "", 1482613200000001, 1482634800000000); test_next("2017-09-24 03:30:00 Pacific/Auckland", "", 12345, 1506177000000000); - // Due to daylight saving time - 2017-09-24 02:30:00 does not exist + /* Due to daylight saving time - 2017-09-24 02:30:00 does not exist */ test_next("2017-09-24 02:30:00 Pacific/Auckland", "", 12345, -1); test_next("2017-04-02 02:30:00 Pacific/Auckland", "", 12345, 1491053400000000); - // Confirm that even though it's a time change here (backward) 02:30 happens only once + /* Confirm that even though it's a time change here (backward) 02:30 happens only once */ test_next("2017-04-02 02:30:00 Pacific/Auckland", "", 1491053400000000, -1); test_next("2017-04-02 03:30:00 Pacific/Auckland", "", 12345, 1491060600000000); - // Confirm that timezones in the Spec work regardless of current timezone + /* 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); From 47b0b65766229a18921a3ce831ef708ef408a34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 22 Mar 2021 11:29:35 +0100 Subject: [PATCH 4/5] test-calendarspec: do not convert timezone "" to ":" I *think* it doesn't actually make any difference, because ":" will be ignored. 437f48a471f51ac9dd2697ee3b848a71b4f101df added prefixing with ":", but didn't take into account the fact that we also use "" with a different meaning than NULL here. But let's restore the original behaviour of specifying the empty string. --- src/test/test-calendarspec.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c index 152ce879f8a..c62e6860cf9 100644 --- a/src/test/test-calendarspec.c +++ b/src/test/test-calendarspec.c @@ -44,7 +44,7 @@ static void _test_next(int line, const char *input, const char *new_tz, usec_t a if (old_tz) old_tz = strdupa(old_tz); - if (new_tz) + if (!isempty(new_tz)) new_tz = strjoina(":", new_tz); assert_se(set_unset_env("TZ", new_tz, true) == 0); From 129cb6e249bef30dc33e08f98f0b27a6de976f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 22 Mar 2021 12:51:47 +0100 Subject: [PATCH 5/5] 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. --- 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 5c666412946..bf24d8d5bbb 100644 --- a/src/shared/calendarspec.c +++ b/src/shared/calendarspec.c @@ -1195,15 +1195,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 c62e6860cf9..4f1d0f64d57 100644 --- a/src/test/test-calendarspec.c +++ b/src/test/test-calendarspec.c @@ -214,6 +214,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 d7f7967e2ff..6b94058fd36 100644 --- a/test/test-functions +++ b/test/test-functions @@ -1340,6 +1340,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