From c74e3aa5b7fd656aad903a6064c8b01b7801498c Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 7 Aug 2026 10:57:22 +0200 Subject: [PATCH] install: preset in the scope the caller asked for unit_file_preset() and unit_file_preset_all() zero-initialized their install contexts, so their scope was always RUNTIME_SCOPE_SYSTEM. In the global scope, specifiers in [Install] were then resolved as if for the system: %U became 0 and a unit got enabled into target-0.target.wants/. Set the scope the caller asked for, so that this fails with -EUNATCH instead. Covered by a new test that presets a unit using %U in the global scope, with both functions. (cherry picked from commit 0a4491ef3fce30d31a35c95031c8026a0235e0a2) --- src/shared/install.c | 4 ++-- src/test/test-install-root.c | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/shared/install.c b/src/shared/install.c index f8f05c624f..23397f7990 100644 --- a/src/shared/install.c +++ b/src/shared/install.c @@ -3688,7 +3688,7 @@ int unit_file_preset( InstallChange **changes, size_t *n_changes) { - _cleanup_(install_context_done) InstallContext plus = {}, minus = {}; + _cleanup_(install_context_done) InstallContext plus = { .scope = scope }, minus = { .scope = scope }; _cleanup_(lookup_paths_done) LookupPaths lp = {}; _cleanup_(unit_file_presets_done) UnitFilePresets presets = {}; const char *config_path; @@ -3727,7 +3727,7 @@ int unit_file_preset_all( InstallChange **changes, size_t *n_changes) { - _cleanup_(install_context_done) InstallContext plus = {}, minus = {}; + _cleanup_(install_context_done) InstallContext plus = { .scope = scope }, minus = { .scope = scope }; _cleanup_(lookup_paths_done) LookupPaths lp = {}; _cleanup_(unit_file_presets_done) UnitFilePresets presets = {}; const char *config_path = NULL; diff --git a/src/test/test-install-root.c b/src/test/test-install-root.c index 2545b1fd04..9786675b4c 100644 --- a/src/test/test-install-root.c +++ b/src/test/test-install-root.c @@ -10,6 +10,7 @@ #include "path-util.h" #include "rm-rf.h" #include "special.h" +#include "stat-util.h" #include "string-util.h" #include "tests.h" #include "tmpfile-util.h" @@ -1131,6 +1132,48 @@ TEST(preset_multiple_instances) { install_changes_free(changes, n_changes); } +TEST(preset_scope) { + const char *unit, *preset, *link; + InstallChange *changes = NULL; + size_t n_changes = 0; + + /* %U can't be resolved in the global scope, since there's no user to resolve it to. Make sure + * presetting in the global scope fails because of that, instead of resolving specifiers in the + * system scope (where %U is 0) and creating a target-0.target.wants/ symlink. Check both + * unit_file_preset() and unit_file_preset_all(), they set up their install contexts separately. */ + + unit = strjoina(root, "/usr/lib/systemd/user/scoped.service"); + ASSERT_OK(mkdir_parents(unit, 0755)); + ASSERT_OK(write_string_file(unit, + "[Install]\n" + "WantedBy=target-%U.target\n", WRITE_STRING_FILE_CREATE)); + + preset = strjoina(root, "/usr/lib/systemd/user-preset/50-scoped.preset"); + ASSERT_OK(mkdir_parents(preset, 0755)); + ASSERT_OK(write_string_file(preset, "enable scoped.service\n", WRITE_STRING_FILE_CREATE)); + + ASSERT_ERROR(unit_file_preset(RUNTIME_SCOPE_GLOBAL, 0, root, STRV_MAKE("scoped.service"), + UNIT_FILE_PRESET_FULL, &changes, &n_changes), EUNATCH); + ASSERT_EQ(n_changes, 1u); + ASSERT_EQ(changes[0].type, -EUNATCH); + ASSERT_STREQ(changes[0].path, "target-%U.target"); + install_changes_free(changes, n_changes); + changes = NULL; n_changes = 0; + + link = strjoina(root, USER_CONFIG_UNIT_DIR"/target-0.target.wants/scoped.service"); + ASSERT_ERROR(is_symlink(link), ENOENT); + + ASSERT_ERROR(unit_file_preset_all(RUNTIME_SCOPE_GLOBAL, 0, root, UNIT_FILE_PRESET_FULL, + &changes, &n_changes), EUNATCH); + ASSERT_EQ(n_changes, 1u); + ASSERT_EQ(changes[0].type, -EUNATCH); + ASSERT_STREQ(changes[0].path, "target-%U.target"); + install_changes_free(changes, n_changes); + changes = NULL; n_changes = 0; + + ASSERT_ERROR(is_symlink(link), ENOENT); +} + static void verify_one( const InstallInfo *i, const char *alias,