]> 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)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Tue, 4 Nov 2025 16:36:47 +0000 (01:36 +0900)
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
src/systemctl/systemctl-compat-shutdown.c

index d50dec372a76e4a2578e56dd5ddef1d1df01d84a..877ea8378b3759894fd888382eeb58c5f0a2507e 100644 (file)
@@ -95,8 +95,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;
         }