core: do not leak resources when handling stale alias state on reload (#41986)

The fix for the corrupted state when units become aliased on reload
leaks the now-aliased unit's resources, which become untracked and
essentially lost.

While fixing the state corruption is of course necessary, leaking
processes/etc. is not ideal for a system and service manager, so
instead attempt to keep track of them by creating stub units
on-the-fly.
This way resources are not leaked, there are clear indications of
where they moved, and all state can be tracked as expected.
This commit is contained in:
Chris Down
2026-05-13 21:25:08 +09:00
committed by GitHub
4 changed files with 310 additions and 57 deletions
+99 -11
View File
@@ -1,5 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-id128.h"
#include "alloc-util.h"
#include "bpf-restrict-fsaccess.h"
#include "dbus.h"
@@ -11,6 +13,7 @@
#include "glyph-util.h"
#include "hashmap.h"
#include "initrd-util.h"
#include "job.h"
#include "manager.h"
#include "manager-serialize.h"
#include "parse-util.h"
@@ -19,6 +22,8 @@
#include "string-util.h"
#include "strv.h"
#include "syslog-util.h"
#include "unit.h"
#include "unit-name.h"
#include "unit-serialize.h"
#include "user-util.h"
#include "varlink.h"
@@ -240,6 +245,87 @@ static int manager_collect_serialized_unit_names(FILE *f, Set **ret) {
return 0;
}
static int manager_synthesize_orphaned_unit(
Manager *m,
const char *original_name,
const char *canonical_name,
FILE *f,
FDSet *fds) {
_cleanup_(unit_freep) Unit *orphan = NULL;
_cleanup_free_ char *orphaned_name = NULL;
sd_id128_t rnd;
UnitType t;
int r;
assert(m);
assert(original_name);
assert(canonical_name);
assert(f);
assert(fds);
t = unit_name_to_type(original_name);
if (t < 0)
return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
"Cannot synthesize unit for '%s' (overridden by alias to '%s'): invalid unit type. Skipping stale state.",
original_name, canonical_name);
/* Only transition units that track external resources, forget internal ones (eg: timers) */
if (!IN_SET(t, UNIT_SERVICE, UNIT_SCOPE, UNIT_MOUNT, UNIT_AUTOMOUNT))
return log_warning_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Cannot synthesize unit for '%s' (overridden by alias to '%s'): unsupported unit type. Skipping stale state.",
original_name, canonical_name);
/* Use a naming convention with an "orphaned-" prefix to make it clear at a glance that these units
* were synthesized to adopt resources from a now-aliased unit */
r = sd_id128_randomize(&rnd);
if (r < 0)
return log_warning_errno(r, "Failed to generate random ID for orphan unit: %m");
if (asprintf(&orphaned_name, "orphaned-r" SD_ID128_FORMAT_STR ".%s",
SD_ID128_FORMAT_VAL(rnd), unit_type_to_string(t)) < 0)
return log_oom();
r = unit_new_for_name(m, unit_vtable[t]->object_size, orphaned_name, &orphan);
if (r < 0)
return log_warning_errno(r, "Failed to allocate orphan unit '%s': %m", orphaned_name);
/* Force the state as we know the unit file is gone, so it will hang around as long as resources are
* active, and then it will be automatically collected */
orphan->load_state = UNIT_NOT_FOUND;
/* Ensure the orphan also gets garbage-collected when it ends up in 'failed' state. */
orphan->collect_mode = COLLECT_INACTIVE_OR_FAILED;
_cleanup_free_ char *description = strjoin("Orphaned resources adopted from aliased unit ", original_name);
if (!description)
return log_oom();
r = unit_deserialize_state(orphan, f, fds);
if (r < 0)
return log_notice_errno(r, "Failed to deserialize state into orphan unit '%s': %m", orphaned_name);
/* From this point on the serialized stream for this unit has been fully consumed, so avoid failing. */
log_warning("Unit file for '%s' was overridden by a symlink to '%s'. Synthesized orphan unit '%s' to retain tracking of the previous unit's processes.",
original_name, canonical_name, orphaned_name);
/* Override Description= so it's clear what this is for and can be traced back to the original. */
free_and_replace(orphan->description, description);
/* Any jobs reinstalled from the deserialized state targeted the original unit. Most of them
* (start, restart, reload, ...) make no sense for the synthesized orphan, especially after a
* service-to-scope conversion, so cancel them. JOB_STOP is the exception: a stop request that was
* already pending on the original unit is exactly what we want to keep around, since it'll cause
* the synthesized unit to just go away immediately. */
if (orphan->job && orphan->job->type != JOB_STOP)
job_finish_and_invalidate(orphan->job, JOB_CANCELED, /* recursive= */ false, /* already= */ false);
if (orphan->nop_job)
job_finish_and_invalidate(orphan->nop_job, JOB_CANCELED, /* recursive= */ false, /* already= */ false);
TAKE_PTR(orphan);
return 0;
}
static int manager_deserialize_one_unit(
Manager *m,
const char *name,
@@ -279,21 +365,23 @@ static int manager_deserialize_one_unit(
* so that any further aliases resolving to the same unit are skipped.
*
* The serialized data represents the old, independent unit. Deserializing this stale state
* would corrupt the canonical unit's live state, so we must discard it.
* onto the canonical unit would corrupt its live state. Instead, we synthesize a new unloaded
* unit with a unique name and migrate the cgroup/PID/etc. tracking from the stale state
* into it, so that the resources from the previously independent unit remain tracked.
*
* Take as an example, a.service is running. Someone created symlink b.service -> a.service.
* On first reload, the state file still has b.service as an independent dead unit (from
* before the symlink existed), but b.service now resolves to a.service. We must discard
* b.service's stale dead state to preserve a.service's running state.
* On first reload, the state file still has b.service as an independent unit (from before
* the symlink existed), but b.service now resolves to a.service. We retain a.service's
* running state, and synthesize an unloaded unit to keep tracking the processes that
* b.service used to own.
*
* Note: This log message is checked in TEST-07-PID1.alias-corruption.sh, so the test case
* may need adjustment if the message is changed.
* Note: Log messages from this code path are checked in TEST-07-PID1.alias-corruption.sh,
* so the test case may need adjustment if they are changed.
*/
log_warning("Unit file for '%s' was overridden by a symlink to '%s', which also has serialized state. Skipping stale state of old unit. Any processes from the overridden unit are now abandoned!",
name,
u->id);
return unit_deserialize_state_skip(f);
r = manager_synthesize_orphaned_unit(m, name, u->id, f, fds);
if (r < 0) /* If we fail to orphan for any reason, then discard the unit */
r = unit_deserialize_state_skip(f);
return r;
}
r = unit_deserialize_state(u, f, fds);