From d3c93d1678ee82047c2ee03981ffb92c73669220 Mon Sep 17 00:00:00 2001 From: Ivan Kruglov Date: Thu, 20 Aug 2026 02:23:58 -0700 Subject: [PATCH 1/4] core/swap: don't adopt a swap unit that isn't backed by anything systemd creates one .swap unit per udev device link of every device listed in /proc/swaps. When such a device link disappears while the swap itself stays on -- e.g. because the link is not unique and udev repoints it at another partition -- /proc/swaps doesn't change, so nothing re-examines the unit and it stays active. The next daemon-reload then orphans it: all units are dropped and rebuilt, the enumeration from /proc/swaps no longer produces this name, and only the deserialization brings the unit back, i.e. by name and without a fragment on disk. swap_coldplug() restored its active state regardless, leaving a unit that is active, unloadable and has no device, and that is never garbage collected because active units aren't. Refuse to adopt such a unit: if neither /proc/swaps nor a fragment vouches for it, it describes a swap that doesn't exist anymore. Units that still have a control process around are left alone, so that the process is re-adopted and the timeout re-armed; those end up dead once it exits, as they aren't backed by /proc/swaps either. Issue: https://github.com/systemd/systemd/issues/43442 --- src/core/swap.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/core/swap.c b/src/core/swap.c index 88e992bd75..0d45246dd5 100644 --- a/src/core/swap.c +++ b/src/core/swap.c @@ -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; From 262b155a327ca4de72d15318a6c89b6094461a96 Mon Sep 17 00:00:00 2001 From: Ivan Kruglov Date: Thu, 20 Aug 2026 02:24:06 -0700 Subject: [PATCH 2/4] core/execute: don't assert on an unpatched exec context Issue: https://github.com/systemd/systemd/issues/43442 --- src/core/execute-serialize.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c index e11ea3253e..3e953bada7 100644 --- a/src/core/execute-serialize.c +++ b/src/core/execute-serialize.c @@ -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; From 7875de8d00427f5990ce2998a959bb0c3eef3830 Mon Sep 17 00:00:00 2001 From: Ivan Kruglov Date: Thu, 20 Aug 2026 02:24:19 -0700 Subject: [PATCH 3/4] core/swap: refuse to run swapoff without a device Issue: https://github.com/systemd/systemd/issues/43442 --- src/core/swap.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/core/swap.c b/src/core/swap.c index 0d45246dd5..38a2b347e2 100644 --- a/src/core/swap.c +++ b/src/core/swap.c @@ -873,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; From ee138cb41e7e92be49620811bef2b5e27396774d Mon Sep 17 00:00:00 2001 From: Ivan Kruglov Date: Thu, 20 Aug 2026 02:24:26 -0700 Subject: [PATCH 4/4] test: check that an orphaned swap unit isn't resurrected by a reload Issue: https://github.com/systemd/systemd/issues/43442 --- test/units/TEST-17-UDEV.issue-43442.sh | 129 +++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100755 test/units/TEST-17-UDEV.issue-43442.sh diff --git a/test/units/TEST-17-UDEV.issue-43442.sh b/test/units/TEST-17-UDEV.issue-43442.sh new file mode 100755 index 0000000000..1d97a6a75f --- /dev/null +++ b/test/units/TEST-17-UDEV.issue-43442.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Issue: https://github.com/systemd/systemd/issues/43442 +# +# A daemon-reload must not resurrect a .swap unit whose device link has disappeared. +# +# systemd creates one .swap unit per udev device link of every device listed in /proc/swaps. When such a +# device link goes away (e.g. because it is not unique and udev repoints it at another partition) while the +# swap itself stays on, the unit named after the link is left behind: the next daemon-reload drops all units +# and rebuilds them, the enumeration from /proc/swaps no longer knows the link, and the unit is only +# resurrected by the deserialization, i.e. by name, with no fragment on disk. That used to leave it +# +# LoadState=not-found, ActiveState=active, What= + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +LOOPDEV="" +UDEV_RULE=/run/udev/rules.d/99-testsuite-orphan-swap.rules +DEVLINK=/dev/testsuite-orphan-swap +SWAP_UNIT="$(systemd-escape --path --suffix=swap "$DEVLINK")" +FRAGMENT="/run/systemd/system/$SWAP_UNIT" +# Not /tmp/, that's a tmpfs here: swapping onto a loop device backed by memory that can itself be swapped +# out is a deadlock waiting to happen. +WORKDIR="$(mktemp -d -p /var/tmp)" + +at_exit() ( + set +e + + [[ -n "$LOOPDEV" ]] && swapoff "$LOOPDEV" + + rm -f "$FRAGMENT" "$UDEV_RULE" + udevadm control --reload + + if [[ -z "$LOOPDEV" ]]; then + rm -rf "$WORKDIR" + else + timeout 30 udevadm trigger --settle --action change "$LOOPDEV" + + # Neither detach the loop device nor unlink its backing file while the swap is still on: the + # /proc/swaps entry outlives both, naming a device node that is gone, and leaks into the subtests + # that follow in the same boot. + if swapon --show=NAME --noheadings | grep -x "$LOOPDEV" >/dev/null; then + echo "$LOOPDEV is still swapped on, leaving it and $WORKDIR behind" >&2 + else + losetup -d "$LOOPDEV" + rm -rf "$WORKDIR" + fi + fi +) + +trap at_exit EXIT + +truncate -s 64M "$WORKDIR"/swap.img +LOOPDEV="$(losetup --show --find "$WORKDIR"/swap.img)" +mkswap "$LOOPDEV" + +# Give the loop device an extra device link, so that systemd creates a second .swap unit named after it +# once the swap is on. +mkdir -p /run/udev/rules.d +cat >"$UDEV_RULE" <"$FRAGMENT" <"$UDEV_RULE" </dev/null