core/swap: don't resurrect swap units whose device link disappeared (#43443)

Fixes #43442.

A `.swap` unit can end up **active but unloadable, with no device**, and
stopping such a unit aborts PID 1.

### What it looks like

```
$ systemctl show 'dev-disk-byx2dpartlabel-primary.swap' -p LoadState,ActiveState,What
LoadState=not-found
ActiveState=active
What=                     # <-- no device

$ systemctl stop 'dev-disk-byx2dpartlabel-primary.swap'
systemd[1]: About to execute: /usr/sbin/swapoff        # <-- no argument
systemd[1]: Caught <ABRT>, from our own process.
systemd[1]: Freezing execution.
```

PID 1 stays alive but frozen, so the machine stops answering `systemctl`
and never reboots.

### How it happens
1. systemd creates one `.swap` unit per udev device link of every device
in `/proc/swaps`.
2. A device link gets repointed at another device while the swap stays
on. `/proc/swaps` doesn't change, so nothing re-examines the unit — it
stays active.
3. The next `daemon-reload` drops all units and rebuilds them.
Enumeration from `/proc/swaps` no longer produces this name, so only the
deserialization brings the unit back: by name, with no fragment on disk.
4. `swap_load()` only calls `swap_add_extras()` for units that loaded a
fragment or are backed by `/proc/swaps`, so this one gets neither a
`What=` nor a patched `ExecContext`.
5. `swap_coldplug()` restores its active state anyway.
6. Shutdown stops it. `unit_stop()` deliberately permits stopping units
whose load failed, so it reaches `exec_spawn()` and trips the
`PrivateVarTmp=` assertion in `exec_context_serialize()`.
This commit is contained in:
Yu Watanabe
2026-09-04 04:11:29 +09:00
committed by GitHub
3 changed files with 164 additions and 2 deletions
+7 -2
View File
@@ -1724,8 +1724,13 @@ static int exec_context_serialize(const ExecContext *c, FILE *f) {
if (r < 0)
return r;
/* This must be set in unit_patch_contexts() before executing a command. */
assert(c->private_var_tmp >= 0 && c->private_var_tmp < _PRIVATE_TMP_MAX);
/* This must be set in unit_patch_contexts() before executing a command. A unit whose load failed
* never goes through that, yet can still be stopped, hence this is reachable. Refuse instead of
* asserting, this runs in PID 1. */
if (c->private_var_tmp < 0 || c->private_var_tmp >= _PRIVATE_TMP_MAX)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"PrivateVarTmp= is not set up, unit was not fully loaded, refusing.");
r = serialize_item(f, "exec-context-private-var-tmp", private_tmp_to_string(c->private_var_tmp));
if (r < 0)
return r;
+28
View File
@@ -566,6 +566,23 @@ static int swap_coldplug(Unit *u) {
else if (s->from_proc_swaps)
new_state = SWAP_ACTIVE;
/* If /proc/swaps doesn't know this unit, and we couldn't load a fragment for it either, then it is
* a leftover of the serialization describing a swap that doesn't exist anymore — typically because
* the device link the unit is named after has been repointed at some other device in the meantime.
* Don't adopt such a unit: it has no device we could pass to swapoff, and since active units are
* never garbage collected it would stick around until the end of time. Note that units with a
* fragment are explicitly *not* covered by this, as those are not backed by /proc/swaps to begin
* with when their name doesn't match What=.
*
* Units with a control process still around are left alone too: we want to re-adopt the process
* and re-arm the timeout rather than drop both on the floor. Once it is gone the state machine
* ends up in dead anyway, as the unit isn't backed by /proc/swaps. */
if (!s->from_proc_swaps && u->load_state != UNIT_LOADED && !SWAP_STATE_WITH_PROCESS(new_state)) {
log_unit_debug(u, "Not backed by /proc/swaps and has no fragment, ignoring.");
unit_add_to_gc_queue(u);
return 0;
}
if (new_state == s->state)
return 0;
@@ -856,6 +873,17 @@ static void swap_enter_deactivating(Swap *s) {
assert(s);
/* A swap unit can be active without ever having been loaded, and hence without a device: an
* orphaned unit that still has a swapon running is adopted by swap_coldplug() on purpose, and
* swap_sigchld_event() moves it to SWAP_ACTIVE once that process exits successfully. There is
* nothing to deactivate for such a unit, and calling swapoff without an argument certainly isn't
* it — go to dead, like swap_process_proc_swaps() does when a unit disappears from /proc/swaps. */
if (isempty(s->what)) {
log_unit_warning(UNIT(s), "Swap unit has no device, assuming it is already deactivated.");
swap_enter_dead(s, SWAP_SUCCESS);
return;
}
s->control_command_id = SWAP_EXEC_DEACTIVATE;
s->control_command = s->exec_command + SWAP_EXEC_DEACTIVATE;