From fcb2c6c259abce485211bdc4be409e4c13213481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 30 Jun 2026 13:59:41 +0200 Subject: [PATCH 01/17] meson: replace -Dbuild-executor-shared=single with -Dsystemd-multicall-binary=true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This effectively reverts the meson config changes introduced in 0c6186695a697831a7e5ccde0852ea35d4b2409b. I wasn't happy with that form back when it was commited, and in hindsight, I like it even less. Let's just add a new option that hopefully is easier to understand and will work better when new functionality is added in the future. Now there are two options: link-executor-shared (boolean, defaults to true) — "link (separate) systemd-executor to libsystemd-shared.so and libsystemd-core.so" systemd-multicall-binary (also boolean, defaults to false) — "link systemd+systemd-executor as a single binary" Build directories will need to be recreated again, sorry! --- meson.build | 3 +-- meson_options.txt | 6 ++++-- src/core/executor.c | 2 +- src/core/main.c | 2 +- src/core/manager.c | 2 +- src/core/meson.build | 8 +++----- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/meson.build b/meson.build index 20eba736d7..0fcd12f0bd 100644 --- a/meson.build +++ b/meson.build @@ -78,8 +78,7 @@ conf.set10('FUZZ_USE_SIZE_LIMIT', fuzzer_build) # We'll set this to '1' for EFI builds in a different place. conf.set10('SD_BOOT', false) -link_executor_shared = get_option('link-executor-shared') -conf.set10('BUILD_EXECUTOR_SINGLE', link_executor_shared == 'single') +conf.set10('SYSTEMD_MULTICALL_BINARY', get_option('systemd-multicall-binary')) # Create a title-less summary section early, so it ends up first in the output. # More items are added later after they have been detected. diff --git a/meson_options.txt b/meson_options.txt index 701272d13a..ced6fac1e3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -15,8 +15,10 @@ option('split-bin', type : 'combo', choices : ['auto', 'true', 'false'], description : 'sbin is not a symlink to bin') option('link-udev-shared', type : 'boolean', description : 'link systemd-udevd and its helpers to libsystemd-shared.so') -option('link-executor-shared', type : 'combo', choices : ['true', 'false', 'single'], - description : 'link systemd-executor to libsystemd-shared.so and libsystemd-core.so, or into systemd') +option('systemd-multicall-binary', type : 'boolean', value : false, + description : 'link systemd+systemd-executor as a single binary') +option('link-executor-shared', type : 'boolean', + description : 'link (separate) systemd-executor to libsystemd-shared.so and libsystemd-core.so') option('link-systemctl-shared', type: 'boolean', description : 'link systemctl against libsystemd-shared.so') option('link-networkd-shared', type: 'boolean', diff --git a/src/core/executor.c b/src/core/executor.c index b227584789..e771bdc024 100644 --- a/src/core/executor.c +++ b/src/core/executor.c @@ -245,6 +245,6 @@ int run_executor(int argc, char *argv[]) { return r < 0 ? EXIT_FAILURE : r; } -#if !BUILD_EXECUTOR_SINGLE +#if !SYSTEMD_MULTICALL_BINARY _alias_(run_executor) main; #endif diff --git a/src/core/main.c b/src/core/main.c index 01911630dc..c476400a93 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -3932,7 +3932,7 @@ finish: } int main(int argc, char *argv[]) { -#if BUILD_EXECUTOR_SINGLE +#if SYSTEMD_MULTICALL_BINARY if (invoked_as(argv, "executor")) return run_executor(argc, argv); #endif diff --git a/src/core/manager.c b/src/core/manager.c index eedb5e710d..7fe2fe0a01 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -900,7 +900,7 @@ static int pin_executor_binary(int *ret_fd) { assert(ret_fd); -#if BUILD_EXECUTOR_SINGLE +#if SYSTEMD_MULTICALL_BINARY int r; r = open_and_check_executable("/proc/self/exe", /* root= */ NULL, &path, ret_fd); diff --git a/src/core/meson.build b/src/core/meson.build index 6e68a59d20..3708caf4e0 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -209,11 +209,9 @@ systemd_deps = [ libselinux_cflags, ] -link_executor_shared = get_option('link-executor-shared') +executor_libs = get_option('link-executor-shared') ? core_libs_shared : core_libs_static -executor_libs = link_executor_shared == 'true' ? core_libs_shared : core_libs_static - -if link_executor_shared == 'single' +if conf.get('SYSTEMD_MULTICALL_BINARY') == 1 systemd_sources += systemd_executor_sources systemd_deps += [ libbpf_cflags, @@ -256,7 +254,7 @@ executables += [ }, ] -if link_executor_shared == 'single' +if conf.get('SYSTEMD_MULTICALL_BINARY') == 1 # Symlink for external callers install_symlink('systemd-executor', pointing_to : 'systemd', From aac88fd8ab84daceb9f2b4bafb5da62c2c3b1716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 19 May 2026 16:22:07 +0200 Subject: [PATCH 02/17] user-util: add lookup_pwent_in_files and lookup_grent_in_files Those functions will be used later. This commits adds the implementations and tests for them. glibc functions require ability to open nss modules, which requires dlopen. Linking will fail if any of the nss-using functions are used. So the following few commits will provide replacements for all of them. --- src/basic/user-util.c | 165 ++++++++++++++++++++++++++++++++++++++ src/basic/user-util.h | 11 +++ src/test/test-user-util.c | 149 ++++++++++++++++++++++++++++++++++ 3 files changed, 325 insertions(+) diff --git a/src/basic/user-util.c b/src/basic/user-util.c index a665a2d780..8d9474fe21 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -1090,6 +1090,87 @@ const char* get_home_root(void) { return "/home"; } +static int copy_struct_passwd(const struct passwd *pw, struct passwd **ret) { + assert(pw); + assert(ret); + + size_t need_bytes = sizeof(struct passwd) + + strlen_ptr(pw->pw_name) + 1 + + strlen_ptr(pw->pw_passwd) + 1 + + strlen_ptr(pw->pw_gecos) + 1 + + strlen_ptr(pw->pw_dir) + 1 + + strlen_ptr(pw->pw_shell) + 1; + + char *buf = malloc(need_bytes); + if (!buf) + return -ENOMEM; + + struct passwd *newpw = (struct passwd *) buf; + + /* The layout in our buffer: + * struct passwd, and then individual strings. */ + char *p = buf + sizeof(struct passwd); + + newpw->pw_name = p; + p = stpcpy(p, strempty(pw->pw_name)) + 1; + + newpw->pw_passwd = p; + p = stpcpy(p, strempty(pw->pw_passwd)) + 1; + + newpw->pw_uid = pw->pw_uid; + newpw->pw_gid = pw->pw_gid; + + newpw->pw_gecos = p; + p = stpcpy(p, strempty(pw->pw_gecos)) + 1; + + newpw->pw_dir = p; + p = stpcpy(p, strempty(pw->pw_dir)) + 1; + + newpw->pw_shell = p; + p = stpcpy(p, strempty(pw->pw_shell)) + 1; + + *ret = newpw; + return 0; +} + +/* Iterate the given list of passwd-format files looking for an entry matching the predicate (by + * name if 'name' is non-NULL and by 'uid' if valid). Returns -ESRCH if no entry is found. */ +int lookup_pwent_in_files( + char * const *files, + const char *name, + uid_t uid, + struct passwd **ret) { + + int r; + + assert(files); + assert(name || uid_is_valid(uid)); + + STRV_FOREACH(fname, files) { + _cleanup_fclose_ FILE *f = NULL; + struct passwd *pw; + + r = fopen_unlocked(*fname, "re", &f); + if (r == -ENOENT) + continue; + if (r < 0) + return r; + + while ((r = fgetpwent_sane(f, &pw)) > 0) { + if (name && !streq_ptr(pw->pw_name, name)) + continue; + if (uid_is_valid(uid) && pw->pw_uid != uid) + continue; + if (ret) + return copy_struct_passwd(pw, ret); + return 0; + } + if (r < 0) + return r; + } + + return -ESRCH; +} static size_t getpw_buffer_size(void) { long bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); return bufsize <= 0 ? 4096U : (size_t) bufsize; @@ -1188,6 +1269,90 @@ int getpwuid_malloc(uid_t uid, struct passwd **ret) { } } +static int copy_struct_group(const struct group *gr, struct group **ret) { + assert(gr); + assert(ret); + + size_t need_bytes = sizeof(struct group) + + strlen_ptr(gr->gr_name) + 1 + + strlen_ptr(gr->gr_passwd) + 1, + n_mem = 0; + STRV_FOREACH(s, gr->gr_mem) { + need_bytes += sizeof(char*) + strlen(*s) + 1; + n_mem++; + } + need_bytes += sizeof(char*); /* NULL terminator for gr_mem */ + + char *buf = malloc(need_bytes); + if (!buf) + return -ENOMEM; + + struct group *newgr = (struct group *) buf; + + /* The layout in our buffer: + * struct group, ->gr_mem pointers terminated by NULL, ->gr_name, ->gr_passwd, ->gr_mem items */ + /* The ->gr_mem array is first, because it needs alignment. */ + assert_cc(sizeof(struct group) % alignof(char*) == 0); + + char *p = buf + sizeof(struct group) + (n_mem + 1) * sizeof(char*); + + newgr->gr_name = p; + p = stpcpy(p, strempty(gr->gr_name)) + 1; + + newgr->gr_passwd = p; + p = stpcpy(p, strempty(gr->gr_passwd)) + 1; + + newgr->gr_gid = gr->gr_gid; + + newgr->gr_mem = (char**) (buf + sizeof(struct group)); + for (size_t i = 0; i < n_mem; i++) { + newgr->gr_mem[i] = p; + p = stpcpy(p, gr->gr_mem[i]) + 1; + } + newgr->gr_mem[n_mem] = NULL; + + *ret = newgr; + return 0; +} + +/* See lookup_pwent_in_files() for the analogous passwd-file version. */ +int lookup_grent_in_files( + char * const *files, + const char *name, + gid_t gid, + struct group **ret) { + + int r; + + assert(files); + assert(name || gid_is_valid(gid)); + + STRV_FOREACH(fname, files) { + _cleanup_fclose_ FILE *f = NULL; + struct group *gr; + + r = fopen_unlocked(*fname, "re", &f); + if (r == -ENOENT) + continue; + if (r < 0) + return r; + + while ((r = fgetgrent_sane(f, &gr)) > 0) { + if (name && !streq_ptr(gr->gr_name, name)) + continue; + if (gid_is_valid(gid) && gr->gr_gid != gid) + continue; + if (ret) + return copy_struct_group(gr, ret); + return 0; + } + if (r < 0) + return r; + } + + return -ESRCH; +} + static size_t getgr_buffer_size(void) { long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX); return bufsize <= 0 ? 4096U : (size_t) bufsize; diff --git a/src/basic/user-util.h b/src/basic/user-util.h index 641bdc06b0..f45e5bddfd 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -173,6 +173,17 @@ static inline bool hashed_password_is_locked_or_invalid(const char *password) { */ #define PASSWORD_UNPROVISIONED "!unprovisioned" +int lookup_pwent_in_files( + char * const *files, + const char *name, + uid_t uid, + struct passwd **ret); +int lookup_grent_in_files( + char * const *files, + const char *name, + gid_t gid, + struct group **ret); + int getpwuid_malloc(uid_t uid, struct passwd **ret); int getpwnam_malloc(const char *name, struct passwd **ret); diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c index 53ad9422f3..e0c8b68fc1 100644 --- a/src/test/test-user-util.c +++ b/src/test/test-user-util.c @@ -3,12 +3,15 @@ #include #include "alloc-util.h" +#include "fd-util.h" +#include "fileio.h" #include "format-util.h" #include "log.h" #include "memory-util.h" #include "path-util.h" #include "string-util.h" #include "tests.h" +#include "tmpfile-util.h" #include "user-util.h" static void test_uid_to_name_one(uid_t uid, const char *name) { @@ -447,4 +450,150 @@ TEST(mangle_gecos) { test_mangle_gecos_one("\xe2\x28\xa1", " ( "); } +TEST(lookup_pwent_in_files) { + _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-user-util-passwd-XXXXXX"; + _cleanup_fclose_ FILE *f = NULL; + + ASSERT_OK(fmkostemp_safe(fn, "w", &f)); + ASSERT_OK(write_string_stream( + f, + "testuser1:x:1234:5678:Test User 1:/home/test1:/bin/sh\n" + "testuser2:x:5678:9999:Test User 2:/home/test2:/bin/bash\n", + /* flags= */ 0)); + ASSERT_OK(fflush_and_check(f)); + + char **files = STRV_MAKE(fn); + + /* Lookup by name */ + _cleanup_free_ struct passwd *pw = NULL; + ASSERT_OK(lookup_pwent_in_files(files, "testuser1", UID_INVALID, &pw)); + ASSERT_STREQ(pw->pw_name, "testuser1"); + ASSERT_EQ(pw->pw_uid, 1234u); + ASSERT_EQ(pw->pw_gid, 5678u); + ASSERT_STREQ(pw->pw_gecos, "Test User 1"); + ASSERT_STREQ(pw->pw_dir, "/home/test1"); + ASSERT_STREQ(pw->pw_shell, "/bin/sh"); + pw = mfree(pw); + + ASSERT_OK(lookup_pwent_in_files(STRV_MAKE(fn, fn, fn, fn), "testuser2", UID_INVALID, &pw)); + ASSERT_STREQ(pw->pw_name, "testuser2"); + ASSERT_EQ(pw->pw_uid, 5678u); + ASSERT_STREQ(pw->pw_shell, "/bin/bash"); + pw = mfree(pw); + + /* Caller doesn't care about contents */ + ASSERT_OK(lookup_pwent_in_files(files, "testuser1", UID_INVALID, /* ret= */ NULL)); + + /* Missing entry */ + ASSERT_ERROR(lookup_pwent_in_files(files, "nosuchuser", UID_INVALID, /* ret= */ NULL), ESRCH); + ASSERT_ERROR(lookup_pwent_in_files(files, "nosuchuser", UID_INVALID, &pw), ESRCH); + ASSERT_NULL(pw); + + /* Lookup by uid */ + ASSERT_OK(lookup_pwent_in_files(files, /* name= */ NULL, 1234, &pw)); + ASSERT_STREQ(pw->pw_name, "testuser1"); + pw = mfree(pw); + + ASSERT_OK(lookup_pwent_in_files(files, /* name= */ NULL, 5678, &pw)); + ASSERT_STREQ(pw->pw_name, "testuser2"); + pw = mfree(pw); + + /* Missing uid */ + ASSERT_ERROR(lookup_pwent_in_files(files, /* name= */ NULL, 424242, &pw), ESRCH); + ASSERT_ERROR(lookup_pwent_in_files(files, /* name= */ NULL, 424242, NULL), ESRCH); + + /* Non-existent file in the list is skipped */ + ASSERT_OK(lookup_pwent_in_files( + STRV_MAKE("/nonexistent-dir/passwd", fn), + "testuser1", + UID_INVALID, + &pw)); + ASSERT_STREQ(pw->pw_name, "testuser1"); + pw = mfree(pw); + + /* All files missing → -ESRCH */ + ASSERT_ERROR(lookup_pwent_in_files( + STRV_MAKE("/nonexistent-dir/passwd"), + "testuser1", + UID_INVALID, + &pw), ESRCH); + + /* First match wins: same name in two files, but the first file's entry should be picked */ + _cleanup_(unlink_tempfilep) char fn2[] = "/tmp/test-user-util-passwd2-XXXXXX"; + _cleanup_fclose_ FILE *f2 = NULL; + + ASSERT_OK(fmkostemp_safe(fn2, "w", &f2)); + ASSERT_OK(write_string_stream( + f2, + "testuser1:x:111:222:First:/h/first:/bin/zsh\n", + /* flags= */ 0)); + ASSERT_OK(fflush_and_check(f2)); + + ASSERT_OK(lookup_pwent_in_files( + STRV_MAKE(fn2, fn), + "testuser1", + UID_INVALID, + &pw)); + ASSERT_EQ(pw->pw_uid, 111u); + ASSERT_STREQ(pw->pw_gecos, "First"); +} + +TEST(lookup_grent_in_files) { + _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-user-util-group-XXXXXX"; + _cleanup_fclose_ FILE *f = NULL; + + ASSERT_OK(fmkostemp_safe(fn, "w", &f)); + ASSERT_OK(write_string_stream( + f, + "testgroup1:x:100:testuser1,testuser2\n" + "testgroup2:x:200:\n", + /* flags= */ 0)); + ASSERT_OK(fflush_and_check(f)); + + char **files = STRV_MAKE(fn); + + /* Lookup by name */ + _cleanup_free_ struct group *gr = NULL; + ASSERT_OK(lookup_grent_in_files(files, "testgroup1", GID_INVALID, &gr)); + ASSERT_STREQ(gr->gr_name, "testgroup1"); + ASSERT_EQ(gr->gr_gid, 100u); + ASSERT_STREQ(gr->gr_mem[0], "testuser1"); + ASSERT_STREQ(gr->gr_mem[1], "testuser2"); + ASSERT_NULL(gr->gr_mem[2]); + gr = mfree(gr); + + ASSERT_OK(lookup_grent_in_files(files, "testgroup2", GID_INVALID, &gr)); + ASSERT_STREQ(gr->gr_name, "testgroup2"); + ASSERT_EQ(gr->gr_gid, 200u); + ASSERT_NULL(gr->gr_mem[0]); + gr = mfree(gr); + + /* Caller doesn't care about contents */ + ASSERT_OK(lookup_grent_in_files(files, "testgroup1", GID_INVALID, /* ret= */ NULL)); + + /* Missing entry */ + ASSERT_ERROR(lookup_grent_in_files(files, "nosuchgroup", GID_INVALID, &gr), ESRCH); + ASSERT_NULL(gr); + + /* Lookup by gid */ + ASSERT_OK(lookup_grent_in_files(files, /* name= */ NULL, 100, &gr)); + ASSERT_STREQ(gr->gr_name, "testgroup1"); + gr = mfree(gr); + + ASSERT_OK(lookup_grent_in_files(files, /* name= */ NULL, 200, &gr)); + ASSERT_STREQ(gr->gr_name, "testgroup2"); + gr = mfree(gr); + + /* Missing gid */ + ASSERT_ERROR(lookup_grent_in_files(files, /* name= */ NULL, 424242, &gr), ESRCH); + + /* Non-existent file is skipped */ + ASSERT_OK(lookup_grent_in_files( + STRV_MAKE("/nonexistent-dir/group", fn), + "testgroup1", + GID_INVALID, + &gr)); + ASSERT_STREQ(gr->gr_name, "testgroup1"); +} + DEFINE_TEST_MAIN(LOG_INFO); From 2c01b4895efe69a52c8eb2da2c15f4641064575e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 21 May 2026 10:11:47 +0200 Subject: [PATCH 03/17] user-util: sysconf_ngroups_max This hides the iffy interface of sysconf(_SC_NGROUPS_MAX) to simplify the calllers. --- src/basic/socket-util.c | 3 ++- src/basic/user-util.c | 13 +++++++++++++ src/basic/user-util.h | 2 ++ src/core/exec-invoke.c | 11 +++-------- src/test/test-condition.c | 4 ++-- src/test/test-socket-util.c | 4 +--- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/basic/socket-util.c b/src/basic/socket-util.c index 13537593cf..1fc8e67204 100644 --- a/src/basic/socket-util.c +++ b/src/basic/socket-util.c @@ -39,6 +39,7 @@ #include "strv.h" #include "sysctl-util.h" #include "tmpfile-util.h" +#include "user-util.h" #include "xattr-util.h" #if ENABLE_IDN @@ -914,7 +915,7 @@ int getpeergroups(int fd, gid_t **ret) { assert(fd >= 0); assert(ret); - long ngroups_max = sysconf(_SC_NGROUPS_MAX); + int ngroups_max = sysconf_ngroups_max(); if (ngroups_max > 0) n = MAX(n, sizeof(gid_t) * (socklen_t) ngroups_max); diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 8d9474fe21..869fabdebd 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -1353,6 +1353,19 @@ int lookup_grent_in_files( return -ESRCH; } +int sysconf_ngroups_max(void) { + /* Query sysconf _SC_NGROUPS_MAX. Returns an int because the expected value is 64k + * and later on this is used as an int with various glibc consumers. */ + + errno = 0; + long ngroups_max = sysconf(_SC_NGROUPS_MAX); + if (ngroups_max <= 0) + return errno_or_else(EOPNOTSUPP); + if (ngroups_max > INT_MAX) + return -ERANGE; + return ngroups_max; +} + static size_t getgr_buffer_size(void) { long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX); return bufsize <= 0 ? 4096U : (size_t) bufsize; diff --git a/src/basic/user-util.h b/src/basic/user-util.h index f45e5bddfd..12fe6f5f8a 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -173,6 +173,8 @@ static inline bool hashed_password_is_locked_or_invalid(const char *password) { */ #define PASSWORD_UNPROVISIONED "!unprovisioned" +int sysconf_ngroups_max(void); + int lookup_pwent_in_files( char * const *files, const char *name, diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 74602efc52..0037890307 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -917,14 +917,9 @@ static int get_supplementary_groups( return 0; } - /* - * If SupplementaryGroups= was passed then NGROUPS_MAX has to - * be positive, otherwise fail. - */ - errno = 0; - long ngroups_max = sysconf(_SC_NGROUPS_MAX); - if (ngroups_max <= 0) - return errno_or_else(EOPNOTSUPP); + int ngroups_max = sysconf_ngroups_max(); + if (ngroups_max < 0) + return ngroups_max; _cleanup_free_ gid_t *l_gids = new(gid_t, ngroups_max); if (!l_gids) diff --git a/src/test/test-condition.c b/src/test/test-condition.c index 8674dab488..a35d08ef63 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -1038,7 +1038,7 @@ TEST(condition_test_group) { char gid[DECIMAL_STR_MAX(uint32_t)]; gid_t *gids, max_gid; int ngroups, r, i; - long ngroups_max; + int ngroups_max; xsprintf(gid, "%u", UINT32_C(0xFFFF)); ASSERT_NOT_NULL((condition = condition_new(CONDITION_GROUP, gid, false, false))); @@ -1054,7 +1054,7 @@ TEST(condition_test_group) { ASSERT_OK_POSITIVE(r); condition_free(condition); - ngroups_max = ASSERT_OK_ERRNO(sysconf(_SC_NGROUPS_MAX)); + ngroups_max = ASSERT_OK(sysconf_ngroups_max()); ASSERT_GT(ngroups_max, 0); gids = newa(gid_t, ngroups_max); diff --git a/src/test/test-socket-util.c b/src/test/test-socket-util.c index 038140d4c5..4b14bd4696 100644 --- a/src/test/test-socket-util.c +++ b/src/test/test-socket-util.c @@ -175,12 +175,10 @@ TEST(getpeercred_getpeergroups) { assert_se(fully_set_uid_gid(test_uid, test_gid, test_gids, n_test_gids) >= 0); } else { - long ngroups_max; - test_uid = getuid(); test_gid = getgid(); - ngroups_max = sysconf(_SC_NGROUPS_MAX); + int ngroups_max = sysconf_ngroups_max(); assert_se(ngroups_max > 0); test_gids = newa(gid_t, ngroups_max); From b3749de21c4f6b4723d9a02414b843a225b588e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 21 May 2026 10:11:47 +0200 Subject: [PATCH 04/17] user-util: add lookup_groups_in_files --- src/basic/user-util.c | 61 +++++++++++++++++++++++++++++++++++++++ src/basic/user-util.h | 5 ++++ src/test/test-user-util.c | 34 ++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 869fabdebd..6252ecc4d9 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -1366,6 +1366,67 @@ int sysconf_ngroups_max(void) { return ngroups_max; } +ssize_t lookup_groups_in_files( + char * const *files, + const char *name, + gid_t gid, + gid_t **ret) { + + _cleanup_free_ gid_t *arr = NULL; + ssize_t n_arr = 0; + int r; + + assert(files); + assert(name); + assert(gid_is_valid(gid)); + + int ngroups_max = sysconf_ngroups_max(); + if (ngroups_max < 0) + return ngroups_max; + + if (!GREEDY_REALLOC(arr, n_arr + 1)) + return -ENOMEM; + arr[n_arr++] = gid; + + STRV_FOREACH(fname, files) { + _cleanup_fclose_ FILE *f = NULL; + struct group *gr; + + r = fopen_unlocked(*fname, "re", &f); + if (r == -ENOENT) + continue; + if (r < 0) + return r; + + while ((r = fgetgrent_sane(f, &gr)) > 0) { + if (gid == gr->gr_gid) + continue; + + /* Deduplicate groups, in case the group is doubly defined in the same or + * different files. */ + if (gid_list_has(arr, n_arr, gr->gr_gid)) + continue; + + if (!strv_contains(gr->gr_mem, name)) + continue; + + /* Do not allow more than _SC_NGROUPS_MAX. */ + if (n_arr >= ngroups_max) + return -ENOBUFS; + + if (!GREEDY_REALLOC(arr, n_arr + 1)) + return -ENOMEM; + arr[n_arr++] = gr->gr_gid; + } + if (r < 0) + return r; + } + + if (ret) + *ret = TAKE_PTR(arr); + return n_arr; +} + static size_t getgr_buffer_size(void) { long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX); return bufsize <= 0 ? 4096U : (size_t) bufsize; diff --git a/src/basic/user-util.h b/src/basic/user-util.h index 12fe6f5f8a..e95ad038e5 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -185,6 +185,11 @@ int lookup_grent_in_files( const char *name, gid_t gid, struct group **ret); +ssize_t lookup_groups_in_files( + char * const *files, + const char *name, + gid_t gid, + gid_t **ret); int getpwuid_malloc(uid_t uid, struct passwd **ret); int getpwnam_malloc(const char *name, struct passwd **ret); diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c index e0c8b68fc1..22eb8df085 100644 --- a/src/test/test-user-util.c +++ b/src/test/test-user-util.c @@ -596,4 +596,38 @@ TEST(lookup_grent_in_files) { ASSERT_STREQ(gr->gr_name, "testgroup1"); } +TEST(lookup_groups_in_files) { + _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-user-util-group-XXXXXX"; + _cleanup_fclose_ FILE *f = NULL; + + ASSERT_OK(fmkostemp_safe(fn, "w", &f)); + ASSERT_OK(write_string_stream( + f, + "testgroup1:x:100:testuser1,testuser2\n" + "testgroup2:x:200:testuser1\n" + "testgroup3:x:300:\n" + "testgroup4:x:400:root,somebody,testuser1\n", + /* flags= */ 0)); + ASSERT_OK(fflush_and_check(f)); + + char **files = STRV_MAKE(fn); + + /* Lookup by name */ + _cleanup_free_ gid_t *arr = NULL; + + ASSERT_EQ(lookup_groups_in_files(files, "testuser1", 100, &arr), 3); + gid_t exp1[] = { 100, 200, 400 }; + ASSERT_EQ(memcmp(arr, exp1, sizeof(exp1)), 0); + arr = mfree(arr); + + ASSERT_EQ(lookup_groups_in_files(files, "testuser1", 0, &arr), 4); + gid_t exp2[] = { 0, 100, 200, 400 }; + ASSERT_EQ(memcmp(arr, exp2, sizeof(exp2)), 0); + arr = mfree(arr); + + ASSERT_EQ(lookup_groups_in_files(files, "testuser3", 300, &arr), 1); + gid_t exp3[] = { 300 }; + ASSERT_EQ(memcmp(arr, exp3, sizeof(exp3)), 0); +} + DEFINE_TEST_MAIN(LOG_INFO); From bbeef4b948a3d7f178950777ae7479480fd2633a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 21 May 2026 10:08:09 +0200 Subject: [PATCH 05/17] various: add -Dbuild-static and alternatives to libc functions When we try to build with -static, the linker refuses dlopen and various functions related to nss. This commit adds the basic support to compile with -static and allow the link to pass. --- meson.build | 2 ++ meson_options.txt | 2 ++ src/basic/dlfcn-util.c | 5 +++ src/basic/user-util.c | 56 ++++++++++++++++++++++++++-------- src/basic/user-util.h | 4 +++ src/libsystemd/sd-bus/sd-bus.c | 5 +++ src/tmpfiles/offline-passwd.c | 8 ++--- 7 files changed, 65 insertions(+), 17 deletions(-) diff --git a/meson.build b/meson.build index 0fcd12f0bd..abdde0040b 100644 --- a/meson.build +++ b/meson.build @@ -80,6 +80,8 @@ conf.set10('SD_BOOT', false) conf.set10('SYSTEMD_MULTICALL_BINARY', get_option('systemd-multicall-binary')) +conf.set10('BUILD_STATIC', get_option('build-static')) + # Create a title-less summary section early, so it ends up first in the output. # More items are added later after they have been detected. summary({ diff --git a/meson_options.txt b/meson_options.txt index ced6fac1e3..8df682742d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -42,6 +42,8 @@ option('static-libudev', type : 'combo', description : 'install a static library for libudev') option('standalone-binaries', type : 'boolean', value : false, description : 'also build standalone versions of supported binaries') +option('build-static', type : 'boolean', value : false, + description : 'build static versions of supported binaries') option('sysvinit-path', type : 'string', value : '/etc/init.d', deprecated : true, description : 'This option is deprecated and will be removed in a future release') diff --git a/src/basic/dlfcn-util.c b/src/basic/dlfcn-util.c index 86ec2d28fd..fe2f6a5f88 100644 --- a/src/basic/dlfcn-util.c +++ b/src/basic/dlfcn-util.c @@ -101,6 +101,10 @@ void block_dlopen(void) { } int dlopen_safe(const char *filename, void **ret, const char **reterr_dlerror) { +#if BUILD_STATIC + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "dlopen is not supported in static builds, cannot load %s.", filename); +#else _cleanup_(dlclosep) void *dl = NULL; int r; @@ -142,4 +146,5 @@ int dlopen_safe(const char *filename, void **ret, const char **reterr_dlerror) { *ret = TAKE_PTR(dl); return 0; +#endif } diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 6252ecc4d9..5c7f783f5c 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -1171,6 +1171,9 @@ int lookup_pwent_in_files( return -ESRCH; } + +#if !BUILD_STATIC + static size_t getpw_buffer_size(void) { long bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); return bufsize <= 0 ? 4096U : (size_t) bufsize; @@ -1182,15 +1185,25 @@ static bool errno_is_user_doesnt_exist(int error) { return IN_SET(abs(error), ENOENT, ESRCH, EBADF, EPERM); } +#endif + int getpwnam_malloc(const char *name, struct passwd **ret) { + /* For static builds use a very simplistic implementation that looks at 'passwd' in /etc/ + * and /usr/lib/, and for non-static builds use NSS. The first matching entry wins. The + * same logic is implemented in getpwnam_malloc, getpwuid_malloc, getgrnam_malloc, and + * getgrgid_malloc. */ + + if (isempty(name)) + return -EINVAL; + +#if BUILD_STATIC + return lookup_pwent_in_files(PASSWD_FILES, name, UID_INVALID, ret); +#else size_t bufsize = getpw_buffer_size(); int r; /* A wrapper around getpwnam_r() that allocates the necessary buffer on the heap. The caller must - * free() the returned structures! */ - - if (isempty(name)) - return -EINVAL; + * free() the returned structure! */ for (;;) { _cleanup_free_ void *buf = NULL; @@ -1226,15 +1239,19 @@ int getpwnam_malloc(const char *name, struct passwd **ret) { return -ENOMEM; bufsize *= 2; } +#endif } int getpwuid_malloc(uid_t uid, struct passwd **ret) { - size_t bufsize = getpw_buffer_size(); - int r; - if (!uid_is_valid(uid)) return -EINVAL; +#if BUILD_STATIC + return lookup_pwent_in_files(PASSWD_FILES, /* name= */ NULL, uid, ret); +#else + size_t bufsize = getpw_buffer_size(); + int r; + for (;;) { _cleanup_free_ void *buf = NULL; @@ -1267,6 +1284,7 @@ int getpwuid_malloc(uid_t uid, struct passwd **ret) { return -ENOMEM; bufsize *= 2; } +#endif } static int copy_struct_group(const struct group *gr, struct group **ret) { @@ -1427,18 +1445,25 @@ ssize_t lookup_groups_in_files( return n_arr; } +#if !BUILD_STATIC + static size_t getgr_buffer_size(void) { long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX); return bufsize <= 0 ? 4096U : (size_t) bufsize; } -int getgrnam_malloc(const char *name, struct group **ret) { - size_t bufsize = getgr_buffer_size(); - int r; +#endif +int getgrnam_malloc(const char *name, struct group **ret) { if (isempty(name)) return -EINVAL; +#if BUILD_STATIC + return lookup_grent_in_files(GROUP_FILES, name, GID_INVALID, ret); +#else + size_t bufsize = getgr_buffer_size(); + int r; + for (;;) { _cleanup_free_ void *buf = NULL; @@ -1471,15 +1496,19 @@ int getgrnam_malloc(const char *name, struct group **ret) { return -ENOMEM; bufsize *= 2; } +#endif } int getgrgid_malloc(gid_t gid, struct group **ret) { - size_t bufsize = getgr_buffer_size(); - int r; - if (!gid_is_valid(gid)) return -EINVAL; +#if BUILD_STATIC + return lookup_grent_in_files(GROUP_FILES, /* name= */ NULL, gid, ret); +#else + size_t bufsize = getgr_buffer_size(); + int r; + for (;;) { _cleanup_free_ void *buf = NULL; @@ -1512,4 +1541,5 @@ int getgrgid_malloc(gid_t gid, struct group **ret) { return -ENOMEM; bufsize *= 2; } +#endif } diff --git a/src/basic/user-util.h b/src/basic/user-util.h index e95ad038e5..6f90c8c453 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -159,6 +159,10 @@ static inline bool hashed_password_is_locked_or_invalid(const char *password) { return password && password[0] != '$'; } +/* Places where we will try to load account data from */ +#define PASSWD_FILES STRV_MAKE("/etc/passwd", "/usr/lib/passwd") +#define GROUP_FILES STRV_MAKE("/etc/group", "/usr/lib/group") + /* A locked *and* invalid password for "struct spwd"'s .sp_pwdp and "struct passwd"'s .pw_passwd field */ #define PASSWORD_LOCKED_AND_INVALID "!*" diff --git a/src/libsystemd/sd-bus/sd-bus.c b/src/libsystemd/sd-bus/sd-bus.c index e44c439fad..2bd0439c59 100644 --- a/src/libsystemd/sd-bus/sd-bus.c +++ b/src/libsystemd/sd-bus/sd-bus.c @@ -827,6 +827,10 @@ static int parse_unix_address(sd_bus *b, const char **p, char **guid) { } static int parse_tcp_address(sd_bus *b, const char **p, char **guid) { +#if BUILD_STATIC + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "tcp:// connections are not supported in static builds"); +#else _cleanup_free_ char *host = NULL, *port = NULL, *family = NULL; int r; struct addrinfo *result, hints = { @@ -889,6 +893,7 @@ static int parse_tcp_address(sd_bus *b, const char **p, char **guid) { b->is_local = false; return 0; +#endif } static int parse_exec_address(sd_bus *b, const char **p, char **guid) { diff --git a/src/tmpfiles/offline-passwd.c b/src/tmpfiles/offline-passwd.c index 75e9085c26..c0490546b8 100644 --- a/src/tmpfiles/offline-passwd.c +++ b/src/tmpfiles/offline-passwd.c @@ -53,10 +53,10 @@ static int populate_uid_cache(const char *root, Hashmap **ret) { /* The directory list is hardcoded here: /etc is the standard, and rpm-ostree uses /usr/lib. This * could be made configurable, but I don't see the point right now. */ - FOREACH_STRING(fname, "/etc/passwd", "/usr/lib/passwd") { + STRV_FOREACH(fname, PASSWD_FILES) { _cleanup_fclose_ FILE *f = NULL; - r = open_passwd_file(root, fname, &f); + r = open_passwd_file(root, *fname, &f); if (r == -ENOENT) continue; if (r < 0) @@ -93,10 +93,10 @@ static int populate_gid_cache(const char *root, Hashmap **ret) { if (!cache) return -ENOMEM; - FOREACH_STRING(fname, "/etc/group", "/usr/lib/group") { + STRV_FOREACH(fname, GROUP_FILES) { _cleanup_fclose_ FILE *f = NULL; - r = open_passwd_file(root, fname, &f); + r = open_passwd_file(root, *fname, &f); if (r == -ENOENT) continue; if (r < 0) From 5c2bd19f40577873b68f645a2cd22431aa677786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 21 May 2026 09:55:56 +0200 Subject: [PATCH 06/17] user-util: add getgrouplist_malloc and initgroups_wrapper --- src/basic/user-util.c | 44 +++++++++++++++++++++++++++++++++++++++++++ src/basic/user-util.h | 3 +++ 2 files changed, 47 insertions(+) diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 5c7f783f5c..fe4c0e6a36 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -1445,6 +1445,50 @@ ssize_t lookup_groups_in_files( return n_arr; } +int getgrouplist_malloc(const char *user, gid_t gid, gid_t **ret) { +#if BUILD_STATIC + return lookup_groups_in_files(GROUP_FILES, user, gid, ret); +#else + int ngroups_max = sysconf_ngroups_max(); + if (ngroups_max < 0) + return ngroups_max; + + _cleanup_free_ gid_t *gids = new(gid_t, ngroups_max); + if (!gids) + return -ENOMEM; + + int k = ngroups_max; + if (getgrouplist(user, gid, gids, &k) < 0) + return -EIO; /* getgrouplist does not set errno */ + + if (ret) + *ret = TAKE_PTR(gids); + return k; +#endif +} + +int initgroups_wrapper(const char *user, gid_t gid) { +#if BUILD_STATIC + _cleanup_free_ gid_t *groups = NULL; + int r, n_groups; + + n_groups = getgrouplist_malloc(user, gid, &groups); + if (n_groups <= 0) + return n_groups; + + /* Try to set the maximum number of groups the kernel can handle. This is what glibc does. */ + for (; n_groups > 0; n_groups--) { + r = RET_NERRNO(setgroups(n_groups, groups)); + if (r != -EINVAL) + break; + } + + return r; +#else + return RET_NERRNO(initgroups(user, gid)); +#endif +} + #if !BUILD_STATIC static size_t getgr_buffer_size(void) { diff --git a/src/basic/user-util.h b/src/basic/user-util.h index 6f90c8c453..ab9222c500 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -189,11 +189,14 @@ int lookup_grent_in_files( const char *name, gid_t gid, struct group **ret); + ssize_t lookup_groups_in_files( char * const *files, const char *name, gid_t gid, gid_t **ret); +int getgrouplist_malloc(const char *user, gid_t gid, gid_t **ret); +int initgroups_wrapper(const char *user, gid_t gid); int getpwuid_malloc(uid_t uid, struct passwd **ret); int getpwnam_malloc(const char *name, struct passwd **ret); From 50b4f1e3bf55059f484bea537e93ad2b4bce151a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 22 Jun 2026 20:19:25 +0200 Subject: [PATCH 07/17] shared/creds-util: use getpwuid_malloc() Static linking, i.e. without dlopen, sometimes fails when getpwuid is used. I doesn't fail in all cases (glibc version?), so I didn't do this initially. --- src/shared/creds-util.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 739973aa46..a15fcfc6b2 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -768,6 +768,7 @@ static int mangle_uid_into_key( uid_t uid, uint8_t md[static SHA256_DIGEST_LENGTH]) { + _cleanup_free_ struct passwd *pw = NULL; sd_id128_t mid; int r; @@ -778,12 +779,9 @@ static int mangle_uid_into_key( * (specifically, UID, user name, machine ID) with the key we'd otherwise use for system credentials, * and use the resulting hash as actual encryption key. */ - errno = 0; - struct passwd *pw = getpwuid(uid); - if (!pw) - return log_error_errno( - IN_SET(errno, 0, ENOENT) ? SYNTHETIC_ERRNO(ESRCH) : errno, - "Failed to resolve UID " UID_FMT ": %m", uid); + r = getpwuid_malloc(uid, &pw); + if (r < 0) + return log_error_errno(r, "Failed to resolve UID "UID_FMT": %m", uid); r = sd_id128_get_machine(&mid); if (r < 0) From b0dfec9fa498a056b6f0cd0639f76931e2ff654a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 1 Jul 2026 19:49:16 +0200 Subject: [PATCH 08/17] core: convert exec-invoke to use the new static-friendly functions The libc API is rather ugly: it requires the caller to pass an array of the maximum possible size (or to repeatedly query the group list to discover the actual size). With our "static-friendly" helpers, we reallocate the array on the fly. --- src/core/exec-invoke.c | 49 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 0037890307..7a7f7f2a70 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -890,7 +890,7 @@ static int get_supplementary_groups( assert(ret_gids); /* - * If user is given, then lookup GID and supplementary groups list. + * If user is given, then look up GID and supplementary groups list. * We avoid NSS lookups for gid=0. Also we have to initialize groups * here and as early as possible so we keep the list of supplementary * groups of the caller. @@ -898,15 +898,16 @@ static int get_supplementary_groups( bool keep_groups = false; if (user && gid_is_valid(gid) && gid != 0) { /* First step, initialize groups from /etc/groups */ - if (initgroups(user, gid) < 0) { + r = initgroups_wrapper(user, gid); + if (r < 0) { /* If our primary gid is already the one specified in Group= (i.e. we're running in * user mode), gracefully handle the case where we have no privilege to re-initgroups(). * * Note that group memberships of the current user might have been modified, but * the change will only take effect after re-login. It's better to continue on with * existing credentials rather than erroring out. */ - if (!ERRNO_IS_PRIVILEGE(errno) || gid != getgid()) - return -errno; + if (!ERRNO_IS_PRIVILEGE(r) || gid != getgid()) + return r; } keep_groups = true; @@ -917,29 +918,30 @@ static int get_supplementary_groups( return 0; } + /* If SupplementaryGroups= was passed then NGROUPS_MAX has to be positive, otherwise fail. */ + _cleanup_free_ gid_t *l_gids = NULL; + + int k = 0; + if (keep_groups) { + /* Look up the list of groups that the user belongs to. + * We avoid NSS lookups here too for gid=0. */ + + k = getgrouplist_malloc(user, gid, &l_gids); + if (k < 0) + return k; + } + int ngroups_max = sysconf_ngroups_max(); if (ngroups_max < 0) return ngroups_max; - _cleanup_free_ gid_t *l_gids = new(gid_t, ngroups_max); - if (!l_gids) - return -ENOMEM; - - int k = 0; - if (keep_groups) { - /* - * Lookup the list of groups that the user belongs to, we - * avoid NSS lookups here too for gid=0. - */ - k = ngroups_max; - if (getgrouplist(user, gid, l_gids, &k) < 0) - return -EINVAL; - } - STRV_FOREACH(i, c->supplementary_groups) { if (k >= ngroups_max) return -E2BIG; + if (!GREEDY_REALLOC(l_gids, k + 1)) + return -ENOMEM; + r = get_group_creds(*i, /* flags= */ 0, /* ret_name= */ NULL, l_gids + k); if (r < 0) return r; @@ -952,12 +954,11 @@ static int get_supplementary_groups( return 0; } - /* Otherwise get the final list of supplementary groups */ - gid_t *groups = newdup(gid_t, l_gids, k); - if (!groups) - return -ENOMEM; + /* We *could* trim the array size with realloc(3), but right now the only caller frees the array + * quickly anyway, so this is not worth the trouble. If other users pop up, this should be + * reconsidered. */ - *ret_gids = groups; + *ret_gids = TAKE_PTR(l_gids); return k; } From fff9c9ba4693d6a8fc69e27f321712e357dc87fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 30 Jun 2026 14:06:28 +0200 Subject: [PATCH 09/17] meson: allow manager to be linked statically After stripping, in an unoptimized build with glibc, we get a 8.7 MB binary: $ file build-static/systemd build-static/systemd: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=1ec004bf61b775d284d909a818a3c4f63f83f055, for GNU/Linux 3.2.0, stripped $ ls -lh build-static/systemd -rwxr-xr-x 1 zbyszek zbyszek 8.7M Jun 30 14:07 build-static/systemd --- src/core/meson.build | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/meson.build b/src/core/meson.build index 3708caf4e0..2d3ed4cff2 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -209,6 +209,9 @@ systemd_deps = [ libselinux_cflags, ] +systemd_link_args = get_option('build-static') ? ['-static'] : [] +systemd_libs = get_option('build-static') ? core_libs_static : core_libs_shared + executor_libs = get_option('link-executor-shared') ? core_libs_shared : core_libs_static if conf.get('SYSTEMD_MULTICALL_BINARY') == 1 @@ -226,7 +229,8 @@ executables += [ 'dbus' : true, 'public' : true, 'sources' : systemd_sources, - 'link_with' : core_libs_shared, + 'link_args' : systemd_link_args, + 'link_with' : systemd_libs, 'dependencies' : systemd_deps, }, From a809b6e565024103958782789dfb5adca5b7f573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 23 Jun 2026 10:43:50 +0200 Subject: [PATCH 10/17] core: add fallback-default-target build option Add a 'fallback-default-target' meson option that configures which unit to activate when default.target is not installed. This makes things more resilient in general and is also useful for minimal/statically-linked installations that may not ship a default.target symlink. 'graphical.target' is used as the default value of the setting, because that's what we symlink as default.target in units/meson.build. In the initrd, we had a fallback to start default.target if initrd.target cannot be started. This fallback is changed to only do that if it is not found, not on other errors. This seems more correct (and makes the two fallbacks symmetrical.) --- man/custom-entities.ent.in | 1 + man/systemd.special.xml | 12 +++++++++++- meson.build | 4 ++++ meson_options.txt | 2 ++ src/core/main.c | 19 +++++++++++++------ src/test/test-unit-name.c | 3 +++ 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/man/custom-entities.ent.in b/man/custom-entities.ent.in index 4494a1c3ab..beff1529a7 100644 --- a/man/custom-entities.ent.in +++ b/man/custom-entities.ent.in @@ -15,6 +15,7 @@ + diff --git a/man/systemd.special.xml b/man/systemd.special.xml index f6f35b861d..deba4f8e4f 100644 --- a/man/systemd.special.xml +++ b/man/systemd.special.xml @@ -1,6 +1,9 @@ + "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ + +%entities; +]> @@ -239,6 +242,13 @@ 3, 5, …; see systemd1. + If default.target is not present on disk and no unit was requested on + the kernel command line, the default set at compile time will be used + (&FALLBACK_DEFAULT_TARGET;). + + If the default unit or one of the fallbacks cannot be started, systemd starts + rescue.target. + For typical unit files please set WantedBy= to a regular target (like multi-user.target or graphical.target), instead of default.target, since such a service will also be run on special diff --git a/meson.build b/meson.build index abdde0040b..423f921834 100644 --- a/meson.build +++ b/meson.build @@ -726,6 +726,9 @@ if fallback_hostname == '' or fallback_hostname[0] == '.' or fallback_hostname[0 endif conf.set_quoted('FALLBACK_HOSTNAME', fallback_hostname) +fallback_default_target = get_option('fallback-default-target') +conf.set_quoted('FALLBACK_DEFAULT_TARGET', fallback_default_target) + extra_net_naming_schemes = [] extra_net_naming_map = [] foreach scheme: get_option('extra-net-naming-schemes').split(',') @@ -2751,6 +2754,7 @@ summary({ 'nobody user name' : nobody_user, 'nobody group name' : nobody_group, 'fallback hostname' : get_option('fallback-hostname'), + 'fallback default target' : fallback_default_target, 'default compression method' : compression, 'default DNSSEC mode' : default_dnssec, 'default DNS-over-TLS mode' : default_dns_over_tls, diff --git a/meson_options.txt b/meson_options.txt index 8df682742d..9c0cda4c38 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -267,6 +267,8 @@ option('clock-valid-range-usec-max', type : 'integer', value : 473364000000000, description : 'maximum value in microseconds for the difference between RTC and epoch, exceeding which is considered an RTC error ["0" disables]') option('default-user-shell', type : 'string', value : '/bin/bash', description : 'default interactive shell') +option('fallback-default-target', type : 'string', value : 'graphical.target', + description : 'unit to activate when default.target is not found on disk') option('system-alloc-uid-min', type : 'integer', value : 0, description : 'minimum system UID used when allocating') diff --git a/src/core/main.c b/src/core/main.c index c476400a93..3dd227f244 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -2768,15 +2768,22 @@ static int do_queue_default_job( log_debug("Activating default unit: %s", unit); r = manager_load_startable_unit_or_warn(m, unit, NULL, &target); - if (r < 0 && in_initrd() && !arg_default_unit) { - /* Fall back to default.target, which we used to always use by default. Only do this if no - * explicit configuration was given. */ + if (r == -ENOENT && !arg_default_unit) { + if (in_initrd()) + /* Fall back to default.target, which we used to always use by default. + * Only do this if no explicit configuration was given. */ + unit = SPECIAL_DEFAULT_TARGET; + else + /* The default.target symlink was not found on disk and the target was not + * explicitly specified. Fall back to the target configured at build time + * via -Ddefault-target=. */ + unit = FALLBACK_DEFAULT_TARGET; - log_info("Falling back to %s.", SPECIAL_DEFAULT_TARGET); - - r = manager_load_startable_unit_or_warn(m, SPECIAL_DEFAULT_TARGET, NULL, &target); + log_info("Falling back to %s.", unit); + r = manager_load_startable_unit_or_warn(m, unit, NULL, &target); } if (r < 0) { + /* We failed. Activate rescue mode. */ log_info("Falling back to %s.", SPECIAL_RESCUE_TARGET); r = manager_load_startable_unit_or_warn(m, SPECIAL_RESCUE_TARGET, NULL, &target); diff --git a/src/test/test-unit-name.c b/src/test/test-unit-name.c index 7f71f5fdc7..c218e015fa 100644 --- a/src/test/test-unit-name.c +++ b/src/test/test-unit-name.c @@ -88,6 +88,9 @@ TEST(unit_name_is_valid) { test_unit_name_is_valid_one("foo.target.wants/plain.service", UNIT_NAME_ANY, false); test_unit_name_is_valid_one("foo.target.conf/foo.conf", UNIT_NAME_ANY, false); test_unit_name_is_valid_one("foo.target.requires/plain.socket", UNIT_NAME_ANY, false); + + /* The build-time configured fallback for default.target must be a valid plain unit name. */ + test_unit_name_is_valid_one(FALLBACK_DEFAULT_TARGET, UNIT_NAME_PLAIN, true); } static void test_unit_name_replace_instance_one(const char *pattern, const char *repl, const char *expected, int ret) { From a0146fe807117c801e9f4a978b30d933dfed187f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Mon, 22 Jun 2026 19:04:04 +0200 Subject: [PATCH 11/17] core: embed essential units as built-in fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compile the contents of a few essential target unit files (graphical.target, multi-user.target, rescue.target, etc.) into the manager binary, with comments stripped, and fall back to those built-in copies when no fragment is found on disk. This lets the manager reach a usable state even on a system that ships none of these unit files, e.g. in minimal environments or one-off chroots. On-disk files always take precedence (masks included), as the fallback is only consulted when nothing is found in the lookup paths. The unit file list is defined in units/meson.build and embedded via a small generator script; 'units' is now processed before 'src/core' so the list is available there. The set of units and services is selected that should allow for a normal operation of starting/stopping/restarting, of the machine. The services all use SuccessAction=…, so they don't require any binaries to be installed. sigpwr.target is included, even though it doesn't do anything useful. The same is true in normal installations. We might want to change it do something useful there too. --- TODO.md | 6 ++++ meson.build | 4 ++- src/core/generate-builtin-units.py | 33 +++++++++++++++++ src/core/load-fragment.c | 57 ++++++++++++++++++++++++++++-- src/core/meson.build | 11 ++++-- units/meson.build | 38 ++++++++++++++++++-- 6 files changed, 140 insertions(+), 9 deletions(-) create mode 100755 src/core/generate-builtin-units.py diff --git a/TODO.md b/TODO.md index 8ef085f693..5537ba86aa 100644 --- a/TODO.md +++ b/TODO.md @@ -2436,6 +2436,12 @@ SPDX-License-Identifier: LGPL-2.1-or-later operate in device mode - add NVMe authentication +- **sigpwr.target** doesn't do anything useful. Consider hooking it up to + poweroff.target. + +- Provide a fallback in **rescue.service** that prints a fixed message + if sulogin-shell could not be started. + - support boot into nvme-over-tcp: add generator that allows specifying nvme devices on kernel cmdline + credentials. Also maybe add interactive mode (where the user is prompted for nvme info), in order to boot from other diff --git a/meson.build b/meson.build index 423f921834..a5572301db 100644 --- a/meson.build +++ b/meson.build @@ -1975,6 +1975,9 @@ module_additional_kwargs = { ##################################################################### +# 'src/core' embeds a built-in copy of some unit files, so the unit file list +# from 'units' must be defined first. +subdir('units') # systemd-analyze requires 'libcore' subdir('src/core') # systemd-networkd requires 'libsystemd_network' @@ -2466,7 +2469,6 @@ subdir('shell-completion/zsh') subdir('sysctl.d') subdir('sysusers.d') subdir('tmpfiles.d') -subdir('units') install_subdir('factory/etc', install_dir : factorydir) diff --git a/src/core/generate-builtin-units.py b/src/core/generate-builtin-units.py new file mode 100755 index 0000000000..6850a9e59b --- /dev/null +++ b/src/core/generate-builtin-units.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Embed the contents of unit files passed on the command line into a C array +# initializer, so that the manager can fall back to a built-in copy when no +# unit file is found on disk. Comment and empty lines are stripped to keep the +# embedded strings small. The unit name is derived from each file's basename. + +import pathlib +import string +import sys + +PRINTABLE = set(string.printable) - set('\t\r\x0b\x0c') + + +def strip_comments(text: str) -> str: + # Unit files use '#' and ';' as comment markers at the start of a line. + # fmt: off + lines = (line for line in text.splitlines() + if line and line[0] not in '#;') + return '\n'.join(lines) + '\n' + + +def c_escape(text: str) -> str: + assert set(text) <= PRINTABLE + return text.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + + +for path in sys.argv[1:]: + path = pathlib.Path(path) + data = strip_comments(path.read_text()) + print(f'{{ "{path.name}", "{c_escape(data)}" }},') diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index ce4517e85b..c9e270a668 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -30,6 +30,7 @@ #include "execute.h" #include "extract-word.h" #include "fd-util.h" +#include "fileio.h" #include "fstab-util.h" #include "hashmap.h" #include "hexdecoct.h" @@ -73,6 +74,16 @@ #include "user-util.h" #include "web-util.h" +/* Built-in copies of a few essential unit files, embedded at build time. They are used as a fallback + * when no fragment for the unit is found on disk, so that the manager can reach a usable state even on + * a system that ships none of these unit files. */ +static const struct { + const char *name; + const char *data; +} builtin_units[] = { +# include "builtin-units.inc" +}; + static int parse_socket_protocol(const char *s) { int r; @@ -6171,6 +6182,16 @@ static int merge_by_names(Unit *u, Set *names, const char *id) { return 0; } +static const char* builtin_unit_lookup(const char *name) { + assert(name); + + FOREACH_ELEMENT(i, builtin_units) + if (streq(i->name, name)) + return i->data; + + return NULL; +} + int unit_load_fragment(Unit *u) { int r; @@ -6252,14 +6273,44 @@ int unit_load_fragment(Unit *u) { r = config_parse(u->id, fragment, f, UNIT_VTABLE(u)->sections, config_item_perf_lookup, load_fragment_gperf_lookup, - 0, - u, - NULL); + /* flags= */ 0, + /* userdata= */ u, + /* ret_stat= */ NULL); if (r == -ENOEXEC) log_unit_notice_errno(u, r, "Unit configuration has fatal error, unit will not be started."); if (r < 0) return r; } + } else if (u->manager->runtime_scope == RUNTIME_SCOPE_SYSTEM) { + /* No fragment found on disk. For system units, fall back to a built-in copy if we have one + * embedded. This way the manager can reach a usable state even if none of these unit files + * are installed. On-disk files always take precedence (including masks), since we only get + * here when nothing was found in the lookup paths. */ + + const char *data = builtin_unit_lookup(u->id); + if (data) { + _cleanup_fclose_ FILE *f = NULL; + + f = fmemopen_unlocked((void*) data, strlen(data), "re"); + if (!f) + return log_oom(); + + log_unit_debug(u, "Loading built-in fragment for %s.", u->id); + + u->load_state = UNIT_LOADED; + u->fragment_mtime = 0; + + r = config_parse(u->id, u->id, f, + UNIT_VTABLE(u)->sections, + config_item_perf_lookup, load_fragment_gperf_lookup, + /* flags= */ 0, + /* userdata= */ u, + /* ret_stat= */ NULL); + if (r == -ENOEXEC) + log_unit_notice_errno(u, r, "Built-in unit configuration has fatal error, unit will not be started."); + if (r < 0) + return r; + } } /* Call merge_by_names with the name derived from the fragment path as the preferred name. diff --git a/src/core/meson.build b/src/core/meson.build index 2d3ed4cff2..26c84fa78b 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -135,6 +135,13 @@ load_fragment_gperf_nulstr_c = custom_target( command : [awk, '-f', '@INPUT0@', '@INPUT1@'], capture : true) +generate_builtin_units = files('generate-builtin-units.py') +builtin_units_inc = custom_target( + input : [generate_builtin_units, embedded_units], + output : 'builtin-units.inc', + command : [python, generate_builtin_units, embedded_units], + capture : true) + generate_bpf_delegate_configs = files('generate-bpf-delegate-configs.py') bpf_delegate_configs_inc = custom_target( input : [generate_bpf_delegate_configs, bpf_delegate_sources], @@ -155,8 +162,8 @@ bpf_delegate_xml = custom_target( capture : true) man_page_depends += bpf_delegate_xml -generated_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc] -libcore_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc] +generated_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc, builtin_units_inc] +libcore_sources += [load_fragment_gperf_c, load_fragment_gperf_nulstr_c, bpf_delegate_configs_inc, builtin_units_inc] libcore_build_dir = meson.current_build_dir() libcore_name = 'systemd-core-@0@'.format(shared_lib_tag) core_includes = [include_directories('.'), includes] diff --git a/units/meson.build b/units/meson.build index 60ce3bf0ca..d61138bf7b 100644 --- a/units/meson.build +++ b/units/meson.build @@ -1070,6 +1070,33 @@ units = [ udev_units = [] hwdb_units = [] +# Unit files whose contents are embedded into the manager binary, so that it can +# fall back to a built-in copy when the file is not present on disk. Referenced +# from src/core/meson.build. +embedded_unit_names = [ + 'basic.target', + 'exit.target', + 'final.target', + 'graphical.target', + 'halt.target', + 'kexec.target', + 'multi-user.target', + 'poweroff.target', + 'reboot.target', + 'shutdown.target', + 'sigpwr.target', + 'soft-reboot.target', + 'sysinit.target', + 'systemd-exit.service', + 'systemd-halt.service', + 'systemd-kexec.service', + 'systemd-poweroff.service', + 'systemd-reboot.service', + 'systemd-soft-reboot.service', + 'umount.target', +] +embedded_units = [] + foreach unit : units source = unit.get('file') @@ -1092,7 +1119,7 @@ foreach unit : units endforeach if needs_jinja - t = custom_target( + processed = custom_target( input : source, output : name, command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], @@ -1100,9 +1127,9 @@ foreach unit : units install_dir : systemunitdir, install_tag : unit.get('install_tag', '')) if unit.get('install_tag', '') == 'udev' - udev_units += t + udev_units += processed elif unit.get('install_tag', '') == 'hwdb' - hwdb_units += t + hwdb_units += processed endif elif install install_data(source, @@ -1110,6 +1137,11 @@ foreach unit : units install_tag : unit.get('install_tag', '')) endif + if name in embedded_unit_names + # Embed either the processed output or the original file. + embedded_units += needs_jinja ? processed : source + endif + if install foreach target : unit.get('symlinks', []) if target.endswith('/') From 7bff0f09fe8b87bb1d9aa7fd1667c6a7d483d34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 23 Jun 2026 12:00:40 +0200 Subject: [PATCH 12/17] core: let manager_load_startable_unit_or_warn take a log level Add a log_level parameter so callers can control the severity of load failures. During boot, when no unit was explicitly requested on the kernel command line (arg_default_unit is unset), failing to load the default unit is not fatal since we fall back to other targets, so log those attempts at LOG_INFO instead of LOG_ERR. All other callers pass LOG_ERR to keep their existing behaviour. --- src/analyze/analyze-condition.c | 2 +- src/analyze/analyze-security.c | 2 +- src/analyze/analyze-verify-util.c | 2 +- src/core/main.c | 11 +++++++---- src/core/manager.c | 9 +++++---- src/core/manager.h | 2 +- src/test/test-cgroup-mask.c | 14 +++++++------- src/test/test-engine.c | 18 +++++++++--------- src/test/test-execute.c | 4 ++-- src/test/test-path.c | 14 +++++++------- src/test/test-sched-prio.c | 12 ++++++------ 11 files changed, 47 insertions(+), 43 deletions(-) diff --git a/src/analyze/analyze-condition.c b/src/analyze/analyze-condition.c index 5c5177c3be..8704e0a6e3 100644 --- a/src/analyze/analyze-condition.c +++ b/src/analyze/analyze-condition.c @@ -109,7 +109,7 @@ static int verify_conditions(char **lines, RuntimeScope scope, const char *unit, if (r < 0) return log_error_errno(r, "Failed to prepare filename %s: %m", unit); - r = manager_load_startable_unit_or_warn(m, NULL, prepared, &u); + r = manager_load_startable_unit_or_warn(m, /* name= */ NULL, prepared, LOG_ERR, &u); if (r < 0) return r; } else { diff --git a/src/analyze/analyze-security.c b/src/analyze/analyze-security.c index 6901956b8d..fbef7e3d2e 100644 --- a/src/analyze/analyze-security.c +++ b/src/analyze/analyze-security.c @@ -2792,7 +2792,7 @@ static int offline_security_checks( return log_error_errno(r, "Failed to copy: %m"); } - k = manager_load_startable_unit_or_warn(m, NULL, prepared, &units[count]); + k = manager_load_startable_unit_or_warn(m, /* name= */ NULL, prepared, LOG_ERR, &units[count]); if (k < 0) { RET_GATHER(r, k); continue; diff --git a/src/analyze/analyze-verify-util.c b/src/analyze/analyze-verify-util.c index ad553078d4..e2adf760a6 100644 --- a/src/analyze/analyze-verify-util.c +++ b/src/analyze/analyze-verify-util.c @@ -335,7 +335,7 @@ int verify_units( continue; } - k = manager_load_startable_unit_or_warn(m, NULL, prepared, &units[count]); + k = manager_load_startable_unit_or_warn(m, /* name= */ NULL, prepared, LOG_ERR, &units[count]); if (k < 0) { RET_GATHER(r, k); continue; diff --git a/src/core/main.c b/src/core/main.c index 3dd227f244..b967a57264 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -2765,9 +2765,12 @@ static int do_queue_default_job( else unit = SPECIAL_DEFAULT_TARGET; - log_debug("Activating default unit: %s", unit); + /* When no unit was explicitly requested, failures to load the default unit are not fatal, + * since we fall back to other targets below. Log them at a lower level in that case. */ + int log_level = arg_default_unit ? LOG_ERR : LOG_INFO; - r = manager_load_startable_unit_or_warn(m, unit, NULL, &target); + log_debug("Activating default unit: %s", unit); + r = manager_load_startable_unit_or_warn(m, unit, /* path= */ NULL, log_level, &target); if (r == -ENOENT && !arg_default_unit) { if (in_initrd()) /* Fall back to default.target, which we used to always use by default. @@ -2780,13 +2783,13 @@ static int do_queue_default_job( unit = FALLBACK_DEFAULT_TARGET; log_info("Falling back to %s.", unit); - r = manager_load_startable_unit_or_warn(m, unit, NULL, &target); + r = manager_load_startable_unit_or_warn(m, unit, /* path= */ NULL, log_level, &target); } if (r < 0) { /* We failed. Activate rescue mode. */ log_info("Falling back to %s.", SPECIAL_RESCUE_TARGET); - r = manager_load_startable_unit_or_warn(m, SPECIAL_RESCUE_TARGET, NULL, &target); + r = manager_load_startable_unit_or_warn(m, SPECIAL_RESCUE_TARGET, /* path= */ NULL, log_level, &target); if (r < 0) { *ret_error_message = r == -ERFKILL ? SPECIAL_RESCUE_TARGET " masked" : "Failed to load " SPECIAL_RESCUE_TARGET; diff --git a/src/core/manager.c b/src/core/manager.c index 7fe2fe0a01..baaac37482 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -2788,6 +2788,7 @@ int manager_load_startable_unit_or_warn( Manager *m, const char *name, const char *path, + int log_level, Unit **ret) { /* Load a unit, make sure it loaded fully and is not masked. */ @@ -2800,13 +2801,13 @@ int manager_load_startable_unit_or_warn( r = manager_load_unit(m, name, path, &error, &unit); if (r < 0) - return log_error_errno(r, "Failed to load %s %s: %s", - name ? "unit" : "unit file", name ?: path, - bus_error_message(&error, r)); + return log_full_errno(log_level, r, "Failed to load %s %s: %s", + name ? "unit" : "unit file", name ?: path, + bus_error_message(&error, r)); r = bus_unit_validate_load_state(unit, &error); if (r < 0) - return log_error_errno(r, "%s", bus_error_message(&error, r)); + return log_full_errno(log_level, r, "%s", bus_error_message(&error, r)); *ret = unit; return 0; diff --git a/src/core/manager.h b/src/core/manager.h index 0a0974bf1c..ab5187cde4 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -592,7 +592,7 @@ bool manager_unit_cache_should_retry_load(Unit *u); int manager_load_unit_prepare(Manager *m, const char *name, const char *path, sd_bus_error *e, Unit **ret); int manager_load_unit(Manager *m, const char *name, const char *path, sd_bus_error *e, Unit **ret); int manager_dispatch_external_fd_to_unit(Manager *m, const char *unit_id, const char *fdname, uint64_t index, int fd, const char *log_context); -int manager_load_startable_unit_or_warn(Manager *m, const char *name, const char *path, Unit **ret); +int manager_load_startable_unit_or_warn(Manager *m, const char *name, const char *path, int log_level, Unit **ret); int manager_load_unit_from_dbus_path(Manager *m, const char *s, sd_bus_error *e, Unit **_u); int manager_add_jobs( diff --git a/src/test/test-cgroup-mask.c b/src/test/test-cgroup-mask.c index 9673415080..adc2f4747f 100644 --- a/src/test/test-cgroup-mask.c +++ b/src/test/test-cgroup-mask.c @@ -54,13 +54,13 @@ TEST_RET(cgroup_mask, .sd_booted = true) { assert_se(manager_startup(m, NULL, NULL, NULL, NULL) >= 0); /* Load units and verify hierarchy. */ - ASSERT_OK(manager_load_startable_unit_or_warn(m, "parent.slice", NULL, &parent)); - ASSERT_OK(manager_load_startable_unit_or_warn(m, "son.service", NULL, &son)); - ASSERT_OK(manager_load_startable_unit_or_warn(m, "daughter.service", NULL, &daughter)); - ASSERT_OK(manager_load_startable_unit_or_warn(m, "grandchild.service", NULL, &grandchild)); - ASSERT_OK(manager_load_startable_unit_or_warn(m, "parent-deep.slice", NULL, &parent_deep)); - ASSERT_OK(manager_load_startable_unit_or_warn(m, "nomem.slice", NULL, &nomem_parent)); - ASSERT_OK(manager_load_startable_unit_or_warn(m, "nomemleaf.service", NULL, &nomem_leaf)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "parent.slice", NULL, LOG_ERR, &parent)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "son.service", NULL, LOG_ERR, &son)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "daughter.service", NULL, LOG_ERR, &daughter)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "grandchild.service", NULL, LOG_ERR, &grandchild)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "parent-deep.slice", NULL, LOG_ERR, &parent_deep)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "nomem.slice", NULL, LOG_ERR, &nomem_parent)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "nomemleaf.service", NULL, LOG_ERR, &nomem_leaf)); assert_se(UNIT_GET_SLICE(son) == parent); assert_se(UNIT_GET_SLICE(daughter) == parent); assert_se(UNIT_GET_SLICE(parent_deep) == parent); diff --git a/src/test/test-engine.c b/src/test/test-engine.c index 5a6a1a4204..abe04182be 100644 --- a/src/test/test-engine.c +++ b/src/test/test-engine.c @@ -100,9 +100,9 @@ int main(int argc, char *argv[]) { assert_se(manager_startup(m, NULL, NULL, NULL, NULL) >= 0); printf("Load1:\n"); - assert_se(manager_load_startable_unit_or_warn(m, "a.service", NULL, &a) >= 0); - assert_se(manager_load_startable_unit_or_warn(m, "b.service", NULL, &b) >= 0); - assert_se(manager_load_startable_unit_or_warn(m, "c.service", NULL, &c) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "a.service", NULL, LOG_ERR, &a) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "b.service", NULL, LOG_ERR, &b) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "c.service", NULL, LOG_ERR, &c) >= 0); manager_dump_units(m, stdout, /* patterns= */ NULL, "\t"); printf("Test1: (Trivial)\n"); @@ -114,8 +114,8 @@ int main(int argc, char *argv[]) { printf("Load2:\n"); manager_clear_jobs(m); - assert_se(manager_load_startable_unit_or_warn(m, "d.service", NULL, &d) >= 0); - assert_se(manager_load_startable_unit_or_warn(m, "e.service", NULL, &e) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "d.service", NULL, LOG_ERR, &d) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "e.service", NULL, LOG_ERR, &e) >= 0); manager_dump_units(m, stdout, /* patterns= */ NULL, "\t"); printf("Test2: (Cyclic Order, Unfixable)\n"); @@ -131,7 +131,7 @@ int main(int argc, char *argv[]) { manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t"); printf("Load3:\n"); - assert_se(manager_load_startable_unit_or_warn(m, "g.service", NULL, &g) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "g.service", NULL, LOG_ERR, &g) >= 0); manager_dump_units(m, stdout, /* patterns= */ NULL, "\t"); printf("Test5: (Colliding transaction, fail)\n"); @@ -153,7 +153,7 @@ int main(int argc, char *argv[]) { manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t"); printf("Load4:\n"); - assert_se(manager_load_startable_unit_or_warn(m, "h.service", NULL, &h) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "h.service", NULL, LOG_ERR, &h) >= 0); manager_dump_units(m, stdout, /* patterns= */ NULL, "\t"); printf("Test10: (Unmergeable job type of auxiliary job, fail)\n"); @@ -162,7 +162,7 @@ int main(int argc, char *argv[]) { printf("Load5:\n"); manager_clear_jobs(m); - assert_se(manager_load_startable_unit_or_warn(m, "i.service", NULL, &i) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "i.service", NULL, LOG_ERR, &i) >= 0); SERVICE(a)->state = SERVICE_RUNNING; SERVICE(d)->state = SERVICE_RUNNING; manager_dump_units(m, stdout, /* patterns= */ NULL, "\t"); @@ -176,7 +176,7 @@ int main(int argc, char *argv[]) { printf("Load6:\n"); manager_clear_jobs(m); - assert_se(manager_load_startable_unit_or_warn(m, "a-conj.service", NULL, &a_conj) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "a-conj.service", NULL, LOG_ERR, &a_conj) >= 0); SERVICE(a)->state = SERVICE_DEAD; manager_dump_units(m, stdout, /* patterns= */ NULL, "\t"); diff --git a/src/test/test-execute.c b/src/test/test-execute.c index 1a832ad2f7..8206452f4c 100644 --- a/src/test/test-execute.c +++ b/src/test/test-execute.c @@ -304,7 +304,7 @@ static void _test(const char *file, unsigned line, const char *func, ASSERT_NOT_NULL(unit_name); - ASSERT_OK(manager_load_startable_unit_or_warn(m, unit_name, NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, unit_name, NULL, LOG_ERR, &unit)); /* We need to start the slices as well otherwise the slice cgroups might be pruned * in on_cgroup_empty_event. */ start_parent_slices(unit); @@ -322,7 +322,7 @@ static void _test_service(const char *file, unsigned line, const char *func, ASSERT_NOT_NULL(unit_name); - ASSERT_OK(manager_load_startable_unit_or_warn(m, unit_name, NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, unit_name, NULL, LOG_ERR, &unit)); ASSERT_OK(unit_start(unit, NULL)); check_service_result(file, line, func, m, unit, result_expected); } diff --git a/src/test/test-path.c b/src/test/test-path.c index ff1dee8474..3a4aa64c2f 100644 --- a/src/test/test-path.c +++ b/src/test/test-path.c @@ -150,7 +150,7 @@ TEST_RET(path_exists) { return r; Unit *unit; - ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-exists.path", NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-exists.path", NULL, LOG_ERR, &unit)); Path *path = PATH(unit); Service *service = service_for_path(m, path, NULL); @@ -183,7 +183,7 @@ TEST_RET(path_existsglob) { return r; Unit *unit; - ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-existsglob.path", NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-existsglob.path", NULL, LOG_ERR, &unit)); Path *path = PATH(unit); Service *service = service_for_path(m, path, NULL); @@ -216,7 +216,7 @@ TEST_RET(path_changed) { return r; Unit *unit; - ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-changed.path", NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-changed.path", NULL, LOG_ERR, &unit)); Path *path = PATH(unit); Service *service = service_for_path(m, path, NULL); @@ -253,7 +253,7 @@ TEST_RET(path_modified) { return r; Unit *unit; - ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-modified.path", NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-modified.path", NULL, LOG_ERR, &unit)); Path *path = PATH(unit); Service *service = service_for_path(m, path, NULL); @@ -291,7 +291,7 @@ TEST_RET(path_unit) { return r; Unit *unit; - ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-unit.path", NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-unit.path", NULL, LOG_ERR, &unit)); Path *path = PATH(unit); Service *service = service_for_path(m, path, "path-mycustomunit.service"); @@ -320,7 +320,7 @@ TEST_RET(path_directorynotempty) { return r; Unit *unit; - ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-directorynotempty.path", NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-directorynotempty.path", NULL, LOG_ERR, &unit)); Path *path = PATH(unit); Service *service = service_for_path(m, path, NULL); @@ -360,7 +360,7 @@ TEST_RET(path_makedirectory_directorymode) { return r; Unit *unit; - ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-makedirectory.path", NULL, &unit)); + ASSERT_OK(manager_load_startable_unit_or_warn(m, "path-makedirectory.path", NULL, LOG_ERR, &unit)); ASSERT_FAIL(access(test_path, F_OK)); diff --git a/src/test/test-sched-prio.c b/src/test/test-sched-prio.c index a523c01e8f..83204eb0d9 100644 --- a/src/test/test-sched-prio.c +++ b/src/test/test-sched-prio.c @@ -36,7 +36,7 @@ int main(int argc, char *argv[]) { assert_se(manager_startup(m, NULL, NULL, NULL, NULL) >= 0); /* load idle ok */ - assert_se(manager_load_startable_unit_or_warn(m, "sched_idle_ok.service", NULL, &idle_ok) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "sched_idle_ok.service", NULL, LOG_ERR, &idle_ok) >= 0); ser = SERVICE(idle_ok); assert_se(ser->exec_context.cpu_sched_policy == SCHED_OTHER); assert_se(ser->exec_context.cpu_sched_priority == 0); @@ -44,7 +44,7 @@ int main(int argc, char *argv[]) { /* * load idle bad. This should print a warning but we have no way to look at it. */ - assert_se(manager_load_startable_unit_or_warn(m, "sched_idle_bad.service", NULL, &idle_bad) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "sched_idle_bad.service", NULL, LOG_ERR, &idle_bad) >= 0); ser = SERVICE(idle_ok); assert_se(ser->exec_context.cpu_sched_policy == SCHED_OTHER); assert_se(ser->exec_context.cpu_sched_priority == 0); @@ -53,7 +53,7 @@ int main(int argc, char *argv[]) { * load rr ok. * Test that the default priority is moving from 0 to 1. */ - assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_ok.service", NULL, &rr_ok) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_ok.service", NULL, LOG_ERR, &rr_ok) >= 0); ser = SERVICE(rr_ok); assert_se(ser->exec_context.cpu_sched_policy == SCHED_RR); assert_se(ser->exec_context.cpu_sched_priority == 1); @@ -62,7 +62,7 @@ int main(int argc, char *argv[]) { * load rr bad. * Test that the value of 0 and 100 is ignored. */ - assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_bad.service", NULL, &rr_bad) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_bad.service", NULL, LOG_ERR, &rr_bad) >= 0); ser = SERVICE(rr_bad); assert_se(ser->exec_context.cpu_sched_policy == SCHED_RR); assert_se(ser->exec_context.cpu_sched_priority == 1); @@ -71,13 +71,13 @@ int main(int argc, char *argv[]) { * load rr change. * Test that anything between 1 and 99 can be set. */ - assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_change.service", NULL, &rr_sched) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_change.service", NULL, LOG_ERR, &rr_sched) >= 0); ser = SERVICE(rr_sched); assert_se(ser->exec_context.cpu_sched_policy == SCHED_RR); assert_se(ser->exec_context.cpu_sched_priority == 99); /* load ext ok */ - assert_se(manager_load_startable_unit_or_warn(m, "sched_ext_ok.service", NULL, &ext_ok) >= 0); + assert_se(manager_load_startable_unit_or_warn(m, "sched_ext_ok.service", NULL, LOG_ERR, &ext_ok) >= 0); ser = SERVICE(ext_ok); assert_se(ser->exec_context.cpu_sched_policy == SCHED_EXT); assert_se(ser->exec_context.cpu_sched_priority == 0); From 8985fc5e571a4bfc26ee63dc96e2ff82e5f6794c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 23 Jun 2026 11:57:30 +0200 Subject: [PATCH 13/17] manager: try to reexecute as /proc/self/exe too If we try to reexecute, and the binary was started from a non-standard path, this would fail: src/core/main.c:2396: Reexecuting. src/core/main.c:2240: Failed to execute our own binary /usr/lib/systemd/systemd: No such file or directory Let's make things more robust by trying /proc/self/exe too. --- src/core/main.c | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/core/main.c b/src/core/main.c index b967a57264..c0f68222ed 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -52,6 +52,7 @@ #include "fileio.h" #include "format-table.h" #include "format-util.h" +#include "fs-util.h" #include "glyph-util.h" #include "hash-funcs.h" #include "hashmap.h" @@ -2130,6 +2131,7 @@ static int do_reexecute( const char **ret_error_message) { size_t i, args_size; + _cleanup_free_ char *our_exe = NULL; const char **args; int r; @@ -2240,14 +2242,32 @@ static int do_reexecute( valgrind_summary_hack(); args[0] = SYSTEMD_BINARY_PATH; - (void) execv(args[0], (char* const*) args); + r = RET_NERRNO(execv(args[0], (char* const*) args)); if (objective == MANAGER_REEXECUTE) { + /* If the binary is missing, try with our current executable too. This way things + * continue to work if the binary is installed into a non-default location. Note that + * we don't use chase here, the symlink the kernel gives is good enough. */ + if (r == -ENOENT) { + int k; + + k = readlink_malloc("/proc/self/exe", &our_exe); + if (k < 0) + log_warning_errno(k, "Failed to read /proc/self/exe: %m"); + else if (endswith(our_exe, " (deleted)")) + log_info("The current binary (/proc/self/exe) has been deleted."); + else { + log_info_errno(r, "Failed to execute our own binary %s, trying /proc/self/exe: %m", args[0]); + args[0] = "/proc/self/exe"; + r = RET_NERRNO(execv(args[0], (char* const*) args)); + } + } + *ret_error_message = "Failed to execute our own binary"; - return log_error_errno(errno, "Failed to execute our own binary %s: %m", args[0]); + return log_error_errno(r, "Failed to execute our own binary %s: %m", args[0]); } - log_debug_errno(errno, "Failed to execute our own binary %s, trying fallback: %m", args[0]); + log_debug_errno(r, "Failed to execute our own binary %s, trying fallback: %m", args[0]); } /* Try the fallback, if there is any, without any serialization. We pass the original argv[] and @@ -2283,13 +2303,12 @@ static int do_reexecute( if (switch_root_init) { args[0] = switch_root_init; - (void) execve(args[0], (char* const*) args, saved_env); - log_warning_errno(errno, "Failed to execute configured init %s, trying fallback: %m", args[0]); + r = RET_NERRNO(execve(args[0], (char* const*) args, saved_env)); + log_warning_errno(r, "Failed to execute configured init %s, trying fallback: %m", args[0]); } args[0] = "/sbin/init"; - (void) execv(args[0], (char* const*) args); - r = -errno; + r = RET_NERRNO(execv(args[0], (char* const*) args)); *ret_error_message = "Failed to execute /sbin/init"; if (r == -ENOENT) { @@ -2301,8 +2320,7 @@ static int do_reexecute( args[0] = "/bin/sh"; args[1] = NULL; - (void) execve(args[0], (char* const*) args, saved_env); - r = -errno; + r = RET_NERRNO(execve(args[0], (char* const*) args, saved_env)); *ret_error_message = "Failed to execute fallback shell"; } From 2e4ef3dca530695bf0f099542add31f2ff5fcc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 23 Jun 2026 18:31:42 +0200 Subject: [PATCH 14/17] core: rename and extend manager_add_job_by_name_and_warn The "and" in the name was a misnomer. We do one *or* the other. So rename 's/_and_/_or_/', and extend with the ability to load a second unit. No functional change. --- src/core/emergency-action.c | 12 ++++++------ src/core/manager.c | 32 +++++++++++++++++++++++--------- src/core/manager.h | 2 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/core/emergency-action.c b/src/core/emergency-action.c index b9a5c66ebf..181da27f7a 100644 --- a/src/core/emergency-action.c +++ b/src/core/emergency-action.c @@ -113,7 +113,7 @@ void emergency_action( log_and_status(m, action, flags, "Rebooting", reason); (void) update_reboot_parameter_and_warn(reboot_arg, true); - (void) manager_add_job_by_name_and_warn(m, JOB_START, SPECIAL_REBOOT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); + (void) manager_add_job_by_name_or_warn(m, JOB_START, SPECIAL_REBOOT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); break; case EMERGENCY_ACTION_REBOOT_FORCE: @@ -141,7 +141,7 @@ void emergency_action( case EMERGENCY_ACTION_SOFT_REBOOT: log_and_status(m, action, flags, "Soft-rebooting", reason); - (void) manager_add_job_by_name_and_warn(m, JOB_START, SPECIAL_SOFT_REBOOT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); + (void) manager_add_job_by_name_or_warn(m, JOB_START, SPECIAL_SOFT_REBOOT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); break; case EMERGENCY_ACTION_SOFT_REBOOT_FORCE: @@ -157,7 +157,7 @@ void emergency_action( if (MANAGER_IS_USER(m) || detect_container() > 0) { log_and_status(m, action, flags, "Exiting", reason); - (void) manager_add_job_by_name_and_warn(m, JOB_START, SPECIAL_EXIT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); + (void) manager_add_job_by_name_or_warn(m, JOB_START, SPECIAL_EXIT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); break; } @@ -166,7 +166,7 @@ void emergency_action( case EMERGENCY_ACTION_POWEROFF: log_and_status(m, action, flags, "Powering off", reason); - (void) manager_add_job_by_name_and_warn(m, JOB_START, SPECIAL_POWEROFF_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); + (void) manager_add_job_by_name_or_warn(m, JOB_START, SPECIAL_POWEROFF_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); break; case EMERGENCY_ACTION_EXIT_FORCE: @@ -199,7 +199,7 @@ void emergency_action( case EMERGENCY_ACTION_KEXEC: log_and_status(m, action, flags, "Executing kexec", reason); - (void) manager_add_job_by_name_and_warn(m, JOB_START, SPECIAL_KEXEC_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); + (void) manager_add_job_by_name_or_warn(m, JOB_START, SPECIAL_KEXEC_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); break; case EMERGENCY_ACTION_KEXEC_FORCE: @@ -209,7 +209,7 @@ void emergency_action( case EMERGENCY_ACTION_HALT: log_and_status(m, action, flags, "Halting", reason); - (void) manager_add_job_by_name_and_warn(m, JOB_START, SPECIAL_HALT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); + (void) manager_add_job_by_name_or_warn(m, JOB_START, SPECIAL_HALT_TARGET, JOB_REPLACE_IRREVERSIBLY, NULL, NULL); break; case EMERGENCY_ACTION_HALT_FORCE: diff --git a/src/core/manager.c b/src/core/manager.c index baaac37482..2e0ac49d98 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -2527,22 +2527,36 @@ int manager_add_job_by_name(Manager *m, JobType type, const char *name, JobMode return manager_add_job_full(m, type, unit, mode, /* extra_flags= */ 0, affected_jobs, e, ret); } -int manager_add_job_by_name_and_warn(Manager *m, JobType type, const char *name, JobMode mode, Set *affected_jobs, Job **ret) { +static int manager_add_job_by_name_or_warn_with_fallback( + Manager *m, + JobType type, + const char *name, + const char *fallback, + JobMode mode, + Set *affected_jobs, + Job **ret) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; int r; - assert(m); - assert(type < _JOB_TYPE_MAX); - assert(name); - assert(mode < _JOB_MODE_MAX); - r = manager_add_job_by_name(m, type, name, mode, affected_jobs, &error, ret); + if (r == -ENOENT && fallback) { + log_debug_errno(r, "Failed to enqueue job %s/%s, fallback to %s: %s", + name, job_mode_to_string(mode), fallback, bus_error_message(&error, r)); + sd_bus_error_free(&error); + name = fallback; + r = manager_add_job_by_name(m, type, name, mode, affected_jobs, &error, ret); + } if (r < 0) - return log_warning_errno(r, "Failed to enqueue %s job for %s: %s", job_mode_to_string(mode), name, bus_error_message(&error, r)); - + return log_warning_errno(r, "Failed to enqueue job %s/%s: %s", + name, job_mode_to_string(mode), bus_error_message(&error, r)); return r; } +int manager_add_job_by_name_or_warn(Manager *m, JobType type, const char *name, JobMode mode, Set *affected_jobs, Job **ret) { + return manager_add_job_by_name_or_warn_with_fallback(m, type, name, /* fallback= */ NULL, mode, affected_jobs, ret); +} + int manager_propagate_reload(Manager *m, Unit *unit, JobMode mode, sd_bus_error *e) { _cleanup_(transaction_abort_and_freep) Transaction *tr = NULL; int r; @@ -3209,7 +3223,7 @@ turn_off: static void manager_start_special(Manager *m, const char *name, JobMode mode) { Job *job; - if (manager_add_job_by_name_and_warn(m, JOB_START, name, mode, NULL, &job) < 0) + if (manager_add_job_by_name_or_warn(m, JOB_START, name, mode, NULL, &job) < 0) return; const char *s = unit_status_string(job->unit, NULL); diff --git a/src/core/manager.h b/src/core/manager.h index ab5187cde4..8262bfbec8 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -624,7 +624,7 @@ int manager_add_job( Job **ret); int manager_add_job_by_name(Manager *m, JobType type, const char *name, JobMode mode, Set *affected_jobs, sd_bus_error *e, Job **ret); -int manager_add_job_by_name_and_warn(Manager *m, JobType type, const char *name, JobMode mode, Set *affected_jobs, Job **ret); +int manager_add_job_by_name_or_warn(Manager *m, JobType type, const char *name, JobMode mode, Set *affected_jobs, Job **ret); int manager_propagate_reload(Manager *m, Unit *unit, JobMode mode, sd_bus_error *e); void manager_clear_jobs(Manager *m); From 83e4074af172c2d3e5e72e9f0c256d3be37c1c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 23 Jun 2026 18:50:25 +0200 Subject: [PATCH 15/17] manager: use reboot.target as the default for ctrl-alt-del.target systemd tries to start ctrl-alt-del.target after receiving a request to reboot in a container (or when ctrl-alt-del is pressed on a real machine). If the alias is missing, this will fail. Let's provide a fallback for this too. I don't think it makes sense to make it configurable: it seems almost everybody is fine with the default mapping. If not, people can always just create the symlink in the filesystem. --- src/basic/special.h | 3 +++ src/core/manager.c | 14 +++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/basic/special.h b/src/basic/special.h index a5cdfebae5..a5774f9590 100644 --- a/src/basic/special.h +++ b/src/basic/special.h @@ -110,6 +110,9 @@ #define SPECIAL_SIGPWR_TARGET "sigpwr.target" #define SPECIAL_CTRL_ALT_DEL_TARGET "ctrl-alt-del.target" +/* The unit to use as fallback if ctrl-alt-del.target (which is usually an alias) cannot be loaded. */ +#define FALLBACK_CTRL_ALT_DEL_TARGET "reboot.target" + /* Where we add all our system units, users and machines by default */ #define SPECIAL_SYSTEM_SLICE "system.slice" #define SPECIAL_USER_SLICE "user.slice" diff --git a/src/core/manager.c b/src/core/manager.c index 2e0ac49d98..ae44f87b2a 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -3220,10 +3220,10 @@ turn_off: return 0; } -static void manager_start_special(Manager *m, const char *name, JobMode mode) { +static void manager_start_special_with_fallback(Manager *m, const char *name, const char *fallback, JobMode mode) { Job *job; - if (manager_add_job_by_name_or_warn(m, JOB_START, name, mode, NULL, &job) < 0) + if (manager_add_job_by_name_or_warn_with_fallback(m, JOB_START, name, fallback, mode, /* affected_jobs= */ NULL, &job) < 0) return; const char *s = unit_status_string(job->unit, NULL); @@ -3235,6 +3235,10 @@ static void manager_start_special(Manager *m, const char *name, JobMode mode) { m->status_ready = false; } +static void manager_start_special(Manager *m, const char *name, JobMode mode) { + manager_start_special_with_fallback(m, name, /* fallback= */ NULL, mode); +} + static void manager_handle_ctrl_alt_del(Manager *m) { assert(m); @@ -3242,7 +3246,11 @@ static void manager_handle_ctrl_alt_del(Manager *m) { * unless it was disabled in system.conf. */ if (ratelimit_below(&m->ctrl_alt_del_ratelimit) || m->cad_burst_action == EMERGENCY_ACTION_NONE) - manager_start_special(m, SPECIAL_CTRL_ALT_DEL_TARGET, JOB_REPLACE_IRREVERSIBLY); + manager_start_special_with_fallback( + m, + SPECIAL_CTRL_ALT_DEL_TARGET, + FALLBACK_CTRL_ALT_DEL_TARGET, + JOB_REPLACE_IRREVERSIBLY); else emergency_action( m, From 9edf0e839f973b369eaa52edc6c8ee65f4483a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 30 Jun 2026 19:22:33 +0200 Subject: [PATCH 16/17] manager: fall back to direct reboot() if shutdown binary is missing When PID 1 reaches the shutdown phase, if systemd-shutdown was missing, it'd complain loudly and freeze. Implement an internal fallback to handle this case gracefully. The fallback is always in place, since it makes things generally more robust and is only a little bit of code. This shutdown is simplified, systemd issues the reboot() matching the requested objective directly, mirroring what systemd-shutdown would do as its final step. This skips all the cleanup systemd-shutdown normally performs (unmounting file systems, detaching storage, killing remaining processes, switching into the exitrd). With SYSTEMD_LOG_LEVEL=debug: ... [ OK ] Reached target poweroff.target. Notify message sent to '/run/host/notify': "X_SYSTEMD_UNIT_ACTIVE=poweroff.target" Shutting down. OSC sequence for shutdown successfully written: ... Failed to execute shutdown binary: No such file or directory Powering off. Container container has been shut down. --- src/core/main.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/core/main.c b/src/core/main.c index c0f68222ed..5c02e30880 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -95,6 +95,7 @@ #include "proc-cmdline.h" #include "process-util.h" #include "random-util.h" +#include "reboot-util.h" #include "rlimit-util.h" #include "rm-rf.h" #include "seccomp-util.h" @@ -1861,6 +1862,52 @@ static int become_shutdown(int objective, int retval) { return -errno; } +static int fallback_shutdown(ManagerObjective objective, int retval) { + int cmd; + + /* Perform the requested shutdown operation ourselves, by issuing the matching reboot() directly. + * This skips the cleanup systemd-shutdown normally does (unmounting file systems, detaching storage, + * killing remaining processes, switching into the exitrd). */ + + switch (objective) { + + case MANAGER_KEXEC: + log_info("Rebooting with kexec."); + (void) kexec(); + /* If we are still here kexec didn't work, fall back to a plain reboot. */ + _fallthrough_; + + case MANAGER_REBOOT: + (void) reboot_with_parameter(REBOOT_LOG); + log_info("Rebooting."); + cmd = RB_AUTOBOOT; + break; + + case MANAGER_EXIT: + if (detect_container() > 0) { + log_info("Exiting container."); + exit(retval); + } + _fallthrough_; + + case MANAGER_POWEROFF: + log_info("Powering off."); + cmd = RB_POWER_OFF; + break; + + case MANAGER_HALT: + log_info("Halting system."); + cmd = RB_HALT_SYSTEM; + break; + + default: + assert_not_reached(); + } + + (void) reboot(cmd); + return log_error_errno(errno, "Failed to shut down: %m"); +} + static void initialize_clock_timewarp(void) { int r; @@ -3934,9 +3981,20 @@ finish: * If we failed above, we want to freeze after finishing cleanup. */ if (arg_runtime_scope == RUNTIME_SCOPE_SYSTEM && IN_SET(r, MANAGER_EXIT, MANAGER_REBOOT, MANAGER_POWEROFF, MANAGER_HALT, MANAGER_KEXEC)) { - r = become_shutdown(r, retval); - log_error_errno(r, "Failed to execute shutdown binary, %s: %m", getpid_cached() == 1 ? "freezing" : "quitting"); - error_message = "Failed to execute shutdown binary"; + ManagerObjective objective = r; + + r = become_shutdown(objective, retval); + assert(r < 0); + log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, + "Failed to execute shutdown binary: %m"); + + /* As PID 1, carry out the requested operation ourselves rather than relying on the shutdown + * binary. This only returns if that didn't work, in which case we freeze/exit/reboot + * below. */ + r = fallback_shutdown(objective, retval); + assert(r < 0); + log_error_errno(r, "Failed to perform shutdown: %m"); + error_message = "Failed to perform shutdown"; } /* This is primarily useful when running systemd in a VM, as it provides the user running the VM with From 833b8d98e04d368b05717d4fbbac7db5303cfbee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Wed, 1 Jul 2026 21:02:42 +0200 Subject: [PATCH 17/17] NEWS,README: describe embedded files and static linking --- NEWS | 9 ++++++++ README | 70 ++++++++++++++++++++++++++++++++++++---------------------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/NEWS b/NEWS index ef3d070df1..80f83da243 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,15 @@ CHANGES WITH 262: have been provided. This clears the way for a new "systemd-sysupdate@.service" unit for varlink activation of sysupdate. + Changes in the system and service manager: + + * The manager now embeds a basic set of unit files (basic.target, + sysinit.target, multi-user.target, reboot.target, shutodown.target, + systemd-poweroff.service, etc.) and will use if files cannot be + loaded from disk. This makes the binary slightly more robust and + allows a container to be started with systemd as PID1 without + installing any unit files. + Changes in systemd-firstboot: * The "systemd.firstboot=" kernel command line option now accepts the diff --git a/README b/README index 5553915be0..279587367b 100644 --- a/README +++ b/README @@ -328,42 +328,49 @@ POLICY FOR SUPPORT OF DISTRIBUTIONS AND ARCHITECTURES: which support has been added. systemd will emit warnings when architecture-specific constants are not defined. -STATIC COMPILATION AND "STANDALONE" BINARIES: - systemd provides a public shared libraries libsystemd.so and - libudev.so. The latter is deprecated, and the sd-device APIs in - libsystemd should be used instead for new code. In addition, systemd is - built with a private shared library, libsystemd-shared-.so, - that also includes the libsystemd code, and by default most systemd - binaries are linked to it. Using shared libraries saves disk space and - memory at runtime, because only one copy of the code is needed. +STATIC LIBRARIES AND "STANDALONE" BINARIES: + systemd provides public shared libraries libsystemd.so and libudev.so. + The latter is deprecated, and the sd-device APIs in libsystemd should + be used instead for new code. Normally systemd is built with a private + shared library, libsystemd-shared-.so, that also includes the + libsystemd code, and by default most systemd binaries are linked to it. + Using shared libraries saves disk space and memory at runtime, because + only one copy of the code is needed. It is possible to build static versions of systemd public shared libraries (via the configuration options '-Dstatic-libsystemd' and '-Dstatic-libudev'). This allows the libsystemd and libudev code to be linked statically into programs. Note that mixing & matching different versions of libsystemd and systemd is generally not recommended, since - various of its APIs wrap internal state and protocols of systemd - (e.g. logind and udev databases), which are not considered - stable. Hence, using static libraries is not recommended since it - generally means that version of the static libsystemd linked into - applications and the host systemd are not in sync, and will thus create - compatibility problems. + various of its APIs wrap internal state and protocols of systemd (e.g. + logind and udev databases), which are not considered stable. Hence, + using static libraries is not recommended since it generally means that + version of the static libsystemd linked into applications and the host + systemd may get out of sync, creating compatibility problems. In addition, it is possible to disable the use of libsystemd-shared-.so for various components (via the configuration options '-Dlink-*-shared'). In this mode, the libsystemd - and libsystemd-shared code is linked statically into selected - binaries. This option is intended for systems where some of the - components are intended to be delivered independently of the main - systemd package. Finally, some binaries can be compiled in a second - version (via the configuration option '-Dstandalone-binaries'). The - version suffixed with ".standalone" has the libsystemd and - libsystemd-shared code linked statically. Those binaries are intended - as replacements to be used in limited installations where the full - systemd suite is not installed. Yet another option is to rebuild - systemd with a different '-Dshared-lib-tag' setting, allowing different - systemd binaries to be linked to instances of the private shared - library that can be installed in parallel. + and libsystemd-shared code is linked statically into selected binaries. + This option is intended for systems where some of the components are + intended to be delivered independently of the main systemd package. + + In addition, some binaries can be compiled in a second version (via the + configuration option '-Dstandalone-binaries'). The version suffixed + with ".standalone" has the libsystemd and libsystemd-shared code linked + statically. Those binaries are intended as replacements to be used in + limited installations where the full systemd suite is not installed. An + alternative approach is to rebuild systemd with a different + '-Dshared-lib-tag' setting, allowing different systemd binaries to be + linked to instances of the private shared library that can be installed + in parallel. + + When compiled in one of the modes described in this section, the code + has identical functionality (apart from the potential compatibility + issues). + + Finally, fully static compilation of systemd (the program) is possible, + see next section. Again: Using the default shared linking is recommended, building static or "standalone" versions is not. Mixing versions of systemd components @@ -372,6 +379,17 @@ STATIC COMPILATION AND "STANDALONE" BINARIES: combinations upstream and cannot provide support. Distributors making use of those options are responsible if things do not work as expected. +STATIC BINARIES: + It is possible to compile systemd (the program) as a static binary with + the meson options: + --default-library=static --prefer-static -Dbuild-static=true -Dsystemd-multicall-binary=true + This results in a static 'systemd' binary that can be used as PID1 in a + container. This binary does not use dlopen(3), which means that it + cannot dynamically extend its functionality by loading other libraries, + even if they were available in the file system. It also does not + support NSS and instead uses simplified code that only supports static + passwd and group files to resolve users. + USERS AND GROUPS: Default udev rules use the following standard system group names, which need to be resolvable by getgrnam() at any time, even in the very early