shutdown: enforce a minimum uptime to make boot loops less annoying (#41215)

Fixes: #9453

Split out of #41016
This commit is contained in:
Lennart Poettering
2026-04-04 23:30:30 +02:00
committed by GitHub
7 changed files with 101 additions and 6 deletions
-3
View File
@@ -298,9 +298,6 @@ Features:
* Add knob to cryptsetup, to trigger automatic reboot on failure to unlock
disk. Enable this by default for rootfs, also in gpt-auto-generator
* Add RebootUptimeMinSec= knob to PID 1, that makes systemd-shutdown sleep
until the specified uptime has passed, to lengthen tight boot loops.
* replace bootctl's PE version check to actually use APIs from pe-binary.[ch]
to find binary version.
+1
View 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
+16
View File
@@ -633,6 +633,22 @@
<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>
+10
View File
@@ -1061,6 +1061,16 @@
<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
+18
View 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) {
+1
View File
@@ -37,6 +37,7 @@
#RebootWatchdogSec=10min
#KExecWatchdogSec=off
#WatchdogDevice=
#MinimumUptimeSec=15s
#CapabilityBoundingSet=
#NoNewPrivileges=no
#ProtectSystem=auto
+55 -3
View 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;
@@ -286,14 +287,30 @@ static void init_watchdog(void) {
const char *s;
int r;
s = getenv("WATCHDOG_DEVICE");
/* NB: we do not insist on $WATCHDOG_PID being set because old systemd versions didn't set it at all,
* and we want to retain some basic compatibility between an old service manager and a new shutdown
* binary. If it *is* set we'll insist on it being set to 1 however. */
s = secure_getenv("WATCHDOG_PID");
if (s) {
pid_t pid;
r = parse_pid(s, &pid);
if (r < 0)
log_warning_errno(r, "Failed to parse $WATCHDOG_PID, ignoring: %s", s);
else if (pid != getpid_cached()) {
log_warning("$WATCHDOG_PID set, but not to " PID_FMT ", skipping watchdog logic.", getpid_cached());
return;
}
}
s = secure_getenv("WATCHDOG_DEVICE");
if (s) {
r = watchdog_set_device(s);
if (r < 0)
log_warning_errno(r, "Failed to set watchdog device to %s, ignoring: %m", s);
log_warning_errno(r, "Failed to set watchdog device to '%s', ignoring: %m", s);
}
s = getenv("WATCHDOG_USEC");
s = secure_getenv("WATCHDOG_USEC");
if (s) {
usec_t usec;
@@ -327,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,
@@ -595,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.");