install: turn off vendor enablement by masking it from /etc/

"systemctl disable" can't turn a unit off that the vendor enabled with a
dependency symlink below /usr/. Where a mask has been created by hand it removes
the mask, which silently enables the unit again. A preset policy of "disable"
can't turn such a unit off either.

/usr/ is frequently read-only and anything we did there would be undone by the
next image update, so let's shadow the symlink instead: disable writes a symlink
to /dev/null into the configuration directory, and PID 1 ignores a dependency
entry that resolves to /dev/null. Only the symlinks that enabling would create
are shadowed, i.e. the ones in the directories the [Install] section names. A
static dependency such as the symlink to remote-cryptsetup.target in
initrd-root-device.target.wants/ stays untouched. Enable removes the masks
again, those of the Also= units included, which is why the removal runs after
the symlinks have been applied. Creating a symlink replaces the mask at the same
path. Disabling a template masks every instance the vendor enabled, and enabling
the template only creates the symlink of its default instance, so the removal
afterwards catches the masks of the other instances.

Disabling a template also removes the symlinks of its instances.
"systemctl disable foo@.service" therefore deletes a foo@bar.service ->
/dev/null unit mask, which unmasks the instance. Disable now leaves all masks
alone, unit masks included.

Static dependencies are left alone throughout, so that a catch-all "disable *"
policy can't stop the likes of systemd-journald.service from starting at boot.
Nothing is masked where something else already exists in the dependency
directory: it already shadows the vendor symlink, and replacing it would destroy
it.

A preset policy of "disable" deliberately doesn't mask this way and leaves the
vendor enablement in place. Preset files never had to name the units the vendor
enabled below /usr/, and with a catch-all "disable *" rule a mask would stop
all of those units from starting at boot.

"systemctl --user disable" warns when a unit stays enabled in global scope, and
checks the global state for that. A vendor symlink in /usr/lib/systemd/user/ now
counts there, even though the mask the disable just created in the user's
configuration shadows it. Let's check the user scope's state instead. It sees
the mask, and it still reports a unit enabled in /etc/systemd/user/ as enabled.

The changes list gains two types for this, "mask-dependency" and
"unmask-dependency", so that callers can tell a mask from a plain unlink. The
bus API documents them as such, GetUnitFileLinks() reports the vendor symlink
rather than the mask that would shadow it, and so does "is-enabled --full".
This commit is contained in:
Daan De Meyer committed 2026-11-03 10:54:59 +01:00
1 parent 25f8fcab66
commit f301d3c01d
9 files changed
+792 -21

No files matched your search

+16
View File
@@ -166,6 +166,22 @@ CHANGES WITH 263 in spe:
1 for them. Distributions install such symlinks for a number of
units, so this may be visible in packaging scripts.
"systemctl disable" can now turn such units off. It places a symlink
to /dev/null with the same name in /etc/, which shadows the symlink
below /usr/, and "systemctl enable" removes it again. The symlink
below /usr/ stays, as /usr/ is frequently read-only and the next
image update would restore it.
"systemctl disable" no longer removes masks. Previously, disabling a
template unit removed the masks of its instances, which unmasked
them.
A preset policy of "disable" leaves the symlinks below /usr/ in
effect, so that "systemctl preset-all" keeps behaving as before on
existing systems. Preset files never had to name the units the
vendor enabled, and shadowing the symlinks would stop those units
from starting at boot.
Only a symlink that "systemctl enable" would create counts, i.e. one
in a directory that WantedBy=, RequiredBy= or UpheldBy= in the
[Install] section of the unit names. Any other dependency symlink
+9 -4
View File
@@ -1667,10 +1667,15 @@ node /org/freedesktop/systemd1 {
<filename>/etc/</filename>). The second one controls whether symlinks pointing to other units shall be
replaced if necessary. This method returns one boolean and an array of the changes made. The boolean
signals whether the unit files contained any enablement information (i.e. an [Install] section). The
changes array consists of structures with three strings: the type of the change (one of
<literal>symlink</literal> or <literal>unlink</literal>), the file name of the symlink and the
destination of the symlink. Note that most of the following calls return a changes list in the same
format.</para>
changes array consists of structures with three strings: the type of the change, the file name of the
symlink and the destination of the symlink. The type is one of <literal>symlink</literal>,
<literal>unlink</literal>, <literal>mask-dependency</literal> (an entry resolving to
<filename>/dev/null</filename>, which the service manager ignores when loading dependencies, was placed
over a dependency symlink the vendor installed below <filename>/usr/</filename>, whose path is reported as
the destination) or
<literal>unmask-dependency</literal> (such a symlink was removed again). Clients should ignore types
they do not know, as further ones may be added. Note that most of the following calls return a changes
list in the same format.</para>
<para>Similarly, <function>DisableUnitFiles()</function> disables one or more units in the system,
i.e. removes all symlinks to them in <filename>/etc/</filename> and <filename>/run/</filename>.</para>
+23
View File
@@ -973,6 +973,11 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
account.
</para>
<para>This command also removes the <filename>/dev/null</filename> entries that a prior
<command>disable</command> placed in the unit configuration directory to shadow the symlinks the
vendor enabled the unit with below <filename>/usr/</filename>, see <command>disable</command>
below.</para>
<para>When using this operation on units without install information, a warning about it is shown.
<option>--no-warn</option> can be used to suppress the warning.</para>
@@ -1004,6 +1009,19 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
<command>enable</command>, the two commands are otherwise not symmetric, as <command>disable</command> may
remove more symlinks than a prior <command>enable</command> invocation of the same unit created.</para>
<para>A unit the vendor enabled with a <filename>.wants/</filename>, <filename>.requires/</filename>
or <filename>.upholds/</filename> symlink below <filename>/usr/</filename> is turned off by
placing a symlink to <filename>/dev/null</filename> with the same name in the unit configuration
directory. The service manager ignores a dependency entry that resolves to
<filename>/dev/null</filename>, so the entry shadows the vendor's symlink. The vendor's symlink
itself stays: <filename>/usr/</filename> is frequently read-only, and the next image update
would restore it. Only the symlinks that <command>enable</command> would create are shadowed,
i.e. the ones in the directories that <varname>WantedBy=</varname>,
<varname>RequiredBy=</varname> and <varname>UpheldBy=</varname> in the [Install] section name.
Any other dependency symlink below <filename>/usr/</filename> is a static dependency and stays in
effect. Existing masks stay too, including the masks of instances when a template unit is
disabled.</para>
<para>This command expects valid unit names only, it does not accept paths to unit files.</para>
<para>In addition to the units specified as arguments, all units are disabled that are listed in the
@@ -1066,6 +1084,11 @@ Jan 12 10:46:45 example.com bluetoothd[8900]: gatt-time-server: Input/output err
<para>Use <option>--preset-mode=</option> to control whether units shall be
enabled and disabled, or only enabled, or only disabled.</para>
<para>A preset policy of <literal>disable</literal> removes the symlinks in the unit
configuration directory. Unlike <command>disable</command>, it leaves the symlinks the vendor
enabled a unit with below <filename>/usr/</filename> in effect. Preset files do not need to name
the units the vendor enabled.</para>
<para>If the unit carries no install information, it will be silently ignored
by this command. <replaceable>UNIT</replaceable> must be the real unit name,
any alias names are ignored silently.</para>
+17 -7
View File
@@ -2873,7 +2873,7 @@ static int method_get_unit_file_links(sd_bus_message *message, void *userdata, s
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Manager *m = ASSERT_PTR(userdata);
InstallChange *changes = NULL;
size_t n_changes = 0, i;
size_t n_changes = 0;
const char *name;
int runtime, r;
@@ -2897,12 +2897,22 @@ static int method_get_unit_file_links(sd_bus_message *message, void *userdata, s
if (r < 0)
return log_error_errno(r, "Failed to get file links for %s: %m", name);
for (i = 0; i < n_changes; i++)
if (changes[i].type == INSTALL_CHANGE_UNLINK) {
r = sd_bus_message_append(reply, "s", changes[i].path);
if (r < 0)
return r;
}
FOREACH_ARRAY(c, changes, n_changes) {
const char *link;
/* For units enabled by the vendor the dry run reports the mask that would shadow the vendor
* symlink. Report the vendor symlink itself, that's the one that enables the unit. */
if (c->type == INSTALL_CHANGE_UNLINK)
link = c->path;
else if (c->type == INSTALL_CHANGE_MASK_DEPENDENCY)
link = c->source;
else
continue;
r = sd_bus_message_append(reply, "s", link);
if (r < 0)
return r;
}
r = sd_bus_message_close_container(reply);
if (r < 0)
+379 -6
View File
@@ -358,6 +358,21 @@ static const char* dependency_dir_suffix(const char *dir) {
return endswith(dir, ".wants") ?: endswith(dir, ".requires") ?: endswith(dir, ".upholds");
}
static int path_is_mask(const LookupPaths *lp, const char *path) {
int r;
assert(lp);
assert(path);
/* null_or_empty_path_with_root() fails on a dangling symlink. PID 1 does not treat a dangling
* symlink as a mask, so return false for it. */
r = null_or_empty_path_with_root(path, lp->root_dir);
if (r == -ENOMEM)
return r;
return r > 0;
}
static const char* config_path_from_flags(const LookupPaths *lp, UnitFileFlags flags) {
assert(lp);
@@ -431,6 +446,12 @@ void install_change_dump_success(const InstallChange *change) {
case INSTALL_CHANGE_UNLINK:
return log_info("Removed '%s'.", change->path);
case INSTALL_CHANGE_MASK_DEPENDENCY:
return log_info("Masked dependency '%s'.", change->path);
case INSTALL_CHANGE_UNMASK_DEPENDENCY:
return log_info("Unmasked dependency '%s'.", change->path);
case INSTALL_CHANGE_IS_MASKED:
return log_info("Unit %s is masked, ignoring.", change->path);
@@ -858,6 +879,15 @@ static int remove_marked_symlinks_fd(
if (!found)
continue;
/* Disabling must not remove masks. Removing a dependency mask enables the unit
* again, and removing a unit mask unmasks the unit. The template match above finds
* a foo@bar.service mask when foo@.service is disabled. */
r = path_is_mask(lp, p);
if (r < 0)
return r;
if (r > 0)
continue;
if (!dry_run) {
if (unlinkat(fd, de->d_name, 0) < 0 && errno != ENOENT) {
RET_GATHER(ret, install_changes_add(changes, n_changes, -errno, p, /* source= */ NULL));
@@ -975,7 +1005,12 @@ static int dependency_entry_is_symlink(const LookupPaths *lp, const char *path)
/* PID 1 looks up an entry name in the dependency directories of a unit in all unit directories, and uses
* only the entry in the directory with the highest priority. PID 1 ignores that entry if it is a mask, if
* it is not a symlink, or if it is not named after a unit. */
static int dependency_entry_is_used(const LookupPaths *lp, const char *dir_path, const char *name) {
static int dependency_entry_is_used(
const LookupPaths *lp,
char **search_path,
const char *dir_path,
const char *name) {
_cleanup_free_ char *dir = NULL, *path = NULL, *unit = NULL, *winner = NULL;
const char *suffix;
int r;
@@ -1009,7 +1044,7 @@ static int dependency_entry_is_used(const LookupPaths *lp, const char *dir_path,
/* PID 1 applies the entries in a directory such as target.wants/ to every unit of that type. The
* directory name contains no unit name to look up, so only the entry itself is checked. */
if (unit_name_is_valid(unit, UNIT_NAME_ANY)) {
r = unit_file_find_dropin_entry(lp->root_dir, lp->search_path, suffix, unit,
r = unit_file_find_dropin_entry(lp->root_dir, search_path, suffix, unit,
/* aliases= */ NULL, name, &winner);
if (r <= 0)
return r;
@@ -1025,6 +1060,200 @@ static int dependency_entry_is_used(const LookupPaths *lp, const char *dir_path,
return true;
}
static int dependency_name_matches(const InstallInfo *info, const char *name) {
_cleanup_free_ char *template = NULL;
int r;
assert(info);
assert(name);
if (streq(name, info->name))
return true;
/* Covers DefaultInstance= too: that is just one particular instance of the template. */
r = unit_name_template(name, &template);
if (r == -EINVAL)
return false;
if (r < 0)
return r;
return streq(template, info->name);
}
static int dependency_symlinks_list(const char *path, char ***ret) {
_cleanup_strv_free_ char **l = NULL;
_cleanup_closedir_ DIR *dir = NULL;
int r;
assert(path);
assert(ret);
dir = opendir(path);
if (!dir) {
if (IN_SET(errno, ENOENT, ENOTDIR, EACCES)) {
*ret = NULL;
return 0;
}
return -errno;
}
FOREACH_DIRENT(de, dir, return -errno) {
_cleanup_closedir_ DIR *sub = NULL;
_cleanup_free_ char *dir_path = NULL;
/* A dependency directory that is a symlink could point outside of --root=. Skip it, as
* find_symlinks() does. */
if (de->d_type != DT_DIR || !dependency_dir_suffix(de->d_name))
continue;
sub = xopendirat(dirfd(dir), de->d_name, O_NOFOLLOW);
if (!sub) {
if (IN_SET(errno, ENOENT, ENOTDIR, ELOOP, EACCES))
continue;
return -errno;
}
dir_path = path_join(path, de->d_name);
if (!dir_path)
return -ENOMEM;
FOREACH_DIRENT(sde, sub, return -errno) {
_cleanup_free_ char *p = NULL;
/* Entries of every type are listed, because an empty file is a mask too. */
if (!unit_name_is_valid(sde->d_name, UNIT_NAME_ANY))
continue;
p = path_join(dir_path, sde->d_name);
if (!p)
return -ENOMEM;
r = strv_consume(&l, TAKE_PTR(p));
if (r < 0)
return r;
}
}
/* Sorted, so that the reported changes come out in a stable order rather than in readdir order. */
strv_sort(l);
*ret = TAKE_PTR(l);
return 0;
}
/* Enablement that lives in the vendor unit directories cannot be removed on behalf of the administrator:
* /usr/ is frequently read-only, and anything we did there would be undone by the next image update. Shadow
* it with a symlink to /dev/null instead, which is what PID 1 looks for when deciding to ignore a dependency
* symlink. Only the symlinks that enabling would create are shadowed, see dependency_is_enablement(). A
* static dependency the vendor installed stays in effect. */
static int install_info_mask_dependencies(
RuntimeScope scope,
const InstallInfo *info,
const LookupPaths *lp,
char **vendor_search_path,
const char *config_path,
bool dry_run,
InstallChange **changes,
size_t *n_changes) {
_cleanup_strv_free_ char **vendor_symlinks = NULL;
int r, ret = 0;
assert(info);
assert(lp);
assert(config_path);
/* Only entries in the vendor directories are looked at. The caller removes the administrator's
* symlinks before calling this function. In a dry run the administrator's symlinks are still there,
* and they would hide the vendor entries. */
STRV_FOREACH(p, vendor_search_path) {
_cleanup_strv_free_ char **l = NULL;
r = dependency_symlinks_list(*p, &l);
if (r < 0)
return r;
r = strv_extend_strv_consume(&vendor_symlinks, TAKE_PTR(l), /* filter_duplicates= */ false);
if (r < 0)
return r;
}
STRV_FOREACH(vendor_path, vendor_symlinks) {
_cleanup_free_ char *dir_path = NULL, *dir = NULL, *entry = NULL, *path = NULL;
r = path_extract_filename(*vendor_path, &entry);
if (r < 0)
return r;
r = dependency_name_matches(info, entry);
if (r <= 0) {
if (r < 0)
return r;
continue;
}
r = path_extract_directory(*vendor_path, &dir_path);
if (r < 0)
return r;
r = path_extract_filename(dir_path, &dir);
if (r < 0)
return r;
r = dependency_is_enablement(scope, info, dir, entry);
if (r <= 0) {
if (r < 0)
return r;
continue;
}
r = dependency_entry_is_used(lp, vendor_search_path, dir_path, entry);
if (r <= 0) {
if (r < 0)
return r;
continue;
}
path = path_join(config_path, dir, entry);
if (!path)
return -ENOMEM;
/* A non-symlink sitting here already shadows the vendor symlink, and PID 1 ignores it for
* not being one, so the unit is off either way. Replacing it would fail on a directory and
* destroy whatever it is otherwise, so leave anything we could not identify alone too.
* -ENOENT is the normal case of there being nothing here yet. */
r = is_symlink(path);
if (r == 0)
continue;
if (r < 0 && r != -ENOENT) {
log_debug_errno(r, "Failed to look at '%s', not masking it: %m", path);
continue;
}
/* Leave an existing mask alone too, so that disabling twice stays quiet. */
r = path_is_mask(lp, path);
if (r < 0)
return r;
if (r > 0)
continue;
if (!dry_run) {
(void) mkdir_parents_label(path, 0755);
r = symlink_atomic("/dev/null", path);
if (r < 0) {
RET_GATHER(ret, install_changes_add(changes, n_changes, r, path, /* source= */ NULL));
continue;
}
}
RET_GATHER(ret, install_changes_add(changes, n_changes,
INSTALL_CHANGE_MASK_DEPENDENCY, path, *vendor_path));
}
return ret;
}
static int find_symlinks_in_directory(
DIR *dir,
const char *dir_path,
@@ -1103,7 +1332,7 @@ static int find_symlinks_in_directory(
*same_name_link = true;
else if (found_path || found_dest) {
if (ignore_destination) {
r = dependency_entry_is_used(lp, dir_path, de->d_name);
r = dependency_entry_is_used(lp, lp->search_path, dir_path, de->d_name);
if (r < 0)
return r;
if (r == 0)
@@ -2041,6 +2270,131 @@ static int install_info_discover(
return r;
}
static int install_context_mask_dependencies(
InstallContext *ctx,
const LookupPaths *lp,
const char *config_path,
bool dry_run,
InstallChange **changes,
size_t *n_changes) {
_cleanup_strv_free_ char **vendor_search_path = NULL;
InstallInfo *i;
int r, ret = 0;
assert(ctx);
assert(lp);
assert(config_path);
if (ordered_hashmap_isempty(ctx->have_processed))
return 0;
STRV_FOREACH(p, lp->search_path)
if (path_is_vendor(lp, *p)) {
r = strv_extend(&vendor_search_path, *p);
if (r < 0)
return r;
}
if (strv_isempty(vendor_search_path))
return 0;
ORDERED_HASHMAP_FOREACH(i, ctx->have_processed) {
if (i->install_mode != INSTALL_MODE_REGULAR)
continue;
RET_GATHER(ret, install_info_mask_dependencies(ctx->scope, i, lp, vendor_search_path, config_path,
dry_run, changes, n_changes));
}
return ret;
}
/* Drops the dependency masks that disabling the units in ctx created. Creating a symlink replaces the mask
* at the same path. Disabling a template masks every instance the vendor installed a symlink for, and
* enabling the template only creates the symlink of its default instance, so the masks of the other
* instances are removed here. */
static int install_context_unmask_dependencies(
InstallContext *ctx,
const LookupPaths *lp,
const char *config_path,
InstallChange **changes,
size_t *n_changes) {
_cleanup_strv_free_ char **symlinks = NULL;
InstallInfo *i;
int r, ret = 0;
assert(ctx);
assert(lp);
assert(config_path);
/* Called after install_context_apply(), which is what discovers the Also= units and moves everything
* into have_processed. No dry run parameter unlike the masking side: nothing on the enabling path
* implements one, see create_symlink(), and honouring it here alone would make a dry run that is
* asked for anyway do half the work. */
if (ordered_hashmap_isempty(ctx->have_processed))
return 0;
r = dependency_symlinks_list(config_path, &symlinks);
if (r < 0)
return r;
ORDERED_HASHMAP_FOREACH(i, ctx->have_processed) {
if (i->install_mode != INSTALL_MODE_REGULAR)
continue;
STRV_FOREACH(symlink, symlinks) {
_cleanup_free_ char *dir_path = NULL, *dir = NULL, *entry = NULL;
r = path_extract_filename(*symlink, &entry);
if (r < 0)
return r;
r = dependency_name_matches(i, entry);
if (r < 0)
return r;
if (r == 0)
continue;
/* Disabling never creates a mask in a directory the [Install] section does not
* name, so a mask there is the administrator's and stays. */
r = path_extract_directory(*symlink, &dir_path);
if (r < 0)
return r;
r = path_extract_filename(dir_path, &dir);
if (r < 0)
return r;
r = dependency_is_enablement(ctx->scope, i, dir, entry);
if (r < 0)
return r;
if (r == 0)
continue;
r = path_is_mask(lp, *symlink);
if (r < 0)
return r;
if (r == 0)
continue;
if (unlink(*symlink) < 0 && errno != ENOENT) {
RET_GATHER(ret, install_changes_add(changes, n_changes, -errno, *symlink, /* source= */ NULL));
continue;
}
(void) rmdir_parents(*symlink, config_path);
RET_GATHER(ret, install_changes_add(changes, n_changes,
INSTALL_CHANGE_UNMASK_DEPENDENCY, *symlink, /* source= */ NULL));
}
}
return ret;
}
static int install_info_discover_and_check(
InstallContext *ctx,
const LookupPaths *lp,
@@ -3058,7 +3412,7 @@ static int do_unit_file_enable(
_cleanup_(install_context_done) InstallContext ctx = { .scope = scope };
InstallInfo *info;
int r;
int q, r;
STRV_FOREACH(name, names_or_paths) {
r = install_info_discover_and_check(&ctx, lp, *name,
@@ -3075,8 +3429,16 @@ static int do_unit_file_enable(
is useful to determine whether the passed units had any
installation data at all. */
return install_context_apply(&ctx, lp, flags, config_path,
SEARCH_LOAD, changes, n_changes);
r = install_context_apply(&ctx, lp, flags, config_path,
SEARCH_LOAD, changes, n_changes);
if (r < 0)
return r;
q = install_context_unmask_dependencies(&ctx, lp, config_path, changes, n_changes);
if (q < 0)
return q;
return r;
}
int unit_file_enable(
@@ -3147,6 +3509,9 @@ static int do_unit_file_disable(
r = install_context_mark_for_removal(&ctx, lp, &remove_symlinks_to, config_path, changes, n_changes);
if (r >= 0)
r = remove_marked_symlinks(remove_symlinks_to, config_path, lp, flags & UNIT_FILE_DRY_RUN, changes, n_changes);
if (r >= 0)
r = install_context_mask_dependencies(&ctx, lp, config_path, flags & UNIT_FILE_DRY_RUN,
changes, n_changes);
if (r < 0)
return r;
@@ -3812,6 +4177,10 @@ static int execute_preset(
if (r < 0)
return r;
/* Unlike unit_file_disable(), a preset policy of "disable" doesn't mask the symlinks the
* vendor installed below /usr/. Preset files have never needed to name the units the
* vendor enabled. A mask would stop all of those units from starting at boot on existing
* systems with a "disable *" rule. "systemctl disable" still masks them. */
r = remove_marked_symlinks(remove_symlinks_to, config_path, lp, /* dry_run= */ false, changes, n_changes);
} else
r = 0;
@@ -3830,6 +4199,8 @@ static int execute_preset(
else
r += q;
}
RET_GATHER(r, install_context_unmask_dependencies(plus, lp, config_path, changes, n_changes));
}
return r;
@@ -4109,6 +4480,8 @@ DEFINE_STRING_TABLE_LOOKUP(unit_file_state, UnitFileState);
static const char* const install_change_type_table[_INSTALL_CHANGE_TYPE_MAX] = {
[INSTALL_CHANGE_SYMLINK] = "symlink",
[INSTALL_CHANGE_UNLINK] = "unlink",
[INSTALL_CHANGE_MASK_DEPENDENCY] = "mask-dependency",
[INSTALL_CHANGE_UNMASK_DEPENDENCY] = "unmask-dependency",
[INSTALL_CHANGE_IS_MASKED] = "masked",
[INSTALL_CHANGE_IS_MASKED_GENERATOR] = "masked by generator",
[INSTALL_CHANGE_IS_DANGLING] = "dangling",
+7 -1
View File
@@ -16,6 +16,8 @@ typedef enum UnitFilePresetMode {
typedef enum InstallChangeType {
INSTALL_CHANGE_SYMLINK,
INSTALL_CHANGE_UNLINK,
INSTALL_CHANGE_MASK_DEPENDENCY,
INSTALL_CHANGE_UNMASK_DEPENDENCY,
INSTALL_CHANGE_IS_MASKED,
INSTALL_CHANGE_IS_MASKED_GENERATOR,
INSTALL_CHANGE_IS_DANGLING,
@@ -59,7 +61,11 @@ typedef struct InstallChange {
static inline bool install_changes_have_modification(const InstallChange *changes, size_t n_changes) {
FOREACH_ARRAY(i, changes, n_changes)
if (IN_SET(i->type, INSTALL_CHANGE_SYMLINK, INSTALL_CHANGE_UNLINK))
if (IN_SET(i->type,
INSTALL_CHANGE_SYMLINK,
INSTALL_CHANGE_UNLINK,
INSTALL_CHANGE_MASK_DEPENDENCY,
INSTALL_CHANGE_UNMASK_DEPENDENCY))
return true;
return false;
}
+5 -2
View File
@@ -280,7 +280,10 @@ int verb_enable(int argc, char *argv[], uintptr_t data, void *userdata) {
if (streq(verb, "disable") && arg_runtime_scope == RUNTIME_SCOPE_USER && !arg_quiet && !arg_no_warn) {
/* If some of the units are disabled in user scope but still enabled in global scope,
* we emit a warning for that. */
* we emit a warning for that. We check the user scope rather than the global scope. A
* unit enabled in /etc/systemd/user/ is enabled in both. A unit the vendor enabled in
* /usr/lib/systemd/user/ is only enabled in the global scope once the disable masked it
* from the user's configuration. */
/* No strv_free here, strings are owned by 'names' */
_cleanup_free_ char **enabled_in_global_scope = NULL;
@@ -288,7 +291,7 @@ int verb_enable(int argc, char *argv[], uintptr_t data, void *userdata) {
STRV_FOREACH(name, names) {
UnitFileState state;
r = unit_file_get_state(RUNTIME_SCOPE_GLOBAL, arg_root, *name, &state);
r = unit_file_get_state(RUNTIME_SCOPE_USER, arg_root, *name, &state);
if (r == -ENOENT)
continue;
if (r < 0)
+4
View File
@@ -34,6 +34,10 @@ static int show_installation_targets_client_side(const char *name) {
FOREACH_ARRAY(c, changes, n_changes)
if (c->type == INSTALL_CHANGE_UNLINK)
printf(" %s\n", c->path);
else if (c->type == INSTALL_CHANGE_MASK_DEPENDENCY)
/* Enabled by the vendor: report the symlink that does it, not the mask that would
* be created to shadow it. */
printf(" %s\n", c->source);
return 0;
}
+332 -1
View File
@@ -1401,6 +1401,15 @@ static bool symlink_exists(const char *path) {
return is_symlink(path) > 0;
}
static bool is_dependency_mask(const char *path) {
_cleanup_free_ char *dest = NULL;
if (readlink_malloc(path, &dest) < 0)
return false;
return path_equal(dest, "/dev/null");
}
static void write_vendor_file(const char *rel, const char *contents) {
const char *p = strjoina(vendor_root, "/usr/lib/systemd/", rel);
@@ -1438,6 +1447,25 @@ static void do_disable(UnitFileFlags flags, char * const *names) {
ASSERT_OK(unit_file_disable(RUNTIME_SCOPE_SYSTEM, flags, vendor_root, names, &changes, &n_changes));
}
static void do_preset(UnitFileFlags flags, char * const *names) {
InstallChange *changes = NULL;
size_t n_changes = 0;
CLEANUP_ARRAY(changes, n_changes, install_changes_free);
ASSERT_OK(unit_file_preset(RUNTIME_SCOPE_SYSTEM, flags, vendor_root, names, UNIT_FILE_PRESET_FULL,
&changes, &n_changes));
}
static void do_preset_all(UnitFileFlags flags) {
InstallChange *changes = NULL;
size_t n_changes = 0;
CLEANUP_ARRAY(changes, n_changes, install_changes_free);
ASSERT_OK(unit_file_preset_all(RUNTIME_SCOPE_SYSTEM, flags, vendor_root, UNIT_FILE_PRESET_FULL,
&changes, &n_changes));
}
#define VENDOR_WANTS "/usr/lib/systemd/system/multi-user.target.wants/"
#define ETC_WANTS SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/"
#define WANTED_BY_MULTI_USER \
@@ -1448,7 +1476,8 @@ TEST(dependency_mask) {
const char *entry;
/* A .wants/, .requires/ or .upholds/ entry that resolves to /dev/null tells PID 1 to ignore the
* dependency, see process_deps(). */
* dependency, see process_deps(). Disabling must leave the mask alone, because removing it would
* enable the unit again. */
write_vendor_file("system/masked-dep.service", WANTED_BY_MULTI_USER);
@@ -1461,9 +1490,14 @@ TEST(dependency_mask) {
ASSERT_OK_ERRNO(unlink(entry));
ASSERT_OK_ERRNO(symlink("/dev/null", entry));
assert_state("masked-dep.service", UNIT_FILE_DISABLED);
do_disable(0, STRV_MAKE("masked-dep.service"));
ASSERT_TRUE(is_dependency_mask(entry));
assert_state("masked-dep.service", UNIT_FILE_DISABLED);
}
TEST(dependency_mask_empty_file) {
_cleanup_free_ char *dest = NULL;
const char *entry, *empty;
/* PID 1 checks dependency entries with null_or_empty_path(), so a symlink to an empty file is a mask
@@ -1479,6 +1513,28 @@ TEST(dependency_mask_empty_file) {
ASSERT_OK_ERRNO(unlink(entry));
ASSERT_OK_ERRNO(symlink("/etc/empty-mask", entry));
assert_state("empty-mask-dep.service", UNIT_FILE_DISABLED);
do_disable(0, STRV_MAKE("empty-mask-dep.service"));
ASSERT_OK(readlink_malloc(entry, &dest));
ASSERT_STREQ(dest, "/etc/empty-mask");
assert_state("empty-mask-dep.service", UNIT_FILE_DISABLED);
}
TEST(disable_template_keeps_instance_mask) {
const char *mask;
/* Disabling a template removes the symlinks of its instances. A mask of an instance is not an
* enablement symlink, so disabling must keep it. */
write_vendor_file("system/masked-tmpl@.service", WANTED_BY_MULTI_USER);
mask = strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/masked-tmpl@inst.service");
ASSERT_OK_ERRNO(symlink("/dev/null", mask));
assert_state("masked-tmpl@inst.service", UNIT_FILE_MASKED);
do_disable(0, STRV_MAKE("masked-tmpl@.service"));
ASSERT_TRUE(symlink_exists(mask));
assert_state("masked-tmpl@inst.service", UNIT_FILE_MASKED);
}
TEST(dependency_shadow) {
@@ -1600,6 +1656,108 @@ TEST(dependency_shadow_mask) {
assert_state("shadow-masked-dep.service", UNIT_FILE_DISABLED);
}
TEST(vendor_enable) {
InstallChange *changes = NULL;
size_t n_changes = 0;
const char *vendor_link, *mask;
CLEANUP_ARRAY(changes, n_changes, install_changes_free);
ASSERT_ERROR(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, vendor_root, "vendor-enabled.service", /* ret= */ NULL), ENOENT);
write_vendor_file("system/vendor-enabled.service", WANTED_BY_MULTI_USER);
assert_state("vendor-enabled.service", UNIT_FILE_DISABLED);
/* The vendor enables the unit from /usr/, the way distribution packages ship .wants/ symlinks. PID 1
* acts on those, so we must report the unit as enabled. */
write_vendor_symlink("multi-user.target.wants/vendor-enabled.service", "../vendor-enabled.service");
assert_state("vendor-enabled.service", UNIT_FILE_ENABLED);
/* Disabling it cannot remove the vendor symlink, so it has to shadow it with a symlink to /dev/null
* in the configuration directory. */
ASSERT_OK(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, vendor_root,
STRV_MAKE("vendor-enabled.service"), &changes, &n_changes));
ASSERT_EQ(n_changes, 1u);
ASSERT_EQ(changes[0].type, INSTALL_CHANGE_MASK_DEPENDENCY);
vendor_link = strjoina(vendor_root, VENDOR_WANTS"vendor-enabled.service");
mask = strjoina(vendor_root, ETC_WANTS"vendor-enabled.service");
ASSERT_STREQ(changes[0].path, mask);
ASSERT_STREQ(changes[0].source, vendor_link);
ASSERT_TRUE(is_dependency_mask(mask));
ASSERT_TRUE(symlink_exists(vendor_link));
assert_state("vendor-enabled.service", UNIT_FILE_DISABLED);
/* Disabling twice must not undo the mask. */
do_disable(0, STRV_MAKE("vendor-enabled.service"));
ASSERT_TRUE(is_dependency_mask(mask));
assert_state("vendor-enabled.service", UNIT_FILE_DISABLED);
/* Enabling drops the mask again. */
do_enable(0, STRV_MAKE("vendor-enabled.service"));
ASSERT_TRUE(symlink_exists(mask));
ASSERT_FALSE(is_dependency_mask(mask));
assert_state("vendor-enabled.service", UNIT_FILE_ENABLED);
}
TEST(vendor_mask_in_unrelated_target) {
const char *mask;
/* A dependency mask only shadows the entry of the same name in the same .wants/ directory. Masking
* the unit out of graphical.target must not make it look disabled when the vendor pulls it into
* multi-user.target. Enabling leaves that mask alone: disabling never creates a mask in a directory
* the [Install] section does not name, so it is the administrator's. */
write_vendor_file("system/vendor-two-targets.service", WANTED_BY_MULTI_USER);
write_vendor_symlink("multi-user.target.wants/vendor-two-targets.service", "../vendor-two-targets.service");
mask = strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/graphical.target.wants/vendor-two-targets.service");
ASSERT_OK(mkdir_parents(mask, 0755));
ASSERT_OK_ERRNO(symlink("/dev/null", mask));
assert_state("vendor-two-targets.service", UNIT_FILE_ENABLED);
do_disable(0, STRV_MAKE("vendor-two-targets.service"));
assert_state("vendor-two-targets.service", UNIT_FILE_DISABLED);
do_enable(0, STRV_MAKE("vendor-two-targets.service"));
ASSERT_TRUE(is_dependency_mask(mask));
assert_state("vendor-two-targets.service", UNIT_FILE_ENABLED);
}
TEST(vendor_preset_leaves_vendor_enablement_alone) {
const char *vendor_link, *mask;
/* A preset policy of "disable" does not reach what the vendor enabled below /usr/. Masking it would
* take units out of the boot on every existing system whose preset files do not name what it wires
* up, which they never had to. Asking for it by name still works. */
write_vendor_file("system/vendor-preset-off.service", WANTED_BY_MULTI_USER);
write_vendor_symlink("multi-user.target.wants/vendor-preset-off.service", "../vendor-preset-off.service");
write_vendor_file("system-preset/13-vendor-off.preset", "disable vendor-preset-off.service\n");
do_preset(0, STRV_MAKE("vendor-preset-off.service"));
vendor_link = strjoina(vendor_root, VENDOR_WANTS"vendor-preset-off.service");
mask = strjoina(vendor_root, ETC_WANTS"vendor-preset-off.service");
ASSERT_FALSE(symlink_exists(mask));
ASSERT_TRUE(symlink_exists(vendor_link));
assert_state("vendor-preset-off.service", UNIT_FILE_ENABLED);
/* An explicit disable is a different matter. */
do_disable(0, STRV_MAKE("vendor-preset-off.service"));
ASSERT_TRUE(is_dependency_mask(mask));
assert_state("vendor-preset-off.service", UNIT_FILE_DISABLED);
/* And presetting it back to "enable" clears that mask again. */
write_vendor_file("system-preset/13-vendor-off.preset", "enable vendor-preset-off.service\n");
do_preset(0, STRV_MAKE("vendor-preset-off.service"));
ASSERT_FALSE(is_dependency_mask(mask));
assert_state("vendor-preset-off.service", UNIT_FILE_ENABLED);
}
TEST(vendor_symlinked_dependency_dir_is_not_followed) {
/* A dependency directory that is itself a symlink is not entered. An absolute one would resolve
* against the host rather than against --root=, which would have us report on and mask in the
@@ -1618,6 +1776,89 @@ TEST(vendor_symlinked_dependency_dir_is_not_followed) {
ASSERT_FALSE(symlink_exists(strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/escape.target.wants/vendor-escape.service")));
}
TEST(vendor_requires_and_upholds) {
/* The three dependency directory kinds go through the same code, but only .wants/ is covered above. */
FOREACH_STRING(suffix, "requires", "upholds") {
_cleanup_free_ char *name = NULL, *rel = NULL, *mask = NULL, *unit = NULL;
ASSERT_NOT_NULL(name = strjoin("vendor-", suffix, ".service"));
ASSERT_NOT_NULL(rel = strjoin("multi-user.target.", suffix, "/", name));
ASSERT_NOT_NULL(mask = strjoin(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.",
suffix, "/", name));
ASSERT_NOT_NULL(unit = strjoin("[Install]\n",
streq(suffix, "requires") ? "RequiredBy" : "UpheldBy",
"=multi-user.target\n"));
write_vendor_file(strjoina("system/", name), unit);
write_vendor_symlink(rel, strjoina("../", name));
assert_state(name, UNIT_FILE_ENABLED);
do_disable(0, STRV_MAKE(name));
ASSERT_TRUE(is_dependency_mask(mask));
assert_state(name, UNIT_FILE_DISABLED);
do_enable(0, STRV_MAKE(name));
ASSERT_FALSE(is_dependency_mask(mask));
assert_state(name, UNIT_FILE_ENABLED);
}
}
TEST(vendor_template_instance) {
const char *mask;
/* Disabling a template has to mask the vendor's instance symlinks, the same way removal takes away
* the configured ones. Enabling the template only creates the symlink of the default instance, so
* the masks of the other instances have to be removed separately. */
write_vendor_file("system/vendor-tmpl@.service",
"[Install]\n"
"DefaultInstance=dflt\n"
"WantedBy=multi-user.target\n");
write_vendor_symlink("multi-user.target.wants/vendor-tmpl@inst.service", "../vendor-tmpl@.service");
assert_state("vendor-tmpl@inst.service", UNIT_FILE_ENABLED);
/* Disabling the template must reach the instance the vendor wired up. */
do_disable(0, STRV_MAKE("vendor-tmpl@.service"));
mask = strjoina(vendor_root, ETC_WANTS"vendor-tmpl@inst.service");
ASSERT_TRUE(is_dependency_mask(mask));
ASSERT_TRUE(symlink_exists(strjoina(vendor_root, VENDOR_WANTS"vendor-tmpl@inst.service")));
assert_state("vendor-tmpl@inst.service", UNIT_FILE_DISABLED);
do_enable(0, STRV_MAKE("vendor-tmpl@.service"));
ASSERT_TRUE(symlink_exists(strjoina(vendor_root, ETC_WANTS"vendor-tmpl@dflt.service")));
ASSERT_FALSE(symlink_exists(mask));
assert_state("vendor-tmpl@inst.service", UNIT_FILE_ENABLED);
}
TEST(vendor_also) {
const char *mask;
/* Disabling has to mask the vendor symlinks of the Also= units too, and enabling has to clear them
* again. Those units are only discovered while the symlinks are being applied. */
write_vendor_file("system/vendor-also.service",
"[Install]\n"
"WantedBy=multi-user.target\n"
"Also=vendor-also.socket\n");
write_vendor_file("system/vendor-also.socket",
"[Install]\n"
"WantedBy=sockets.target\n");
write_vendor_symlink("sockets.target.wants/vendor-also.socket", "../vendor-also.socket");
do_disable(0, STRV_MAKE("vendor-also.service"));
mask = strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/sockets.target.wants/vendor-also.socket");
ASSERT_TRUE(is_dependency_mask(mask));
assert_state("vendor-also.socket", UNIT_FILE_DISABLED);
do_enable(0, STRV_MAKE("vendor-also.service"));
ASSERT_FALSE(is_dependency_mask(mask));
assert_state("vendor-also.socket", UNIT_FILE_ENABLED);
}
TEST(vendor_mask_shadows_lower_priority_dir) {
const char *high_mask, *mask;
@@ -1639,6 +1880,34 @@ TEST(vendor_mask_shadows_lower_priority_dir) {
ASSERT_FALSE(symlink_exists(mask));
}
TEST(vendor_multiple_units) {
/* Disabling and enabling several units at once, each with a symlink in two targets, must go
* through every unit and every mask rather than stop at the first. */
FOREACH_STRING(name, "vendor-multi-a.service", "vendor-multi-b.service", "vendor-multi-c.service") {
write_vendor_file(strjoina("system/", name),
"[Install]\n"
"WantedBy=multi-user.target graphical.target\n");
FOREACH_STRING(target, "multi-user.target", "graphical.target")
write_vendor_symlink(strjoina(target, ".wants/", name), strjoina("../", name));
}
do_disable(0, STRV_MAKE("vendor-multi-a.service", "vendor-multi-b.service", "vendor-multi-c.service"));
FOREACH_STRING(name, "vendor-multi-a.service", "vendor-multi-b.service", "vendor-multi-c.service")
FOREACH_STRING(target, "multi-user.target", "graphical.target")
ASSERT_TRUE(is_dependency_mask(strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/", target,
".wants/", name)));
do_enable(0, STRV_MAKE("vendor-multi-a.service", "vendor-multi-b.service", "vendor-multi-c.service"));
FOREACH_STRING(name, "vendor-multi-a.service", "vendor-multi-b.service", "vendor-multi-c.service")
FOREACH_STRING(target, "multi-user.target", "graphical.target")
ASSERT_FALSE(is_dependency_mask(strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/", target,
".wants/", name)));
}
TEST(vendor_entry_kinds) {
const char *entry;
@@ -1703,6 +1972,13 @@ TEST(vendor_static_dependency) {
write_vendor_symlink("multi-user.target.wants/vendor-static-dep.service", "../vendor-static-dep.service");
assert_state("vendor-static-dep.service", UNIT_FILE_ENABLED);
/* Disabling masks the symlink that enables the unit and leaves the static dependency symlinks alone. */
do_disable(0, STRV_MAKE("vendor-static-dep.service"));
ASSERT_TRUE(is_dependency_mask(strjoina(vendor_root, ETC_WANTS"vendor-static-dep.service")));
ASSERT_FALSE(symlink_exists(strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/initrd-root-device.target.wants/vendor-static-dep.service")));
ASSERT_FALSE(symlink_exists(strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.requires/vendor-static-dep.service")));
assert_state("vendor-static-dep.service", UNIT_FILE_DISABLED);
/* An instance with a static dependency symlink reports as "static", like an instance with an alias
* symlink below /usr/ does. */
write_vendor_file("system/vendor-static-dep@.service", WANTED_BY_MULTI_USER);
@@ -1711,6 +1987,61 @@ TEST(vendor_static_dependency) {
write_vendor_symlink("multi-user.target.wants/vendor-static-dep@b.service", "../vendor-static-dep@.service");
assert_state("vendor-static-dep@b.service", UNIT_FILE_ENABLED);
do_disable(0, STRV_MAKE("vendor-static-dep@.service"));
ASSERT_TRUE(is_dependency_mask(strjoina(vendor_root, ETC_WANTS"vendor-static-dep@b.service")));
ASSERT_FALSE(symlink_exists(strjoina(vendor_root, SYSTEM_CONFIG_UNIT_DIR"/initrd-root-device.target.wants/vendor-static-dep@a.service")));
assert_state("vendor-static-dep@a.service", UNIT_FILE_STATIC);
assert_state("vendor-static-dep@b.service", UNIT_FILE_DISABLED);
}
TEST(vendor_dry_run_writes_nothing) {
InstallChange *changes = NULL;
size_t n_changes = 0;
/* "is-enabled --full" drives a dry run through the masking code to list what enables the unit. A
* regression there would disable units from a read-only query. */
CLEANUP_ARRAY(changes, n_changes, install_changes_free);
write_vendor_file("system/vendor-dry.service", WANTED_BY_MULTI_USER);
write_vendor_symlink("multi-user.target.wants/vendor-dry.service", "../vendor-dry.service");
ASSERT_OK(unit_file_disable(RUNTIME_SCOPE_SYSTEM, UNIT_FILE_DRY_RUN, vendor_root,
STRV_MAKE("vendor-dry.service"), &changes, &n_changes));
ASSERT_EQ(n_changes, 1u);
ASSERT_EQ(changes[0].type, INSTALL_CHANGE_MASK_DEPENDENCY);
ASSERT_STREQ(changes[0].source, strjoina(vendor_root, VENDOR_WANTS"vendor-dry.service"));
ASSERT_FALSE(symlink_exists(strjoina(vendor_root, ETC_WANTS"vendor-dry.service")));
assert_state("vendor-dry.service", UNIT_FILE_ENABLED);
}
TEST(vendor_also_keeps_sibling_preset_policy) {
/* Presetting a unit must not drag its Also= units into the removal set: they have a preset policy of
* their own, which by then has either already been applied or is yet to be. */
write_vendor_file("system/vendor-main.service",
"[Install]\n"
"WantedBy=multi-user.target\n"
"Also=vendor-aux.socket\n");
write_vendor_file("system/vendor-aux.socket",
"[Install]\n"
"WantedBy=sockets.target\n");
write_vendor_file("system-preset/20-vendor-mixed.preset",
"disable vendor-main.service\n"
"enable vendor-aux.socket\n");
do_enable(0, STRV_MAKE("vendor-aux.socket"));
assert_state("vendor-aux.socket", UNIT_FILE_ENABLED);
do_preset(0, STRV_MAKE("vendor-main.service"));
assert_state("vendor-main.service", UNIT_FILE_DISABLED);
assert_state("vendor-aux.socket", UNIT_FILE_ENABLED);
do_preset_all(0);
assert_state("vendor-main.service", UNIT_FILE_DISABLED);
assert_state("vendor-aux.socket", UNIT_FILE_ENABLED);
}
static int intro(void) {