]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
systemctl: Fix shutdown time parsing across DST changes
authorChris Down <chris@chrisdown.name>
Tue, 4 Nov 2025 10:19:07 +0000 (18:19 +0800)
committerLuca Boccassi <luca.boccassi@gmail.com>
Thu, 6 Nov 2025 21:26:42 +0000 (21:26 +0000)
When parsing an absolute time specification like `hh:mm` for the
`shutdown` command, the code interprets a time in the past as "tomorrow
at this time". It currently implements this by adding a fixed 24-hour
duration (`USEC_PER_DAY`) to the timestamp.

This assumption breaks across DST transitions, as the day might not be
24 hours long. This can cause the shutdown to be scheduled at the wrong
time (typically off by one hour in either direction).

Change the logic to perform calendar arithmetic instead of timestamp
arithmetic. If the calculated time is in the past, we increment
`tm.tm_mday` and call `mktime_or_timegm_usec()` a second time.

This delegates all date normalization logic to `mktime()`, which
correctly handles all edge cases, including DST transitions, month-end
rollovers, and leap years.

Fixes: https://github.com/systemd/systemd/issues/39232
(cherry picked from commit a8c3ac66721de23cceff359d946ecd9695bbacb8)

src/systemctl/systemctl-compat-shutdown.c

index d85ff9ab11768b073af27f5b88ffede8d3ca3518..ebe8ec65418e6dbf146eacbc483b9bb400052241 100644 (file)
@@ -96,8 +96,28 @@ static int parse_shutdown_time_spec(const char *t, usec_t *ret) {
                 if (r < 0)
                         return r;
 
-                while (s <= n)
-                        s += USEC_PER_DAY;
+                if (s <= n) {
+                        /* The specified time is today, but in the past. We need to schedule it for tomorrow
+                         * at the same time. Adding USEC_PER_DAY would be wrong across DST changes, so just
+                         * let mktime() normalise it. */
+                        int requested_hour = tm.tm_hour;
+                        int requested_min = tm.tm_min;
+
+                        tm.tm_mday++;
+                        tm.tm_isdst = -1;
+                        r = mktime_or_timegm_usec(&tm, /* utc= */ false, &s);
+                        if (r < 0)
+                                return r;
+
+                        if (tm.tm_hour != requested_hour || tm.tm_min != requested_min) {
+                                log_warning("Requested shutdown time %02d:%02d does not exist. "
+                                            "Rescheduling to %02d:%02d.",
+                                            requested_hour,
+                                            requested_min,
+                                            tm.tm_hour,
+                                            tm.tm_min);
+                        }
+                }
 
                 *ret = s;
         }