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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user