]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shutdown: enforce a minimum uptime to make boot loops less annoying
authorLennart Poettering <lennart@amutable.com>
Tue, 10 Mar 2026 07:06:02 +0000 (08:06 +0100)
committerLennart Poettering <lennart@amutable.com>
Sat, 4 Apr 2026 20:52:58 +0000 (22:52 +0200)
Fixes: #9453
man/kernel-command-line.xml
man/systemd-system.conf.xml
man/systemd.xml
src/core/main.c
src/core/system.conf.in
src/shutdown/shutdown.c

index 088ce24154042200a4f1b674f41e1c4a6b5e41ec..9e24e749b3f97b9d53d6bb8259de02a4d1e0795a 100644 (file)
@@ -69,6 +69,7 @@
         <term><varname>systemd.import_credentials=</varname></term>
         <term><varname>systemd.reload_limit_interval_sec=</varname></term>
         <term><varname>systemd.reload_limit_burst=</varname></term>
+        <term><varname>systemd.minimum_uptime_sec=</varname></term>
         <listitem>
           <para>Parameters understood by the system and service
           manager to control system behavior. For details, see
index 172657de65cbf9c7bcab2c016f5d1afc100ed3a7..e9e7d3d78db575851a6bae2c5a2ebea460324021 100644 (file)
 
         <xi:include href="version-info.xml" xpointer="v253"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>MinimumUptimeSec=</varname></term>
+
+        <listitem><para>Specifies the minimum uptime for the system which has to be reached before a shutdown
+        is executed. Defaults to 15s. This mechanism is introduced to avoid high frequency reboot loops, when
+        technical failures trigger an automatic shutdown during the boot process. Each reboot cycle is
+        delayed to the specified minimum time, giving the user a chance to review screen contents or
+        otherwise interact with the device before the shutdown proceeds. The delay takes place during the
+        very last phase of system shutdown, immediately before the <function>reboot()</function> system call
+        is executed. If the system is already running for longer than the specified time, this setting has no
+        effect. This logic is also skipped in container environments. Set to zero in order to disable this
+        logic.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 06d9102e475f165b6873dc8f7c17fb229ceccb3c..30ae385029b1085b03ea15c36e795311c903e58f 100644 (file)
 
         <xi:include href="version-info.xml" xpointer="v186"/></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>systemd.minimum_uptime_sec=</varname></term>
+
+        <listitem><para>Takes a time in seconds. Specifies the minimum uptime of the system before the system
+        shuts down. For more information see the <varname>MinimumUptimeSec=</varname> setting described in
+        <citerefentry><refentrytitle>systemd-system.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
     </variablelist>
 
     <para>For other kernel command line parameters understood by
index bd065c351d23a87ccb8cfdf7940c396653a032f9..d0c0023f899f431ba6c361db385badc659d775fa 100644 (file)
@@ -163,6 +163,7 @@ static void *arg_random_seed;
 static size_t arg_random_seed_size;
 static usec_t arg_reload_limit_interval_sec;
 static unsigned arg_reload_limit_burst;
+static usec_t arg_minimum_uptime_usec;
 
 /* A copy of the original environment block */
 static char **saved_env = NULL;
@@ -554,6 +555,17 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat
                         return 0;
                 }
 
+        } else if (proc_cmdline_key_streq(key, "systemd.minimum_uptime_sec")) {
+
+                if (proc_cmdline_value_missing(key, value))
+                        return 0;
+
+                r = parse_sec(value, &arg_minimum_uptime_usec);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to parse systemd.minimum_uptime_sec= argument '%s', ignoring: %m", value);
+                        return 0;
+                }
+
         } else if (streq(key, "quiet") && !value) {
 
                 if (arg_show_status == _SHOW_STATUS_INVALID)
@@ -813,6 +825,7 @@ static int parse_config_file(void) {
                 { "Manager", "ReloadLimitIntervalSec",       config_parse_sec,                   0,                        &arg_reload_limit_interval_sec               },
                 { "Manager", "ReloadLimitBurst",             config_parse_unsigned,              0,                        &arg_reload_limit_burst                      },
                 { "Manager", "DefaultMemoryZSwapWriteback",  config_parse_bool,                  0,                        &arg_defaults.memory_zswap_writeback         },
+                { "Manager", "MinimumUptimeSec",             config_parse_sec,                   0,                        &arg_minimum_uptime_usec                     },
 #if ENABLE_SMACK
                 { "Manager", "DefaultSmackProcessLabel",     config_parse_string,                0,                        &arg_defaults.smack_process_label            },
 #else
@@ -1740,6 +1753,9 @@ static int become_shutdown(int objective, int retval) {
         if (arg_watchdog_device)
                 (void) strv_extendf(&env_block, "WATCHDOG_DEVICE=%s", arg_watchdog_device);
 
+        if (arg_minimum_uptime_usec != USEC_INFINITY)
+                (void) strv_extendf(&env_block, "MINIMUM_UPTIME_USEC=" USEC_FMT, arg_minimum_uptime_usec);
+
         (void) write_boot_or_shutdown_osc("shutdown");
 
         execve(SYSTEMD_SHUTDOWN_BINARY_PATH, (char **) command_line, env_block);
@@ -2830,6 +2846,8 @@ static void reset_arguments(void) {
 
         arg_reload_limit_interval_sec = 0;
         arg_reload_limit_burst = 0;
+
+        arg_minimum_uptime_usec = USEC_INFINITY;
 }
 
 static void determine_default_oom_score_adjust(void) {
index 6000d1702e097b29b6d7ef021c9134b66c8dea8f..ef2c59bd79a8e8afe0b06a2ecffa639de322a7e6 100644 (file)
@@ -37,6 +37,7 @@
 #RebootWatchdogSec=10min
 #KExecWatchdogSec=off
 #WatchdogDevice=
+#MinimumUptimeSec=15s
 #CapabilityBoundingSet=
 #NoNewPrivileges=no
 #ProtectSystem=auto
index 9b0328bfca05efc624fa64231cda281fe026c8e2..1fb0f422e53b746fad53f7f225b801c676c6a5ff 100644 (file)
@@ -51,6 +51,7 @@
 
 #define SYNC_PROGRESS_ATTEMPTS 3
 #define SYNC_TIMEOUT_USEC (10*USEC_PER_SEC)
+#define DEFAULT_MINIMUM_UPTIME_USEC (15U * USEC_PER_SEC)
 
 static const char *arg_verb = NULL;
 static uint8_t arg_exit_code = 0;
@@ -343,6 +344,35 @@ static void notify_supervisor(void) {
                                   arg_exit_code, arg_verb);
 }
 
+static void sleep_until_minimum_uptime(void) {
+        uint64_t minimum_uptime_usec = DEFAULT_MINIMUM_UPTIME_USEC;
+        int r;
+
+        const char *e = secure_getenv("MINIMUM_UPTIME_USEC");
+        if (e) {
+                r = safe_atou64(e, &minimum_uptime_usec);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to parse $MINIMUM_UPTIME_USEC, ignoring: %s", e);
+        }
+
+        if (minimum_uptime_usec <= 0) /* turned off? */
+                return;
+
+        for (;;) {
+                usec_t n = now(CLOCK_BOOTTIME);
+                if (n >= minimum_uptime_usec)
+                        break;
+
+                usec_t m = minimum_uptime_usec - n;
+                log_notice("Delaying shutdown for %s, in order to reach minimum uptime of %s.",
+                           FORMAT_TIMESPAN(m, USEC_PER_SEC),
+                           FORMAT_TIMESPAN(minimum_uptime_usec, USEC_PER_SEC));
+
+                /* Sleep for up to 3s, then show message again, as a progress indicator. */
+                usleep_safe(MIN(m, 3 * USEC_PER_SEC));
+        }
+}
+
 int main(int argc, char *argv[]) {
         static const char* const dirs[] = {
                 SYSTEM_SHUTDOWN_PATH,
@@ -611,6 +641,12 @@ int main(int argc, char *argv[]) {
 
         notify_supervisor();
 
+        /* Enforce the minimum uptime, but don't bother with it in containers, since – unlike on bare metal
+         * and VMs – the screen output isn't flushed out immediately when we reboot (as OVMF or real PC
+         * firmwares do) */
+        if (!in_container)
+                sleep_until_minimum_uptime();
+
         if (streq(arg_verb, "exit")) {
                 if (in_container) {
                         log_info("Exiting container.");