Skip to content

Commit 8c08ad7

Browse files
kmeawbluca
authored andcommitted
shared/calendarspec: fix normalization when DST is negative
When trying to calculate the next firing of 'hourly', we'd lose the tm_isdst value on the next iteration. On most systems in Europe/Dublin it would cause a 100% cpu hang due to timers restarting. This happens in Europe/Dublin because Ireland defines the Irish Standard Time as UTC+1, so winter time is encoded in tzdata as negative 1 hour of daylight saving. Before this patch: $ env TZ=IST-1GMT-0,M10.5.0/1,M3.5.0/1 systemd-analyze calendar --base-time='Sat 2025-03-29 22:00:00 UTC' --iterations=5 'hourly' Original form: hourly Normalized form: *-*-* *:00:00 Next elapse: Sat 2025-03-29 23:00:00 GMT (in UTC): Sat 2025-03-29 23:00:00 UTC From now: 13h ago Iteration #2: Sun 2025-03-30 00:00:00 GMT (in UTC): Sun 2025-03-30 00:00:00 UTC From now: 12h ago Iteration #3: Sun 2025-03-30 00:00:00 GMT <-- note every next iteration having the same firing time (in UTC): Sun 2025-03-30 00:00:00 UTC From now: 12h ago ... With this patch: $ env TZ=IST-1GMT-0,M10.5.0/1,M3.5.0/1 systemd-analyze calendar --base-time='Sat 2025-03-29 22:00:00 UTC' --iterations=5 'hourly' Original form: hourly Normalized form: *-*-* *:00:00 Next elapse: Sat 2025-03-29 23:00:00 GMT (in UTC): Sat 2025-03-29 23:00:00 UTC From now: 13h ago Iteration #2: Sun 2025-03-30 00:00:00 GMT (in UTC): Sun 2025-03-30 00:00:00 UTC From now: 12h ago Iteration #3: Sun 2025-03-30 02:00:00 IST <-- the expected 1 hour jump (in UTC): Sun 2025-03-30 01:00:00 UTC From now: 11h ago ... This bug isn't reproduced on Debian and Ubuntu because they mitigate it by using the rearguard version of tzdata. ArchLinux and NixOS don't, so it would cause pid1 to spin during DST transition. This is how the affected tzdata looks like: $ zdump -V -c 2024,2025 Europe/Dublin Europe/Dublin Sun Mar 31 00:59:59 2024 UT = Sun Mar 31 00:59:59 2024 GMT isdst=1 gmtoff=0 Europe/Dublin Sun Mar 31 01:00:00 2024 UT = Sun Mar 31 02:00:00 2024 IST isdst=0 gmtoff=3600 Europe/Dublin Sun Oct 27 00:59:59 2024 UT = Sun Oct 27 01:59:59 2024 IST isdst=0 gmtoff=3600 Europe/Dublin Sun Oct 27 01:00:00 2024 UT = Sun Oct 27 01:00:00 2024 GMT isdst=1 gmtoff=0 Compare it to Europe/London: $ zdump -V -c 2024,2025 Europe/London Europe/London Sun Mar 31 00:59:59 2024 UT = Sun Mar 31 00:59:59 2024 GMT isdst=0 gmtoff=0 Europe/London Sun Mar 31 01:00:00 2024 UT = Sun Mar 31 02:00:00 2024 BST isdst=1 gmtoff=3600 Europe/London Sun Oct 27 00:59:59 2024 UT = Sun Oct 27 01:59:59 2024 BST isdst=1 gmtoff=3600 Europe/London Sun Oct 27 01:00:00 2024 UT = Sun Oct 27 01:00:00 2024 GMT isdst=0 gmtoff=0 Fixes #32039. (cherry picked from commit e4bb033) (cherry picked from commit 07c01efc82d4a239ef0d14da54d36053294ad203) There were some conflicts related to the skipping of 6f5cf41, but the tests pass with and the example output above also looks good, so I think the backport is correct. (cherry picked from commit 1568dea89ebb84ed2c9cf8c45aaf90c07858cbc0) (cherry picked from commit f3dc34e) (cherry picked from commit 2230a5d)
1 parent c67949e commit 8c08ad7

File tree

2 files changed

+48
-4
lines changed

2 files changed

+48
-4
lines changed

src/shared/calendarspec.c

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,14 +1239,43 @@ static bool matches_weekday(int weekdays_bits, const struct tm *tm, bool utc) {
12391239
return (weekdays_bits & (1 << k));
12401240
}
12411241

1242+
static int tm_compare(const struct tm *t1, const struct tm *t2) {
1243+
int r;
1244+
1245+
assert(t1);
1246+
assert(t2);
1247+
1248+
r = CMP(t1->tm_year, t2->tm_year);
1249+
if (r != 0)
1250+
return r;
1251+
1252+
r = CMP(t1->tm_mon, t2->tm_mon);
1253+
if (r != 0)
1254+
return r;
1255+
1256+
r = CMP(t1->tm_mday, t2->tm_mday);
1257+
if (r != 0)
1258+
return r;
1259+
1260+
r = CMP(t1->tm_hour, t2->tm_hour);
1261+
if (r != 0)
1262+
return r;
1263+
1264+
r = CMP(t1->tm_min, t2->tm_min);
1265+
if (r != 0)
1266+
return r;
1267+
1268+
return CMP(t1->tm_sec, t2->tm_sec);
1269+
}
1270+
12421271
/* A safety valve: if we get stuck in the calculation, return an error.
12431272
* C.f. https://bugzilla.redhat.com/show_bug.cgi?id=1941335. */
12441273
#define MAX_CALENDAR_ITERATIONS 1000
12451274

12461275
static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
12471276
struct tm c;
1248-
int tm_usec;
1249-
int r;
1277+
int tm_usec, r;
1278+
bool invalidate_dst = false;
12501279

12511280
/* Returns -ENOENT if the expression is not going to elapse anymore */
12521281

@@ -1259,7 +1288,8 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
12591288
for (unsigned iteration = 0; iteration < MAX_CALENDAR_ITERATIONS; iteration++) {
12601289
/* Normalize the current date */
12611290
(void) mktime_or_timegm(&c, spec->utc);
1262-
c.tm_isdst = spec->dst;
1291+
if (!invalidate_dst)
1292+
c.tm_isdst = spec->dst;
12631293

12641294
c.tm_year += 1900;
12651295
r = find_matching_component(spec, spec->year, &c, &c.tm_year);
@@ -1349,6 +1379,18 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) {
13491379
if (r == 0)
13501380
continue;
13511381

1382+
r = tm_compare(tm, &c);
1383+
if (r == 0) {
1384+
assert(tm_usec + 1 <= 1000000);
1385+
r = CMP(*usec, (usec_t) tm_usec + 1);
1386+
}
1387+
if (r >= 0) {
1388+
/* We're stuck - advance, let mktime determine DST transition and try again. */
1389+
invalidate_dst = true;
1390+
c.tm_hour++;
1391+
continue;
1392+
}
1393+
13521394
*tm = c;
13531395
*usec = tm_usec;
13541396
return 0;

src/test/test-calendarspec.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ static void _test_next(int line, const char *input, const char *new_tz, usec_t a
4848
if (old_tz)
4949
old_tz = strdupa_safe(old_tz);
5050

51-
if (!isempty(new_tz))
51+
if (!isempty(new_tz) && !strchr(new_tz, ','))
5252
new_tz = strjoina(":", new_tz);
5353

5454
assert_se(set_unset_env("TZ", new_tz, true) == 0);
@@ -225,6 +225,8 @@ TEST(calendar_spec_next) {
225225
/* Check that we don't start looping if mktime() moves us backwards */
226226
test_next("Sun *-*-* 01:00:00 Europe/Dublin", "", 1616412478000000, 1617494400000000);
227227
test_next("Sun *-*-* 01:00:00 Europe/Dublin", "IST", 1616412478000000, 1617494400000000);
228+
/* Europe/Dublin TZ that moves DST backwards */
229+
test_next("hourly", "IST-1GMT-0,M10.5.0/1,M3.5.0/1", 1743292800000000, 1743296400000000);
228230
}
229231

230232
TEST(calendar_spec_from_string) {

0 commit comments

Comments
 (0)