From 14af94428a6eb8d140391db22f29c48e090ac55e Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 10 Feb 2026 21:53:02 +0100 Subject: [PATCH 01/17] shared-forward: sort definitions --- src/shared/shared-forward.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index 4804dd9e49e..f35287746b0 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -45,8 +45,8 @@ typedef enum UserRecordLoadFlags UserRecordLoadFlags; typedef enum UserStorage UserStorage; typedef struct Bitmap Bitmap; -typedef struct BPFProgram BPFProgram; typedef struct BootConfig BootConfig; +typedef struct BPFProgram BPFProgram; typedef struct BusObjectImplementation BusObjectImplementation; typedef struct CalendarSpec CalendarSpec; typedef struct Condition Condition; From f9e0a62392fa62364bb7ac31a1cd4a8cc521cced Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 10 Feb 2026 15:58:06 +0100 Subject: [PATCH 02/17] sd-daemon: escape special characters in notification logging text Follow-up for 21eb636aaad28707bd371bdfd721dea66277e1f5 Our sd_notify() protocol uses newline as separator, which is not suitable for direct logging. --- src/libsystemd/sd-daemon/sd-daemon.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libsystemd/sd-daemon/sd-daemon.c b/src/libsystemd/sd-daemon/sd-daemon.c index 8d7dd0823f7..2ab50287b4f 100644 --- a/src/libsystemd/sd-daemon/sd-daemon.c +++ b/src/libsystemd/sd-daemon/sd-daemon.c @@ -13,6 +13,7 @@ #include "alloc-util.h" #include "errno-util.h" +#include "escape.h" #include "extract-word.h" #include "fd-util.h" #include "fs-util.h" @@ -633,7 +634,11 @@ static int pid_notify_with_fds_internal( return log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Unexpectedly received data on notify socket."); } - log_debug("Notify message sent to '%s': \"%s\"", e, state); + if (DEBUG_LOGGING) { + _cleanup_free_ char *escaped = xescape_full(state, "\"", /* console_width = */ SIZE_MAX, XESCAPE_8_BIT); + log_debug("Notify message sent to '%s': \"%s\"", e, escaped ?: state); + } + return 1; } From de65956bdb0f53a3fccb145d21358b0a53f5b0f8 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sat, 13 Dec 2025 18:11:07 +0100 Subject: [PATCH 03/17] core/exec-credential: treat credentials dir as populated if it's mounted We should only fall back to the dir_is_empty() check if it's a plain dir, where we can't reasonably differentiate populated yet empty vs not set up. Otherwise let's stick to the existing mount if we're told to reuse it. Yes, this is a minor compat break, but with the to-be-introduced credential reloading support it should fulfill the goal of keeping the passed set of credentials stable better, while still allowing things to be refreshed when requested. --- src/core/exec-credential.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 1971aafee88..79ec0da49d7 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -1020,22 +1020,32 @@ static int setup_credentials_internal( assert(unit); assert(cred_dir); - if (!FLAGS_SET(params->flags, EXEC_SETUP_CREDENTIALS_FRESH)) { - /* We may reuse the previous credential dir */ - r = dir_is_empty(cred_dir, /* ignore_hidden_or_backup= */ false); - if (r < 0) - return r; - if (r == 0) { - log_debug("Credential dir for unit '%s' already set up, skipping.", unit); - return 0; - } - } - r = path_is_mount_point(cred_dir); if (r < 0) return log_debug_errno(r, "Failed to determine if '%s' is a mountpoint: %m", cred_dir); dir_mounted = r > 0; + if (!FLAGS_SET(params->flags, EXEC_SETUP_CREDENTIALS_FRESH)) { + bool populated; + + /* If the cred dir is a mount, let's treat it as populated, and only look at the contents + * if it's a plain dir, where we can't reasonably differentiate populated yet empty vs + * not set up. */ + + if (dir_mounted) + populated = true; + else { + r = dir_is_empty(cred_dir, /* ignore_hidden_or_backup= */ false); + if (r < 0) + return r; + populated = r == 0; + } + if (populated) { + log_debug("Credential dir for unit '%s' already set up, skipping.", unit); + return 0; + } + } + mfd = fsmount_credentials_fs(&fs_fd); if (ERRNO_IS_NEG_PRIVILEGE(mfd) && !dir_mounted) { log_debug_errno(mfd, "Lacking privilege to mount credentials fs, falling back to plain directory."); From 482fb9ae4015fa898564476a20a8422f750e308b Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sat, 13 Dec 2025 17:37:26 +0100 Subject: [PATCH 04/17] core/exec-credential: stop removing empty credentials dir Starting from cfbf7538d87023840c5574fa5b0452e5b0f42149 we'd always install the credentials dir regardless of whether it's empty, with the correct permissions. Hence the problem stated in the comment should no longer be a concern. Moreover, this ensures later in setup_namespace() the mountpoint would be in-place. This is important for credential reloading as it saves the trouble of remounting the upper tmpfs as rw again and create the mountpoint. --- src/core/exec-credential.c | 7 ++----- src/core/namespace.c | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 79ec0da49d7..531845623f6 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -1145,11 +1145,8 @@ int exec_setup_credentials( return r; r = setup_credentials_internal(context, cgroup_context, params, unit, p, uid, gid); + if (r < 0) + (void) rmdir(p); - /* If the credentials dir is empty and not a mount point, then there's no point in having it. Let's - * try to remove it. This matters in particular if we created the dir as mount point but then didn't - * actually end up mounting anything on it. In that case we'd rather have ENOENT than EACCESS being - * seen by users when trying access this inode. */ - (void) rmdir(p); return r; } diff --git a/src/core/namespace.c b/src/core/namespace.c index 6e9c6a09f87..0ed686f9ece 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2845,7 +2845,6 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { .mode = MOUNT_BIND, .read_only = true, .source_const = p->creds_path, - .ignore = true, }; } From 7c358751a4f7a2f4e21e9d3a4f8ca7d3436c0bba Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 18 Jan 2026 18:32:46 +0100 Subject: [PATCH 05/17] core/exec-credential: avoid duplicate call to hashmap_contains() --- src/core/exec-credential.c | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 531845623f6..83ed112471e 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -695,17 +695,20 @@ static int load_credential( else assert_not_reached(); - if (r == -ENOENT && (missing_ok || hashmap_contains(args->context->set_credentials, id))) { - /* Make a missing inherited credential non-fatal, let's just continue. After all apps - * will get clear errors if we don't pass such a missing credential on as they - * themselves will get ENOENT when trying to read them, which should not be much - * worse than when we handle the error here and make it fatal. - * - * Also, if the source file doesn't exist, but a fallback is set via SetCredentials= - * we are fine, too. */ - log_full_errno(hashmap_contains(args->context->set_credentials, id) ? LOG_DEBUG : LOG_INFO, - r, "Couldn't read inherited credential '%s', skipping: %m", path); - return 0; + if (r == -ENOENT) { + bool in_set_credentials = hashmap_contains(args->context->set_credentials, id); + if (missing_ok || in_set_credentials) { + /* Make a missing inherited credential non-fatal, let's just continue. After all apps + * will get clear errors if we don't pass such a missing credential on as they + * themselves will get ENOENT when trying to read them, which should not be much + * worse than when we handle the error here and make it fatal. + * + * Also, if the source file doesn't exist, but a fallback is set via SetCredentials= + * we are fine, too. */ + log_full_errno(in_set_credentials ? LOG_DEBUG : LOG_INFO, + r, "Couldn't read inherited credential '%s', skipping: %m", path); + return 0; + } } if (r < 0) return log_debug_errno(r, "Failed to read credential '%s': %m", path); From bae58ceff7c7a10280017a64b12ff097dba48147 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 14 Dec 2025 08:56:35 +0100 Subject: [PATCH 06/17] core/exec-credential: only pass needed bits of ExecParameters down in exec_setup_credentials() No functional change, preparation for credential reloading support, where we'd operate on Unit rather than ExecParameters. --- src/core/exec-credential.c | 179 +++++++++++++++++++------------------ src/core/exec-credential.h | 1 - src/core/exec-invoke.c | 2 +- 3 files changed, 93 insertions(+), 89 deletions(-) diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 83ed112471e..3766096a416 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -310,6 +310,34 @@ int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_p return 0; } +typedef struct SetupCredentialsContext { + RuntimeScope scope; + + const ExecContext *exec_context; + const char *unit; + + const char *runtime_prefix; + + const char *received_credentials_directory; + const char *received_encrypted_credentials_directory; + + bool always_ipc; + + uid_t uid; + gid_t gid; +} SetupCredentialsContext; + +typedef struct LoadCredentialArguments { + const SetupCredentialsContext *context; + + bool encrypted; + + int write_dfd; + bool ownership_ok; + + uint64_t left; +} LoadCredentialArguments; + typedef enum CredentialSearchPath { CREDENTIAL_SEARCH_PATH_TRUSTED, CREDENTIAL_SEARCH_PATH_ENCRYPTED, @@ -318,11 +346,15 @@ typedef enum CredentialSearchPath { _CREDENTIAL_SEARCH_PATH_INVALID = -EINVAL, } CredentialSearchPath; -static int credential_search_path(const ExecParameters *params, CredentialSearchPath path, char ***ret) { +static int credential_search_path( + const SetupCredentialsContext *context, + CredentialSearchPath path, + char ***ret) { + _cleanup_strv_free_ char **l = NULL; int r; - assert(params); + assert(context); assert(path >= 0 && path < _CREDENTIAL_SEARCH_PATH_MAX); assert(ret); @@ -331,12 +363,12 @@ static int credential_search_path(const ExecParameters *params, CredentialSearch * credentials, we'll look in /etc/credstore.encrypted/ (and similar dirs). */ if (IN_SET(path, CREDENTIAL_SEARCH_PATH_ENCRYPTED, CREDENTIAL_SEARCH_PATH_ALL)) { - r = strv_extend(&l, params->received_encrypted_credentials_directory); + r = strv_extend(&l, context->received_encrypted_credentials_directory); if (r < 0) return r; _cleanup_strv_free_ char **add = NULL; - r = credential_store_path_encrypted(params->runtime_scope, &add); + r = credential_store_path_encrypted(context->scope, &add); if (r < 0) return r; @@ -346,12 +378,12 @@ static int credential_search_path(const ExecParameters *params, CredentialSearch } if (IN_SET(path, CREDENTIAL_SEARCH_PATH_TRUSTED, CREDENTIAL_SEARCH_PATH_ALL)) { - r = strv_extend(&l, params->received_credentials_directory); + r = strv_extend(&l, context->received_credentials_directory); if (r < 0) return r; _cleanup_strv_free_ char **add = NULL; - r = credential_store_path(params->runtime_scope, &add); + r = credential_store_path(context->scope, &add); if (r < 0) return r; @@ -369,23 +401,6 @@ static int credential_search_path(const ExecParameters *params, CredentialSearch return 0; } -struct load_cred_args { - const ExecContext *context; - const ExecParameters *params; - const char *unit; - - bool always_ipc; - - bool encrypted; - - int write_dfd; - uid_t uid; - gid_t gid; - bool ownership_ok; - - uint64_t left; -}; - static int write_credential( int dfd, const char *id, @@ -431,7 +446,7 @@ static int write_credential( } static int maybe_decrypt_and_write_credential( - struct load_cred_args *args, + LoadCredentialArguments *args, const char *id, const char *data, size_t size, @@ -449,7 +464,7 @@ static int maybe_decrypt_and_write_credential( if (args->encrypted) { CredentialFlags flags = 0; /* only allow user creds in user scope */ - switch (args->params->runtime_scope) { + switch (args->context->scope) { case RUNTIME_SCOPE_SYSTEM: /* In system mode talk directly to the TPM – unless we live in a device sandbox @@ -457,7 +472,7 @@ static int maybe_decrypt_and_write_credential( flags |= CREDENTIAL_ANY_SCOPE; - if (!args->always_ipc) { + if (!args->context->always_ipc) { r = decrypt_credential_and_warn( id, now(CLOCK_REALTIME), @@ -506,7 +521,7 @@ static int maybe_decrypt_and_write_credential( if (add > args->left) return -E2BIG; - r = write_credential(args->write_dfd, id, data, size, args->uid, args->gid, args->ownership_ok); + r = write_credential(args->write_dfd, id, data, size, args->context->uid, args->context->gid, args->ownership_ok); if (r < 0) return log_debug_errno(r, "Failed to write credential '%s': %m", id); @@ -516,7 +531,7 @@ static int maybe_decrypt_and_write_credential( } static int load_credential_glob( - struct load_cred_args *args, + LoadCredentialArguments *args, const ExecImportCredential *ic, char * const *search_path, ReadFullFileFlags flags) { @@ -595,7 +610,7 @@ static int load_credential_glob( } static int load_credential( - struct load_cred_args *args, + LoadCredentialArguments *args, const char *id, int read_dfd, const char *path) { @@ -611,9 +626,8 @@ static int load_credential( assert(args); assert(args->context); - assert(args->params); - assert(args->unit); - assert(args->write_dfd >= 0); + assert(args->context->exec_context); + assert(args->context->unit); assert(id); assert(read_dfd >= 0 || read_dfd == AT_FDCWD); assert(path); @@ -641,7 +655,7 @@ static int load_credential( /* Pass some minimal info about the unit and the credential name we are looking to acquire * via the source socket address in case we read off an AF_UNIX socket. */ - if (asprintf(&bindname, "@%" PRIx64 "/unit/%s/%s", random_u64(), args->unit, id) < 0) + if (asprintf(&bindname, "@%" PRIx64 "/unit/%s/%s", random_u64(), args->context->unit, id) < 0) return -ENOMEM; missing_ok = false; @@ -652,7 +666,7 @@ static int load_credential( * directory we received ourselves. We don't support the AF_UNIX stuff in this mode, since we * are operating on a credential store, i.e. this is guaranteed to be regular files. */ - r = credential_search_path(args->params, CREDENTIAL_SEARCH_PATH_ALL, &search_path); + r = credential_search_path(args->context, CREDENTIAL_SEARCH_PATH_ALL, &search_path); if (r < 0) return r; @@ -696,7 +710,7 @@ static int load_credential( assert_not_reached(); if (r == -ENOENT) { - bool in_set_credentials = hashmap_contains(args->context->set_credentials, id); + bool in_set_credentials = hashmap_contains(args->context->exec_context->set_credentials, id); if (missing_ok || in_set_credentials) { /* Make a missing inherited credential non-fatal, let's just continue. After all apps * will get clear errors if we don't pass such a missing credential on as they @@ -725,7 +739,7 @@ static int load_cred_recurse_dir_cb( const struct statx *sx, void *userdata) { - struct load_cred_args *args = ASSERT_PTR(userdata); + LoadCredentialArguments *args = ASSERT_PTR(userdata); _cleanup_free_ char *sub_id = NULL; int r; @@ -782,38 +796,27 @@ static bool device_nodes_restricted( } static int acquire_credentials( - const ExecContext *context, - const CGroupContext *cgroup_context, - const ExecParameters *params, - const char *unit, + const SetupCredentialsContext *context, int dfd, - uid_t uid, - gid_t gid, bool ownership_ok) { int r; assert(context); - assert(cgroup_context); - assert(params); - assert(unit); + assert(context->exec_context); assert(dfd >= 0); - struct load_cred_args args = { + LoadCredentialArguments args = { .context = context, - .params = params, - .unit = unit, - .always_ipc = device_nodes_restricted(context, cgroup_context), + .write_dfd = dfd, - .uid = uid, - .gid = gid, .ownership_ok = ownership_ok, .left = CREDENTIALS_TOTAL_SIZE_MAX, }; /* First, load credentials off disk (or acquire via AF_UNIX socket) */ ExecLoadCredential *lc; - HASHMAP_FOREACH(lc, context->load_credentials) { + HASHMAP_FOREACH(lc, context->exec_context->load_credentials) { _cleanup_close_ int sub_fd = -EBADF; args.encrypted = lc->encrypted; @@ -852,10 +855,10 @@ static int acquire_credentials( /* Next, look for system credentials and credentials in the credentials store. Note that these do not * override any credentials found earlier. */ ExecImportCredential *ic; - ORDERED_SET_FOREACH(ic, context->import_credentials) { + ORDERED_SET_FOREACH(ic, context->exec_context->import_credentials) { _cleanup_free_ char **search_path = NULL; - r = credential_search_path(params, CREDENTIAL_SEARCH_PATH_TRUSTED, &search_path); + r = credential_search_path(context, CREDENTIAL_SEARCH_PATH_TRUSTED, &search_path); if (r < 0) return r; @@ -871,7 +874,7 @@ static int acquire_credentials( search_path = strv_free(search_path); - r = credential_search_path(params, CREDENTIAL_SEARCH_PATH_ENCRYPTED, &search_path); + r = credential_search_path(context, CREDENTIAL_SEARCH_PATH_ENCRYPTED, &search_path); if (r < 0) return r; @@ -889,7 +892,7 @@ static int acquire_credentials( /* Finally, we add in literally specified credentials. If the credentials already exist, we'll not * add them, so that they can act as a "default" if the same credential is specified multiple times. */ ExecSetCredential *sc; - HASHMAP_FOREACH(sc, context->set_credentials) { + HASHMAP_FOREACH(sc, context->exec_context->set_credentials) { args.encrypted = sc->encrypted; if (faccessat(dfd, sc->id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) { @@ -937,13 +940,8 @@ static int credentials_dir_finalize_permissions(int dfd, uid_t uid, gid_t gid, b } static int setup_credentials_plain_dir( - const ExecContext *context, - const CGroupContext *cgroup_context, - const ExecParameters *params, - const char *unit, - const char *cred_dir, - uid_t uid, - gid_t gid) { + const SetupCredentialsContext *context, + const char *cred_dir) { _cleanup_free_ char *t = NULL, *workspace = NULL; _cleanup_(rm_rf_safep) const char *workspace_rm = NULL; @@ -951,13 +949,13 @@ static int setup_credentials_plain_dir( int r; assert(context); - assert(params); - assert(unit); + assert(context->unit); + assert(context->runtime_prefix); assert(cred_dir); /* Temporary workspace, that remains inaccessible all the time. We prepare stuff there before moving * it into place, so that users can't access half-initialized credential stores. */ - t = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "systemd/temporary-credentials"); + t = path_join(context->runtime_prefix, "systemd/temporary-credentials"); if (!t) return -ENOMEM; @@ -965,7 +963,7 @@ static int setup_credentials_plain_dir( if (r < 0 && r != -EEXIST) return r; - workspace = path_join(t, unit); + workspace = path_join(t, context->unit); if (!workspace) return -ENOMEM; @@ -976,7 +974,7 @@ static int setup_credentials_plain_dir( (void) label_fix_full(dfd, /* inode_path= */ NULL, cred_dir, /* flags= */ 0); - r = acquire_credentials(context, cgroup_context, params, unit, dfd, uid, gid, /* ownership_ok= */ false); + r = acquire_credentials(context, dfd, /* ownership_ok= */ false); if (r < 0) return r; @@ -998,7 +996,7 @@ static int setup_credentials_plain_dir( /* rename() requires both the source and target to be writable, hence lock down write permission * as last step. */ - r = credentials_dir_finalize_permissions(dfd, uid, gid, /* ownership_ok= */ false); + r = credentials_dir_finalize_permissions(dfd, context->uid, context->gid, /* ownership_ok= */ false); if (r < 0) return log_debug_errno(r, "Failed to adjust ACLs of credentials dir: %m"); @@ -1006,21 +1004,15 @@ static int setup_credentials_plain_dir( } static int setup_credentials_internal( - const ExecContext *context, - const CGroupContext *cgroup_context, - const ExecParameters *params, - const char *unit, - const char *cred_dir, - uid_t uid, - gid_t gid) { + const SetupCredentialsContext *context, + bool may_reuse, + const char *cred_dir) { _cleanup_close_ int fs_fd = -EBADF, mfd = -EBADF, dfd = -EBADF; bool dir_mounted; int r; assert(context); - assert(params); - assert(unit); assert(cred_dir); r = path_is_mount_point(cred_dir); @@ -1028,7 +1020,7 @@ static int setup_credentials_internal( return log_debug_errno(r, "Failed to determine if '%s' is a mountpoint: %m", cred_dir); dir_mounted = r > 0; - if (!FLAGS_SET(params->flags, EXEC_SETUP_CREDENTIALS_FRESH)) { + if (may_reuse) { bool populated; /* If the cred dir is a mount, let's treat it as populated, and only look at the contents @@ -1044,7 +1036,7 @@ static int setup_credentials_internal( populated = r == 0; } if (populated) { - log_debug("Credential dir for unit '%s' already set up, skipping.", unit); + log_debug("Credential dir for unit '%s' already set up, skipping.", context->unit); return 0; } } @@ -1052,7 +1044,7 @@ static int setup_credentials_internal( mfd = fsmount_credentials_fs(&fs_fd); if (ERRNO_IS_NEG_PRIVILEGE(mfd) && !dir_mounted) { log_debug_errno(mfd, "Lacking privilege to mount credentials fs, falling back to plain directory."); - return setup_credentials_plain_dir(context, cgroup_context, params, unit, cred_dir, uid, gid); + return setup_credentials_plain_dir(context, cred_dir); } if (mfd < 0) return log_debug_errno(mfd, "Failed to mount credentials fs: %m"); @@ -1063,11 +1055,11 @@ static int setup_credentials_internal( (void) label_fix_full(dfd, /* inode_path= */ NULL, cred_dir, /* flags= */ 0); - r = acquire_credentials(context, cgroup_context, params, unit, dfd, uid, gid, /* ownership_ok= */ true); + r = acquire_credentials(context, dfd, /* ownership_ok= */ true); if (r < 0) return r; - r = credentials_dir_finalize_permissions(dfd, uid, gid, /* ownership_ok= */ true); + r = credentials_dir_finalize_permissions(dfd, context->uid, context->gid, /* ownership_ok= */ true); if (r < 0) return log_debug_errno(r, "Failed to adjust ACLs of credentials dir: %m"); @@ -1112,7 +1104,6 @@ int exec_setup_credentials( const ExecContext *context, const CGroupContext *cgroup_context, const ExecParameters *params, - const char *unit, uid_t uid, gid_t gid) { @@ -1121,7 +1112,6 @@ int exec_setup_credentials( assert(context); assert(params); - assert(unit); if (!exec_params_need_credentials(params) || !exec_context_has_credentials(context)) return 0; @@ -1139,7 +1129,7 @@ int exec_setup_credentials( if (r < 0 && r != -EEXIST) return r; - p = path_join(q, unit); + p = path_join(q, params->unit_id); if (!p) return -ENOMEM; @@ -1147,7 +1137,22 @@ int exec_setup_credentials( if (r < 0 && r != -EEXIST) return r; - r = setup_credentials_internal(context, cgroup_context, params, unit, p, uid, gid); + SetupCredentialsContext ctx = { + .scope = params->runtime_scope, + .exec_context = context, + .unit = params->unit_id, + + .runtime_prefix = params->prefix[EXEC_DIRECTORY_RUNTIME], + .received_credentials_directory = params->received_credentials_directory, + .received_encrypted_credentials_directory = params->received_encrypted_credentials_directory, + + .always_ipc = device_nodes_restricted(context, cgroup_context), + + .uid = uid, + .gid = gid, + }; + + r = setup_credentials_internal(&ctx, /* may_reuse = */ !FLAGS_SET(params->flags, EXEC_SETUP_CREDENTIALS_FRESH), p); if (r < 0) (void) rmdir(p); diff --git a/src/core/exec-credential.h b/src/core/exec-credential.h index 056e5173588..e7e60a9e024 100644 --- a/src/core/exec-credential.h +++ b/src/core/exec-credential.h @@ -57,7 +57,6 @@ int exec_setup_credentials( const ExecContext *context, const CGroupContext *cgroup_context, const ExecParameters *params, - const char *unit, uid_t uid, gid_t gid); diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index a0c101a662d..a0b0a87a659 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -5705,7 +5705,7 @@ int exec_invoke( return log_error_errno(r, "Failed to set up special execution directory in %s: %m", params->prefix[dt]); } - r = exec_setup_credentials(context, cgroup_context, params, params->unit_id, uid, gid); + r = exec_setup_credentials(context, cgroup_context, params, uid, gid); if (r < 0) { *exit_status = EXIT_CREDENTIALS; return log_error_errno(r, "Failed to set up credentials: %m"); From 1a1c5ab758eca2d94130a7a4d1ba2bfa424f706e Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 17 Dec 2025 12:32:14 +0100 Subject: [PATCH 07/17] namespace-util: introduce namespace_enter_delegated() Typically when entering a namespace the userns is handled last, because we assume our process is more privileged than the userns. However, that assumption no longer holds for user managers, which have no privilege over initial userns and all other namespaces are actually owned by the userns unshared first (in executor). Hence, let's add another flavor namespace_enter_delegated() to accommodate that use case. --- src/basic/namespace-util.c | 65 ++++++++++++++++++++++++++++++++++++++ src/basic/namespace-util.h | 1 + 2 files changed, 66 insertions(+) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index c3b84ea7840..69bd4945dfb 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -262,6 +262,71 @@ int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int return 0; } +static int namespace_enter_one_idempotent(int nsfd, NamespaceType type) { + int r; + + /* Join a namespace, but only if we're not part of it already. This is important if we don't necessarily + * own the namespace in question, as kernel would unconditionally return EPERM otherwise. */ + + assert(nsfd >= 0); + assert(type >= 0 && type < _NAMESPACE_TYPE_MAX); + + r = is_our_namespace(nsfd, type); + if (r < 0) + return r; + if (r > 0) + return 0; + + if (setns(nsfd, namespace_info[type].clone_flag) < 0) + return -errno; + + return 1; +} + +int namespace_enter_delegated(int userns_fd, int pidns_fd, int mntns_fd, int netns_fd, int root_fd) { + int r; + + /* Similar to namespace_enter(), but operates on a set of namespaces that are potentially owned + * by the userns ("delegated"), in which case we'll need to gain CAP_SYS_ADMIN by joining + * the userns first, and the rest later. */ + + assert(userns_fd >= 0); + + /* Block dlopen() now, to avoid us inadvertently loading shared library from another namespace */ + block_dlopen(); + + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return -errno; + + if (pidns_fd >= 0) { + r = namespace_enter_one_idempotent(pidns_fd, NAMESPACE_PID); + if (r < 0) + return r; + } + + if (mntns_fd >= 0) { + r = namespace_enter_one_idempotent(mntns_fd, NAMESPACE_MOUNT); + if (r < 0) + return r; + } + + if (netns_fd >= 0) { + r = namespace_enter_one_idempotent(netns_fd, NAMESPACE_NET); + if (r < 0) + return r; + } + + if (root_fd >= 0) { + if (fchdir(root_fd) < 0) + return -errno; + + if (chroot(".") < 0) + return -errno; + } + + return maybe_setgroups(/* size = */ 0, NULL); +} + int fd_is_namespace(int fd, NamespaceType type) { int r; diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 32e64fa9e68..994125818f9 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -47,6 +47,7 @@ int namespace_open( int *ret_root_fd); int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int root_fd); +int namespace_enter_delegated(int userns_fd, int pidns_fd, int mntns_fd, int netns_fd, int root_fd); int fd_is_namespace(int fd, NamespaceType type); int is_our_namespace(int fd, NamespaceType type); From 8f89ed439d1e5ccc0b6b4d51f347a92a1485be81 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Mon, 15 Dec 2025 19:46:59 +0100 Subject: [PATCH 08/17] process-util: teach namespace_fork() to optionally use namespace_enter_delegated() --- src/basic/process-util.c | 6 +++++- src/basic/process-util.h | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 9c44193b67c..52af6a01c8a 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -1778,6 +1778,7 @@ int namespace_fork_full( int netns_fd, int userns_fd, int root_fd, + bool delegated, PidRef *ret) { _cleanup_(pidref_done_sigkill_wait) PidRef pidref_outer = PIDREF_NULL; @@ -1823,7 +1824,10 @@ int namespace_fork_full( errno_pipe_fd[0] = safe_close(errno_pipe_fd[0]); - r = namespace_enter(pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd); + if (delegated) + r = namespace_enter_delegated(userns_fd, pidns_fd, mntns_fd, netns_fd, root_fd); + else + r = namespace_enter(pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd); if (r < 0) { log_full_errno(prio, r, "Failed to join namespace: %m"); report_errno_and_exit(errno_pipe_fd[1], r); diff --git a/src/basic/process-util.h b/src/basic/process-util.h index 46a5612048f..66bb194bac0 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -201,6 +201,7 @@ int namespace_fork_full( int netns_fd, int userns_fd, int root_fd, + bool delegated, PidRef *ret); static inline int namespace_fork( @@ -215,7 +216,7 @@ static inline int namespace_fork( PidRef *ret) { return namespace_fork_full(outer_name, inner_name, NULL, 0, flags, - pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, + pidns_fd, mntns_fd, netns_fd, userns_fd, root_fd, false, ret); } From 65ea71ead72a456d7ede35e38f812c44f6d6b3f0 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 14 Dec 2025 14:31:30 +0100 Subject: [PATCH 09/17] core/exec-credential: introduce unit_refresh_credentials() --- src/core/exec-credential.c | 190 +++++++++++++++++++++++++++++++++++++ src/core/exec-credential.h | 2 + 2 files changed, 192 insertions(+) diff --git a/src/core/exec-credential.c b/src/core/exec-credential.c index 3766096a416..07d83ac7a6a 100644 --- a/src/core/exec-credential.c +++ b/src/core/exec-credential.c @@ -16,19 +16,25 @@ #include "iovec-util.h" #include "label-util.h" #include "log.h" +#include "manager.h" #include "mkdir-label.h" #include "mount-util.h" #include "mountpoint-util.h" +#include "namespace-util.h" #include "ordered-set.h" #include "path-lookup.h" #include "path-util.h" +#include "pidref.h" +#include "process-util.h" #include "random-util.h" #include "recurse-dir.h" #include "rm-rf.h" #include "siphash24.h" +#include "socket-util.h" #include "stat-util.h" #include "string-util.h" #include "strv.h" +#include "unit.h" #include "user-util.h" ExecSetCredential* exec_set_credential_free(ExecSetCredential *sc) { @@ -1158,3 +1164,187 @@ int exec_setup_credentials( return r; } + +static int refresh_credentials_in_namespace_child(int cfd, const char *cred_dir) { + int r; + + assert(cfd >= 0); + assert(cred_dir); + + /* Paranoia: before doing anything, check if the credentials tree inside the mountns is available. + * + * Note that setup_namespace() always installs a mount for cred dir, hence path_is_mount_point() + * is the appropriate check here. */ + r = path_is_mount_point(cred_dir); + if (IN_SET(r, 0, -ENOENT)) { + log_full_errno_zerook(LOG_WARNING, r, + "Credentials tree in the unit mount namespace is masked, skipping refresh."); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to check whether '%s' is a mountpoint in unit mount namespace: %m", + cred_dir); + + /* Inform the parent that we're good to go */ + ssize_t n = write(cfd, &r, sizeof(r)); + if (n < 0) + return log_error_errno(errno, "Failed to write to socket: %m"); + + _cleanup_close_ int mfd = receive_one_fd(cfd, /* flags = */ 0); + if (mfd < 0) + return log_error_errno(mfd, "Failed to receive credentials tree fd from socket: %m"); + + r = mount_exchange_graceful(mfd, cred_dir, /* mount_beneath = */ true); + if (r < 0) + return log_error_errno(r, "Failed to update credentials mount in namespace: %m"); + + return 1; +} + +int unit_refresh_credentials(Unit *u) { + _cleanup_free_ char *cred_dir = NULL; + int r; + + /* Refresh the credentials for a unit, potentially forking off a second process to join the mountns + * if needed. Returns > 0 on successful refresh, == 0 if the credentials tree is masked and the operation + * is skipped. */ + + assert(u); + assert(u->manager); + + r = get_credential_directory(u->manager->prefix[EXEC_DIRECTORY_RUNTIME], u->id, &cred_dir); + if (r < 0) + return log_oom(); + assert(r > 0); + + if (access(cred_dir, F_OK) < 0) { + if (errno == ENOENT) { + log_warning_errno(errno, "Requested to refresh credentials, but credentials aren't populated, skipping."); + return 0; + } + + return log_error_errno(errno, "Failed to check if credentials dir '%s' exists: %m", cred_dir); + } + + _cleanup_close_pair_ int tunnel_fds[2] = EBADF_PAIR; + _cleanup_(pidref_done) PidRef child = PIDREF_NULL; + _cleanup_close_ int userns_fd = -EBADF; + + PidRef *main_pid = unit_main_pid(u); + if (pidref_is_set(main_pid)) { + _cleanup_close_ int mntns_fd = -EBADF, root_fd = -EBADF, pidns_fd = -EBADF; + + r = pidref_namespace_open(main_pid, + &pidns_fd, + &mntns_fd, + /* ret_netns_fd = */ NULL, + MANAGER_IS_USER(u->manager) ? &userns_fd : NULL, + &root_fd); + if (r < 0) + return log_error_errno(r, "Failed to open namespace of unit main process '" PID_FMT "': %m", + main_pid->pid); + + r = is_our_namespace(mntns_fd, NAMESPACE_MOUNT); + if (r < 0) + return log_error_errno(r, "Failed to check if main process resides in a separate mount namespace: %m"); + if (r == 0) { + if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, tunnel_fds) < 0) + return log_error_errno(errno, "Failed to allocate socket pair: %m"); + + r = namespace_fork_full("(sd-creds-ns)", "(sd-creds-ns-inner)", + (int[]) { tunnel_fds[1] }, 1, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_CLOSE_ALL_FDS|FORK_REOPEN_LOG, + pidns_fd, mntns_fd, /* netns_fd = */ -EBADF, userns_fd, root_fd, + /* delegated = */ MANAGER_IS_USER(u->manager), + &child); + if (r < 0) + return log_full_errno(ERRNO_IS_NEG_PRIVILEGE(r) ? LOG_WARNING : LOG_ERR, r, + "Failed to fork off process into unit namespace to refresh credentials: %m"); + if (r == 0) { + r = refresh_credentials_in_namespace_child(tunnel_fds[1], cred_dir); + report_errno_and_exit(tunnel_fds[1], r); + } + + tunnel_fds[1] = safe_close(tunnel_fds[1]); + + /* Wait for the child to validate the creds tree in the unit namespace is populated. */ + ssize_t n = read(tunnel_fds[0], &r, sizeof(r)); + if (n < 0) + return log_error_errno(errno, "Failed to read from socket: %m"); + if (!IN_SET(n, 0, sizeof(r))) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Received unexpected amount of bytes (%zi) while reading errno.", n); + if (n == 0 || r == 0) { + /* The child exited without sending anything or 0 is received signifying + * the credentials are masked? Check the exit status to be sure. */ + r = pidref_wait_for_terminate_and_check("(sd-creds-ns)", &child, WAIT_LOG); + if (r < 0) + return r; + if (r != EXIT_SUCCESS) + return -EPROTO; + + return 0; /* skipped */ + } + if (r < 0) + return r; + + /* Yay! Got > 0 from child indicating all good, proceed with refreshing. */ + } + } + + SetupCredentialsContext ctx = { + .scope = u->manager->runtime_scope, + .exec_context = ASSERT_PTR(unit_get_exec_context(u)), + .unit = u->id, + + .runtime_prefix = u->manager->prefix[EXEC_DIRECTORY_RUNTIME], + .received_credentials_directory = u->manager->received_credentials_directory, + .received_encrypted_credentials_directory = u->manager->received_encrypted_credentials_directory, + + .always_ipc = false, /* we don't migrate to unit cgroup, hence cannot be restricted by cgroup bpf */ + + .uid = u->ref_uid, + .gid = u->ref_gid, + }; + + r = setup_credentials_internal(&ctx, /* may_reuse = */ false, cred_dir); + if (r < 0) + return log_error_errno(r, "Failed to set up credentials: %m"); + + /* The main process doesn't run in a mountns hence nothing got forked off? Then we're all set. */ + if (!pidref_is_set(&child)) + return 1; + + if (userns_fd >= 0) { + assert(MANAGER_IS_USER(u->manager)); + + /* Enter the unit userns now and unshare mountns, so that we have permissions to clone + * the mount tree using open_tree() */ + + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return log_error_errno(errno, "Failed to enter user namespace: %m"); + + if (unshare(CLONE_NEWNS) < 0) + return log_error_errno(errno, "Failed to unshare mount namespace: %m"); + } + + _cleanup_close_ int tfd = open_tree(AT_FDCWD, cred_dir, OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW); + if (tfd < 0) + return log_error_errno(errno, "Failed to clone mount tree at '%s': %m", cred_dir); + + r = send_one_fd(tunnel_fds[0], tfd, /* flags = */ 0); + if (r < 0) + return log_error_errno(r, "Failed to send mount fd to child: %m"); + + r = pidref_wait_for_terminate_and_check("(sd-creds-ns)", &child, WAIT_LOG_ABNORMAL); + if (r < 0) + return r; + if (r != EXIT_SUCCESS) { + r = read_errno(tunnel_fds[0]); + if (r < 0) + return r; + + return -EPROTO; + } + + return 1; +} diff --git a/src/core/exec-credential.h b/src/core/exec-credential.h index e7e60a9e024..89fd12e5bd5 100644 --- a/src/core/exec-credential.h +++ b/src/core/exec-credential.h @@ -60,4 +60,6 @@ int exec_setup_credentials( uid_t uid, gid_t gid); +int unit_refresh_credentials(Unit *u); + bool mount_point_is_credentials(const char *runtime_prefix, const char *path); From a43bc1aed81211af566aa0eb338dbecca2d99824 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 14 Dec 2025 18:20:36 +0100 Subject: [PATCH 10/17] core/service: introduce RefreshOnReload= setting This allows controlling resources to be refreshed before performing reload, with one extra benefit that in the future we can permit "seemless reload"s, i.e. no active signaling is done to the main process after refreshing get updated. This could come in handy for programs that loads stuff on demand or watches changes via inotify. --- src/core/dbus-service.c | 66 +++++++++++++ src/core/load-fragment-gperf.gperf.in | 1 + src/core/load-fragment.c | 49 +++++++++ src/core/load-fragment.h | 1 + src/core/service.c | 137 ++++++++++++++++++++++---- src/core/service.h | 15 +++ src/shared/bus-unit-util.c | 51 ++++++++++ 7 files changed, 303 insertions(+), 17 deletions(-) diff --git a/src/core/dbus-service.c b/src/core/dbus-service.c index f95b5259ff9..9b1b1f77218 100644 --- a/src/core/dbus-service.c +++ b/src/core/dbus-service.c @@ -30,6 +30,7 @@ #include "signal-util.h" #include "stat-util.h" #include "string-util.h" +#include "strv.h" #include "unit.h" static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_type, service_type, ServiceType); @@ -100,6 +101,29 @@ static int property_get_extra_file_descriptors( return sd_bus_message_close_container(reply); } +static int property_get_refresh_on_reload( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *reterr_error) { + + Service *s = ASSERT_PTR(userdata); + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(bus); + assert(reply); + + r = service_refresh_on_reload_to_strv(s->refresh_on_reload_flags, &l); + if (r < 0) + return r; + + return sd_bus_message_append_strv(reply, l); +} + static int property_get_exit_status_set( sd_bus *bus, const char *path, @@ -373,6 +397,7 @@ const sd_bus_vtable bus_service_vtable[] = { SD_BUS_PROPERTY("OpenFile", "a(sst)", property_get_open_files, offsetof(Service, open_files), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ExtraFileDescriptorNames", "as", property_get_extra_file_descriptors, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ReloadSignal", "i", bus_property_get_int, offsetof(Service, reload_signal), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("RefreshOnReload", "as", property_get_refresh_on_reload, 0, SD_BUS_VTABLE_PROPERTY_CONST), BUS_EXEC_STATUS_VTABLE("ExecMain", offsetof(Service, main_exec_status), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), BUS_EXEC_COMMAND_LIST_VTABLE("ExecCondition", offsetof(Service, exec_command[SERVICE_EXEC_CONDITION]), SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION), @@ -840,6 +865,47 @@ static int bus_service_set_transient_property( return 1; } + if (streq(name, "RefreshOnReload")) { + const char *t; + int invert; + + r = sd_bus_message_enter_container(message, 'a', "(bs)"); + if (r < 0) + return r; + + while ((r = sd_bus_message_read(message, "(bs)", &invert, &t)) > 0) { + ServiceRefreshOnReload f; + + f = service_refresh_on_reload_flag_from_string(t); + if (f < 0) + return sd_bus_error_setf(reterr_error, SD_BUS_ERROR_INVALID_ARGS, "Invalid RefreshOnReload= value: %s", t); + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + if (!s->refresh_on_reload_set) + s->refresh_on_reload_flags = invert ? (SERVICE_REFRESH_ON_RELOAD_DEFAULT & ~f) : f; + else + SET_FLAG(s->refresh_on_reload_flags, f, !invert); + + s->refresh_on_reload_set = true; + unit_write_settingf(u, flags, name, "%s=%s%s", name, invert ? "~" : "", t); + } + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!UNIT_WRITE_FLAGS_NOOP(flags) && !s->refresh_on_reload_set) { /* empty array? */ + s->refresh_on_reload_flags = 0; + s->refresh_on_reload_set = true; + unit_write_settingf(u, flags, name, "%s=no", name); + } + + return 1; + } + return 0; } diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 7f8198ff487..5605445c34e 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -479,6 +479,7 @@ Service.USBFunctionStrings, config_parse_unit_path_printf, Service.OOMPolicy, config_parse_oom_policy, 0, offsetof(Service, oom_policy) Service.OpenFile, config_parse_open_file, 0, offsetof(Service, open_files) Service.ReloadSignal, config_parse_signal, 0, offsetof(Service, reload_signal) +Service.RefreshOnReload, config_parse_service_refresh_on_reload, 0, 0 {{ EXEC_CONTEXT_CONFIG_ITEMS('Service') }} {{ CGROUP_CONTEXT_CONFIG_ITEMS('Service') }} {{ KILL_CONTEXT_CONFIG_ITEMS('Service') }} diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index ce90b08e562..d2bfd20fd43 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -4816,6 +4816,55 @@ int config_parse_import_credential( return 0; } +int config_parse_service_refresh_on_reload( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Service *s = ASSERT_PTR(userdata); + int r; + + if (isempty(rvalue)) { + s->refresh_on_reload_set = false; + return 0; + } + + r = parse_boolean(rvalue); + if (r >= 0) { + s->refresh_on_reload_flags = r > 0 ? _SERVICE_REFRESH_ON_RELOAD_ALL : 0; + s->refresh_on_reload_set = true; + return 0; + } + + ServiceRefreshOnReload f; + bool invert = false; + + if (rvalue[0] == '~') { + invert = true; + rvalue++; + } + + r = service_refresh_on_reload_from_string_many(rvalue, &f); + if (r < 0) + return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue); + + /* If the first entry is negated, mask off from default; otherwise assign "positive" values directly */ + if (!s->refresh_on_reload_set) + s->refresh_on_reload_flags = invert ? (SERVICE_REFRESH_ON_RELOAD_DEFAULT & ~f) : f; + else + SET_FLAG(s->refresh_on_reload_flags, f, !invert); + + s->refresh_on_reload_set = true; + return 0; +} + int config_parse_set_status( const char *unit, const char *filename, diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index 3b0ee5fb30c..4677564904c 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -105,6 +105,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_exec_quota); CONFIG_PARSER_PROTOTYPE(config_parse_set_credential); CONFIG_PARSER_PROTOTYPE(config_parse_load_credential); CONFIG_PARSER_PROTOTYPE(config_parse_import_credential); +CONFIG_PARSER_PROTOTYPE(config_parse_service_refresh_on_reload); CONFIG_PARSER_PROTOTYPE(config_parse_set_status); CONFIG_PARSER_PROTOTYPE(config_parse_namespace_path_strv); CONFIG_PARSER_PROTOTYPE(config_parse_temporary_filesystems); diff --git a/src/core/service.c b/src/core/service.c index ec3fd2ed8c3..77cc254f597 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -134,6 +134,8 @@ static void service_enter_signal(Service *s, ServiceState state, ServiceResult f static void service_reload_finish(Service *s, ServiceResult f); static void service_enter_reload_by_notify(Service *s); +static bool service_can_reload_extensions(Service *s, bool warn); + static bool SERVICE_STATE_WITH_MAIN_PROCESS(ServiceState state) { return IN_SET(state, SERVICE_START, SERVICE_START_POST, @@ -754,6 +756,11 @@ static int service_verify(Service *s) { s->restart_usec = s->restart_max_delay_usec; } + if (s->refresh_on_reload_set && s->refresh_on_reload_flags != _SERVICE_REFRESH_ON_RELOAD_ALL) { + if (FLAGS_SET(s->refresh_on_reload_flags, SERVICE_RELOAD_EXTENSIONS)) + service_can_reload_extensions(s, /* warn = */ true); + } + return 0; } @@ -891,6 +898,11 @@ static int service_add_extras(Service *s) { (IN_SET(s->type, SERVICE_NOTIFY, SERVICE_NOTIFY_RELOAD) || s->watchdog_usec > 0 || s->n_fd_store_max > 0)) s->notify_access = NOTIFY_MAIN; + if (!s->refresh_on_reload_set) { + assert_cc(SERVICE_REFRESH_ON_RELOAD_DEFAULT == SERVICE_RELOAD_EXTENSIONS); + s->refresh_on_reload_flags = service_can_reload_extensions(s, /* warn = */ false) ? SERVICE_RELOAD_EXTENSIONS : 0; + } + /* If no OOM policy was explicitly set, then default to the configure default OOM policy. Except when * delegation is on, in that case it we assume the payload knows better what to do and can process * things in a more focused way. */ @@ -2823,28 +2835,53 @@ static void service_enter_reload(Service *s) { service_enter_reload_signal(s); } -static bool service_should_reload_extensions(Service *s) { - int r; - +static bool service_can_reload_extensions(Service *s, bool warn) { assert(s); - if (!pidref_is_set(&s->main_pid)) { - log_unit_debug(UNIT(s), "Not reloading extensions for service without main PID."); - return false; - } - - r = exec_context_has_vpicked_extensions(&s->exec_context); - if (r < 0) - log_unit_warning_errno(UNIT(s), r, "Failed to determine if service should reload extensions, assuming false: %m"); - if (r == 0) - log_unit_debug(UNIT(s), "Service has no extensions to reload."); - if (r <= 0) - return false; - // TODO: Add support for user services, which can use ExtensionDirectories= + notify-reload. // For now, skip for user services. + + if (exec_context_has_vpicked_extensions(&s->exec_context) <= 0) { + if (warn) + log_unit_warning(UNIT(s), "Service uses RefreshOnReload=extensions, but has no extensions using vpick. Ignoring."); + return false; + } + + if (!s->exec_command[SERVICE_EXEC_START]) { + if (warn) + log_unit_warning(UNIT(s), "Service uses RefreshOnReload=extensions, but has no main process (ExecStart=). Ignoring."); + return false; + } + if (!MANAGER_IS_SYSTEM(UNIT(s)->manager)) { - log_once(LOG_WARNING, "Not reloading extensions for user services."); + if (warn) + log_unit_warning(UNIT(s), "Service uses RefreshOnReload=extensions, which is not supported in user mode. Ignoring."); + return false; + } + + return true; +} + +static bool service_get_effective_reload_extensions(Service *s) { + assert(s); + + if (!FLAGS_SET(s->refresh_on_reload_flags, SERVICE_RELOAD_EXTENSIONS)) + return false; + + if (!service_can_reload_extensions(s, /* warn = */ false)) + return false; + + return true; +} + +static bool service_should_reload_extensions(Service *s) { + assert(s); + + if (!service_get_effective_reload_extensions(s)) + return false; + + if (!pidref_is_set(&s->main_pid)) { + log_unit_debug(UNIT(s), "Not reloading extensions for service without main PID."); return false; } @@ -5888,6 +5925,72 @@ static const char* const service_timeout_failure_mode_table[_SERVICE_TIMEOUT_FAI DEFINE_STRING_TABLE_LOOKUP(service_timeout_failure_mode, ServiceTimeoutFailureMode); +static const struct { + ServiceRefreshOnReload flag; + const char *name; +} service_refresh_on_reload_table[] = { + { SERVICE_RELOAD_EXTENSIONS, "extensions" }, +}; + +ServiceRefreshOnReload service_refresh_on_reload_flag_from_string(const char *s) { + assert(s); + + FOREACH_ELEMENT(i, service_refresh_on_reload_table) + if (streq(s, i->name)) + return i->flag; + + return _SERVICE_REFRESH_ON_RELOAD_INVALID; +} + +int service_refresh_on_reload_from_string_many(const char *s, ServiceRefreshOnReload *ret) { + ServiceRefreshOnReload flags = 0; + int r; + + assert(s); + assert(ret); + + for (;;) { + _cleanup_free_ char *v = NULL; + ServiceRefreshOnReload f; + + r = extract_first_word(&s, &v, NULL, 0); + if (r < 0) + return r; + if (r == 0) + break; + + f = service_refresh_on_reload_flag_from_string(v); + if (f < 0) + return f; + assert(f > 0); + + flags |= f; + } + + *ret = flags; + return 0; +} + +int service_refresh_on_reload_to_strv(ServiceRefreshOnReload flags, char ***ret) { + _cleanup_strv_free_ char **l = NULL; + int r; + + assert(flags >= 0); + assert(ret); + + FOREACH_ELEMENT(i, service_refresh_on_reload_table) { + if (!FLAGS_SET(flags, i->flag)) + continue; + + r = strv_extend(&l, i->name); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(l); + return 0; +} + const UnitVTable service_vtable = { .object_size = sizeof(Service), .exec_context_offset = offsetof(Service, exec_context), diff --git a/src/core/service.h b/src/core/service.h index 167b7891e08..9aeba9916af 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -96,6 +96,14 @@ typedef enum ServiceRestartMode { _SERVICE_RESTART_MODE_INVALID = -EINVAL, } ServiceRestartMode; +typedef enum ServiceRefreshOnReload { + SERVICE_RELOAD_EXTENSIONS = 1 << 0, + _SERVICE_REFRESH_ON_RELOAD_ALL = (1 << 1) - 1, + _SERVICE_REFRESH_ON_RELOAD_INVALID = -EINVAL, +} ServiceRefreshOnReload; + +#define SERVICE_REFRESH_ON_RELOAD_DEFAULT SERVICE_RELOAD_EXTENSIONS + typedef struct ServiceFDStore { Service *service; @@ -237,6 +245,9 @@ typedef struct Service { int reload_signal; usec_t reload_begin_usec; + bool refresh_on_reload_set; + ServiceRefreshOnReload refresh_on_reload_flags; + OOMPolicy oom_policy; char *usb_function_descriptors; @@ -288,6 +299,10 @@ DECLARE_STRING_TABLE_LOOKUP(service_result, ServiceResult); DECLARE_STRING_TABLE_LOOKUP(service_timeout_failure_mode, ServiceTimeoutFailureMode); +ServiceRefreshOnReload service_refresh_on_reload_flag_from_string(const char *s) _pure_; +int service_refresh_on_reload_from_string_many(const char *s, ServiceRefreshOnReload *ret); +int service_refresh_on_reload_to_strv(ServiceRefreshOnReload flags, char ***ret); + DEFINE_CAST(SERVICE, Service); /* Only exported for unit tests */ diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 9a8e31474a3..057a89b8abe 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -1172,6 +1172,56 @@ static int bus_append_import_credential(sd_bus_message *m, const char *field, co return 1; } +static int bus_append_refresh_on_reload(sd_bus_message *m, const char *field, const char *eq) { + int r; + + r = sd_bus_message_open_container(m, 'r', "sv"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_basic(m, 's', field); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'v', "a(bs)"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'a', "(bs)"); + if (r < 0) + return bus_log_create_error(r); + + bool invert = *eq == '~'; + + for (const char *p = eq + invert;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, NULL, 0); + if (r < 0) + return parse_log_error(r, field, eq); + if (r == 0) + break; + + r = sd_bus_message_append(m, "(bs)", invert, word); + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + return 1; +} + static int bus_append_log_extra_fields(sd_bus_message *m, const char *field, const char *eq) { int r; @@ -2665,6 +2715,7 @@ static const BusProperty service_properties[] = { { "SuccessExitStatus", bus_append_exit_status }, { "OpenFile", bus_append_open_file }, { "ReloadSignal", bus_append_signal_from_string }, + { "RefreshOnReload", bus_append_refresh_on_reload }, {} }; From cb5c6dc599170ef9d2a4569e69d337baf072a7f4 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 16 Dec 2025 00:02:01 +0100 Subject: [PATCH 11/17] macro: add 22nd case for IN_SET Dejavu moment ;) --- src/fundamental/macro-fundamental.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/fundamental/macro-fundamental.h b/src/fundamental/macro-fundamental.h index 9eebbfb2a9a..e8757b1fc37 100644 --- a/src/fundamental/macro-fundamental.h +++ b/src/fundamental/macro-fundamental.h @@ -351,10 +351,11 @@ assert_cc(sizeof(long long) == sizeof(intmax_t)); #define CASE_F_19(X, ...) case X: CASE_F_18( __VA_ARGS__) #define CASE_F_20(X, ...) case X: CASE_F_19( __VA_ARGS__) #define CASE_F_21(X, ...) case X: CASE_F_20( __VA_ARGS__) +#define CASE_F_22(X, ...) case X: CASE_F_21( __VA_ARGS__) -#define GET_CASE_F(_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,_13,_14,_15,_16,_17,_18,_19,_20,_21,NAME,...) NAME +#define GET_CASE_F(_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,_13,_14,_15,_16,_17,_18,_19,_20,_21,_22,NAME,...) NAME #define FOR_EACH_MAKE_CASE(...) \ - GET_CASE_F(__VA_ARGS__,CASE_F_21,CASE_F_20,CASE_F_19,CASE_F_18,CASE_F_17,CASE_F_16,CASE_F_15,CASE_F_14,CASE_F_13,CASE_F_12, \ + GET_CASE_F(__VA_ARGS__,CASE_F_22,CASE_F_21,CASE_F_20,CASE_F_19,CASE_F_18,CASE_F_17,CASE_F_16,CASE_F_15,CASE_F_14,CASE_F_13,CASE_F_12, \ CASE_F_11,CASE_F_10,CASE_F_9,CASE_F_8,CASE_F_7,CASE_F_6,CASE_F_5,CASE_F_4,CASE_F_3,CASE_F_2,CASE_F_1) \ (__VA_ARGS__) @@ -364,7 +365,7 @@ assert_cc(sizeof(long long) == sizeof(intmax_t)); /* If the build breaks in the line below, you need to extend the case macros. We use typeof(+x) \ * here to widen the type of x if it is a bit-field as this would otherwise be illegal. */ \ static const typeof(+x) __assert_in_set[] _unused_ = { first, __VA_ARGS__ }; \ - assert_cc(ELEMENTSOF(__assert_in_set) <= 21); \ + assert_cc(ELEMENTSOF(__assert_in_set) <= 22); \ switch (x) { \ FOR_EACH_MAKE_CASE(first, __VA_ARGS__) \ _found = true; \ From fff07be411a39b6aba4deb48710a6b67f5de0013 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 14 Dec 2025 14:46:57 +0100 Subject: [PATCH 12/17] core/service: support credential refreshing on reload Closes #21099 --- src/basic/unit-def.c | 1 + src/basic/unit-def.h | 1 + src/core/manager.c | 12 +++-- src/core/service.c | 108 +++++++++++++++++++++++++++++++++++++++---- src/core/service.h | 3 +- 5 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/basic/unit-def.c b/src/basic/unit-def.c index e8afaf1c3f7..6da61c510d8 100644 --- a/src/basic/unit-def.c +++ b/src/basic/unit-def.c @@ -223,6 +223,7 @@ static const char* const service_state_table[_SERVICE_STATE_MAX] = { [SERVICE_RUNNING] = "running", [SERVICE_EXITED] = "exited", [SERVICE_REFRESH_EXTENSIONS] = "refresh-extensions", + [SERVICE_REFRESH_CREDENTIALS] = "refresh-credentials", [SERVICE_RELOAD] = "reload", [SERVICE_RELOAD_SIGNAL] = "reload-signal", [SERVICE_RELOAD_NOTIFY] = "reload-notify", diff --git a/src/basic/unit-def.h b/src/basic/unit-def.h index 0324dc5a374..6ed4c0cbb7a 100644 --- a/src/basic/unit-def.h +++ b/src/basic/unit-def.h @@ -132,6 +132,7 @@ typedef enum ServiceState { SERVICE_RUNNING, SERVICE_EXITED, /* Nothing is running anymore, but RemainAfterExit is true hence this is OK */ SERVICE_REFRESH_EXTENSIONS, /* Refreshing extensions for a reload request */ + SERVICE_REFRESH_CREDENTIALS, /* ditto, but for credentials */ SERVICE_RELOAD, /* Reloading via ExecReload= */ SERVICE_RELOAD_SIGNAL, /* Reloading via SIGHUP requested */ SERVICE_RELOAD_NOTIFY, /* Waiting for READY=1 after RELOADING=1 notify */ diff --git a/src/core/manager.c b/src/core/manager.c index fec11065652..79fa19d976e 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -1888,13 +1888,15 @@ static bool manager_dbus_is_running(Manager *m, bool deserialized) { u = manager_get_unit(m, SPECIAL_DBUS_SERVICE); if (!u) return false; - if (!IN_SET((deserialized ? SERVICE(u)->deserialized_state : SERVICE(u)->state), + if (!IN_SET(deserialized ? SERVICE(u)->deserialized_state : SERVICE(u)->state, SERVICE_RUNNING, - SERVICE_MOUNTING, - SERVICE_RELOAD, - SERVICE_RELOAD_NOTIFY, SERVICE_REFRESH_EXTENSIONS, - SERVICE_RELOAD_SIGNAL)) + SERVICE_REFRESH_CREDENTIALS, + SERVICE_RELOAD, + SERVICE_RELOAD_SIGNAL, + SERVICE_RELOAD_NOTIFY, + SERVICE_RELOAD_POST, + SERVICE_MOUNTING)) return false; return true; diff --git a/src/core/service.c b/src/core/service.c index 77cc254f597..93253b45f49 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -69,6 +69,7 @@ static const UnitActiveState state_translation_table[_SERVICE_STATE_MAX] = { [SERVICE_RUNNING] = UNIT_ACTIVE, [SERVICE_EXITED] = UNIT_ACTIVE, [SERVICE_REFRESH_EXTENSIONS] = UNIT_REFRESHING, + [SERVICE_REFRESH_CREDENTIALS] = UNIT_REFRESHING, [SERVICE_RELOAD] = UNIT_RELOADING, [SERVICE_RELOAD_SIGNAL] = UNIT_RELOADING, [SERVICE_RELOAD_NOTIFY] = UNIT_RELOADING, @@ -102,6 +103,7 @@ static const UnitActiveState state_translation_table_idle[_SERVICE_STATE_MAX] = [SERVICE_RUNNING] = UNIT_ACTIVE, [SERVICE_EXITED] = UNIT_ACTIVE, [SERVICE_REFRESH_EXTENSIONS] = UNIT_REFRESHING, + [SERVICE_REFRESH_CREDENTIALS] = UNIT_REFRESHING, [SERVICE_RELOAD] = UNIT_RELOADING, [SERVICE_RELOAD_SIGNAL] = UNIT_RELOADING, [SERVICE_RELOAD_NOTIFY] = UNIT_RELOADING, @@ -140,7 +142,8 @@ static bool SERVICE_STATE_WITH_MAIN_PROCESS(ServiceState state) { return IN_SET(state, SERVICE_START, SERVICE_START_POST, SERVICE_RUNNING, - SERVICE_REFRESH_EXTENSIONS, SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, + SERVICE_REFRESH_EXTENSIONS, SERVICE_REFRESH_CREDENTIALS, + SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, SERVICE_MOUNTING, SERVICE_STOP, SERVICE_STOP_WATCHDOG, SERVICE_STOP_SIGTERM, SERVICE_STOP_SIGKILL, SERVICE_STOP_POST, SERVICE_FINAL_WATCHDOG, SERVICE_FINAL_SIGTERM, SERVICE_FINAL_SIGKILL); @@ -150,7 +153,7 @@ static bool SERVICE_STATE_WITH_CONTROL_PROCESS(ServiceState state) { return IN_SET(state, SERVICE_CONDITION, SERVICE_START_PRE, SERVICE_START, SERVICE_START_POST, - SERVICE_REFRESH_EXTENSIONS, SERVICE_RELOAD, SERVICE_RELOAD_POST, + SERVICE_REFRESH_EXTENSIONS, SERVICE_REFRESH_CREDENTIALS, SERVICE_RELOAD, SERVICE_RELOAD_POST, SERVICE_MOUNTING, SERVICE_STOP, SERVICE_STOP_WATCHDOG, SERVICE_STOP_SIGTERM, SERVICE_STOP_SIGKILL, SERVICE_STOP_POST, SERVICE_FINAL_WATCHDOG, SERVICE_FINAL_SIGTERM, SERVICE_FINAL_SIGKILL, @@ -161,7 +164,8 @@ static bool SERVICE_STATE_WITH_WATCHDOG(ServiceState state) { return IN_SET(state, SERVICE_START_POST, SERVICE_RUNNING, - SERVICE_REFRESH_EXTENSIONS, SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, + SERVICE_REFRESH_EXTENSIONS, SERVICE_REFRESH_CREDENTIALS, + SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, SERVICE_MOUNTING); } @@ -759,6 +763,10 @@ static int service_verify(Service *s) { if (s->refresh_on_reload_set && s->refresh_on_reload_flags != _SERVICE_REFRESH_ON_RELOAD_ALL) { if (FLAGS_SET(s->refresh_on_reload_flags, SERVICE_RELOAD_EXTENSIONS)) service_can_reload_extensions(s, /* warn = */ true); + + if (FLAGS_SET(s->refresh_on_reload_flags, SERVICE_RELOAD_CREDENTIALS) && + !exec_context_has_credentials(&s->exec_context)) + log_unit_warning(UNIT(s), "Service has RefreshOnReload=credentials, but no credentials are in use. The credentials tree will be masked which blocks further refreshing. Continuing."); } return 0; @@ -1308,7 +1316,8 @@ static void service_set_state(Service *s, ServiceState state) { if (!IN_SET(state, SERVICE_CONDITION, SERVICE_START_PRE, SERVICE_START, SERVICE_START_POST, SERVICE_RUNNING, - SERVICE_REFRESH_EXTENSIONS, SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, + SERVICE_REFRESH_EXTENSIONS, SERVICE_REFRESH_CREDENTIALS, + SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, SERVICE_MOUNTING, SERVICE_STOP, SERVICE_STOP_WATCHDOG, SERVICE_STOP_SIGTERM, SERVICE_STOP_SIGKILL, SERVICE_STOP_POST, SERVICE_FINAL_WATCHDOG, SERVICE_FINAL_SIGTERM, SERVICE_FINAL_SIGKILL, @@ -1379,6 +1388,7 @@ static usec_t service_coldplug_timeout(Service *s) { case SERVICE_START: case SERVICE_START_POST: case SERVICE_REFRESH_EXTENSIONS: + case SERVICE_REFRESH_CREDENTIALS: case SERVICE_RELOAD: case SERVICE_RELOAD_SIGNAL: case SERVICE_RELOAD_NOTIFY: @@ -2835,6 +2845,67 @@ static void service_enter_reload(Service *s) { service_enter_reload_signal(s); } +static bool service_get_effective_reload_credentials(Service *s) { + assert(s); + + return FLAGS_SET(s->refresh_on_reload_flags, SERVICE_RELOAD_CREDENTIALS) && + exec_context_has_credentials(&s->exec_context); +} + +static void service_enter_refresh_credentials(Service *s) { + _cleanup_(pidref_done) PidRef worker = PIDREF_NULL; + int r; + + assert(s); + + if (!service_get_effective_reload_credentials(s)) + return service_enter_reload(s); + + service_unwatch_control_pid(s); + s->control_command = NULL; + s->control_command_id = _SERVICE_EXEC_COMMAND_INVALID; + + r = service_arm_timer(s, /* relative = */ true, s->timeout_start_usec); + if (r < 0) { + log_unit_warning_errno(UNIT(s), r, "Failed to install timer: %m"); + goto fail; + } + + r = unit_fork_helper_process_full(UNIT(s), "(sd-refresh-creds)", /* into_cgroup = */ false, + FORK_ALLOW_DLOPEN, /* allow loading libacl to avoid doing so in pid1 */ + &worker); + if (r < 0) { + log_unit_error_errno(UNIT(s), r, "Failed to fork process to refresh credentials in unit's namespace: %m"); + goto fail; + } + if (r == 0) { + LOG_CONTEXT_PUSH_UNIT(UNIT(s)); + + r = unit_refresh_credentials(UNIT(s)); + if (ERRNO_IS_NEG_PRIVILEGE(r)) + _exit(EXIT_NOPERMISSION); + if (r < 0) + _exit(EXIT_FAILURE); + if (r == 0) + _exit(EXIT_NOTINSTALLED); + + _exit(EXIT_SUCCESS); + } + + r = unit_watch_pidref(UNIT(s), &worker, /* exclusive = */ true); + if (r < 0) { + log_unit_warning_errno(UNIT(s), r, "Failed to watch credentials refresh helper process: %m"); + goto fail; + } + + s->control_pid = TAKE_PIDREF(worker); + service_set_state(s, SERVICE_REFRESH_CREDENTIALS); + return; + +fail: + service_reload_finish(s, SERVICE_FAILURE_RESOURCES); +} + static bool service_can_reload_extensions(Service *s, bool warn) { assert(s); @@ -2894,9 +2965,9 @@ static void service_enter_refresh_extensions(Service *s) { assert(s); - /* If we don't have extensions to refresh, immediately transition to reload state */ + /* If we don't have extensions to refresh, immediately transition to next state */ if (!service_should_reload_extensions(s)) - return service_enter_reload(s); + return service_enter_refresh_credentials(s); service_unwatch_control_pid(s); s->control_command = NULL; @@ -3161,6 +3232,7 @@ static int service_stop(Unit *u) { service_live_mount_finish(s, SERVICE_FAILURE_PROTOCOL, BUS_ERROR_UNIT_INACTIVE); _fallthrough_; case SERVICE_REFRESH_EXTENSIONS: + case SERVICE_REFRESH_CREDENTIALS: service_kill_control_process(s); _fallthrough_; case SERVICE_CONDITION: @@ -4225,6 +4297,7 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) { case SERVICE_START_POST: case SERVICE_REFRESH_EXTENSIONS: + case SERVICE_REFRESH_CREDENTIALS: case SERVICE_RELOAD: case SERVICE_RELOAD_SIGNAL: case SERVICE_RELOAD_NOTIFY: @@ -4344,7 +4417,7 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) { success, code, status); - if (!IN_SET(s->state, SERVICE_REFRESH_EXTENSIONS, SERVICE_RELOAD, SERVICE_RELOAD_POST, SERVICE_MOUNTING) && + if (!IN_SET(s->state, SERVICE_REFRESH_EXTENSIONS, SERVICE_REFRESH_CREDENTIALS, SERVICE_RELOAD, SERVICE_RELOAD_POST, SERVICE_MOUNTING) && s->result == SERVICE_SUCCESS) s->result = f; @@ -4434,7 +4507,15 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) { case SERVICE_REFRESH_EXTENSIONS: if (f == SERVICE_SUCCESS) - /* Remounting extensions asynchronously done, proceed to reload */ + service_enter_refresh_credentials(s); + else + service_reload_finish(s, f); + break; + + case SERVICE_REFRESH_CREDENTIALS: + if (f == SERVICE_SUCCESS || + (f == SERVICE_FAILURE_EXIT_CODE && IN_SET(status, EXIT_NOTINSTALLED, EXIT_NOPERMISSION))) + /* Refreshing asynchronously done, proceed to reload */ service_enter_reload(s); else service_reload_finish(s, f); @@ -4552,6 +4633,7 @@ static int service_dispatch_timer(sd_event_source *source, usec_t usec, void *us break; case SERVICE_REFRESH_EXTENSIONS: + case SERVICE_REFRESH_CREDENTIALS: case SERVICE_RELOAD: case SERVICE_RELOAD_SIGNAL: case SERVICE_RELOAD_NOTIFY: @@ -4875,7 +4957,9 @@ static void service_notify_message_process_state(Service *s, char * const *tags) if (strv_contains(tags, "STOPPING=1")) { s->notify_state = NOTIFY_STOPPING; - if (IN_SET(s->state, SERVICE_RUNNING, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_REFRESH_EXTENSIONS)) + if (IN_SET(s->state, SERVICE_RUNNING, + SERVICE_REFRESH_EXTENSIONS, SERVICE_REFRESH_CREDENTIALS, + SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY)) service_enter_stop_by_notify(s); return; @@ -4972,7 +5056,8 @@ static void service_notify_message( r = service_notify_message_parse_new_pid(u, tags, fds, &new_main_pid); if (r > 0 && IN_SET(s->state, SERVICE_START, SERVICE_START_POST, SERVICE_RUNNING, - SERVICE_REFRESH_EXTENSIONS, SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, + SERVICE_REFRESH_EXTENSIONS, SERVICE_REFRESH_CREDENTIALS, + SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, SERVICE_RELOAD_POST, SERVICE_STOP, SERVICE_STOP_SIGTERM) && (!s->main_pid_known || !pidref_equal(&new_main_pid, &s->main_pid))) { @@ -5241,6 +5326,7 @@ static bool pick_up_pid_from_bus_name(Service *s) { SERVICE_START_POST, SERVICE_RUNNING, SERVICE_REFRESH_EXTENSIONS, + SERVICE_REFRESH_CREDENTIALS, SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, @@ -5427,6 +5513,7 @@ static bool service_needs_console(Unit *u) { SERVICE_START_POST, SERVICE_RUNNING, SERVICE_REFRESH_EXTENSIONS, + SERVICE_REFRESH_CREDENTIALS, SERVICE_RELOAD, SERVICE_RELOAD_SIGNAL, SERVICE_RELOAD_NOTIFY, @@ -5930,6 +6017,7 @@ static const struct { const char *name; } service_refresh_on_reload_table[] = { { SERVICE_RELOAD_EXTENSIONS, "extensions" }, + { SERVICE_RELOAD_CREDENTIALS, "credentials" }, }; ServiceRefreshOnReload service_refresh_on_reload_flag_from_string(const char *s) { diff --git a/src/core/service.h b/src/core/service.h index 9aeba9916af..44882a6fd32 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -98,7 +98,8 @@ typedef enum ServiceRestartMode { typedef enum ServiceRefreshOnReload { SERVICE_RELOAD_EXTENSIONS = 1 << 0, - _SERVICE_REFRESH_ON_RELOAD_ALL = (1 << 1) - 1, + SERVICE_RELOAD_CREDENTIALS = 1 << 1, + _SERVICE_REFRESH_ON_RELOAD_ALL = (1 << 2) - 1, _SERVICE_REFRESH_ON_RELOAD_INVALID = -EINVAL, } ServiceRefreshOnReload; From f385b3f3dde55702de8643cd0a5cb1b2bdedffef Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 14 Dec 2025 14:47:20 +0100 Subject: [PATCH 13/17] core/service: pass credentials to ExecReload*= if refreshed Closes #35227 --- src/core/service.c | 66 +++++++++++++++++++++++++++++++++++----------- src/core/service.h | 1 + 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/core/service.c b/src/core/service.c index 93253b45f49..49dd82ebb8c 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -1689,11 +1689,16 @@ static Service *service_get_triggering_service(Service *s) { return NULL; } -static ExecFlags service_exec_flags(ServiceExecCommand command_id, ExecFlags cred_flag) { +static ExecFlags service_exec_flags( + const Service *s, + ServiceExecCommand command_id, + ExecFlags cred_flag) { + /* All service main/control processes honor sandboxing and namespacing options (except those explicitly excluded in service_spawn()) */ ExecFlags flags = EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT; + assert(s); assert(command_id >= 0); assert(command_id < _SERVICE_EXEC_COMMAND_MAX); assert((cred_flag & ~(EXEC_SETUP_CREDENTIALS_FRESH|EXEC_SETUP_CREDENTIALS)) == 0); @@ -1725,6 +1730,12 @@ static ExecFlags service_exec_flags(ServiceExecCommand command_id, ExecFlags cre if (!IN_SET(command_id, SERVICE_EXEC_CONDITION, SERVICE_EXEC_START_PRE)) flags |= EXEC_CONTROL_CGROUP; + /* Pass credentials to ExecReload*= too, but only if the credentials are actually refreshed, + * to make sure they have the same understanding of the world as the main process. */ + if (IN_SET(command_id, SERVICE_EXEC_RELOAD, SERVICE_EXEC_RELOAD_POST) && + FLAGS_SET(s->refreshed_mask, SERVICE_RELOAD_CREDENTIALS)) + flags |= EXEC_SETUP_CREDENTIALS; + if (IN_SET(command_id, SERVICE_EXEC_STOP, SERVICE_EXEC_STOP_POST)) flags |= EXEC_SETENV_RESULT; @@ -2250,7 +2261,7 @@ static void service_enter_stop_post(Service *s, ServiceResult f) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), s->timeout_stop_usec, &s->control_pid); if (r < 0) { @@ -2362,7 +2373,7 @@ static void service_enter_stop(Service *s, ServiceResult f) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), s->timeout_stop_usec, &s->control_pid); if (r < 0) { @@ -2447,7 +2458,7 @@ static void service_enter_start_post(Service *s) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), s->timeout_start_usec, &s->control_pid); if (r < 0) { @@ -2557,7 +2568,7 @@ static void service_enter_start(Service *s) { r = service_spawn(s, c, - service_exec_flags(SERVICE_EXEC_START, EXEC_SETUP_CREDENTIALS_FRESH), + service_exec_flags(s, SERVICE_EXEC_START, EXEC_SETUP_CREDENTIALS_FRESH), timeout, &pidref); if (r < 0) { @@ -2617,7 +2628,7 @@ static void service_enter_start_pre(Service *s) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), s->timeout_start_usec, &s->control_pid); if (r < 0) { @@ -2653,7 +2664,7 @@ static void service_enter_condition(Service *s) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), s->timeout_start_usec, &s->control_pid); if (r < 0) { @@ -2757,7 +2768,7 @@ static void service_enter_reload_post(Service *s) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), s->timeout_start_usec, &s->control_pid); if (r < 0) { @@ -2832,7 +2843,7 @@ static void service_enter_reload(Service *s) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), s->timeout_start_usec, &s->control_pid); if (r < 0) { @@ -3063,7 +3074,7 @@ static void service_run_next_control(Service *s) { r = service_spawn(s, s->control_command, - service_exec_flags(s->control_command_id, /* cred_flag= */ 0), + service_exec_flags(s, s->control_command_id, /* cred_flag = */ 0), timeout, &s->control_pid); if (r < 0) { @@ -3094,7 +3105,7 @@ static void service_run_next_main(Service *s) { r = service_spawn(s, s->main_command, - service_exec_flags(SERVICE_EXEC_START, EXEC_SETUP_CREDENTIALS), + service_exec_flags(s, SERVICE_EXEC_START, EXEC_SETUP_CREDENTIALS), s->timeout_start_usec, &pidref); if (r < 0) { @@ -3275,6 +3286,7 @@ static int service_reload(Unit *u) { assert(IN_SET(s->state, SERVICE_RUNNING, SERVICE_EXITED)); s->reload_result = SERVICE_SUCCESS; + s->refreshed_mask = 0; service_enter_refresh_extensions(s); @@ -3495,6 +3507,21 @@ static int service_serialize(Unit *u, FILE *f, FDSet *fds) { (void) serialize_usec(f, "reload-begin-usec", s->reload_begin_usec); + if (s->refreshed_mask > 0) { + _cleanup_strv_free_ char **l = NULL; + _cleanup_free_ char *t = NULL; + + r = service_refresh_on_reload_to_strv(s->refreshed_mask, &l); + if (r < 0) + return log_oom(); + + t = strv_join(l, " "); + if (!t) + return log_oom(); + + (void) serialize_item(f, "refreshed-mask", t); + } + return 0; } @@ -3884,7 +3911,11 @@ static int service_deserialize_item(Unit *u, const char *key, const char *value, } else if (streq(key, "reload-begin-usec")) (void) deserialize_usec(value, &s->reload_begin_usec); - else + else if (streq(key, "refreshed-mask")) { + r = service_refresh_on_reload_from_string_many(value, &s->refreshed_mask); + if (r < 0) + log_unit_debug_errno(u, r, "Failed to parse refresh-mask value: %s", value); + } else log_unit_debug(u, "Unknown serialization key: %s", key); return 0; @@ -4506,18 +4537,21 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) { break; case SERVICE_REFRESH_EXTENSIONS: - if (f == SERVICE_SUCCESS) + if (f == SERVICE_SUCCESS) { + s->refreshed_mask |= SERVICE_RELOAD_EXTENSIONS; service_enter_refresh_credentials(s); - else + } else service_reload_finish(s, f); break; case SERVICE_REFRESH_CREDENTIALS: if (f == SERVICE_SUCCESS || - (f == SERVICE_FAILURE_EXIT_CODE && IN_SET(status, EXIT_NOTINSTALLED, EXIT_NOPERMISSION))) + (f == SERVICE_FAILURE_EXIT_CODE && IN_SET(status, EXIT_NOTINSTALLED, EXIT_NOPERMISSION))) { + /* Refreshing asynchronously done, proceed to reload */ + s->refreshed_mask |= f == SERVICE_SUCCESS ? SERVICE_RELOAD_CREDENTIALS : 0; service_enter_reload(s); - else + } else service_reload_finish(s, f); break; diff --git a/src/core/service.h b/src/core/service.h index 44882a6fd32..9750b19ce28 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -248,6 +248,7 @@ typedef struct Service { bool refresh_on_reload_set; ServiceRefreshOnReload refresh_on_reload_flags; + ServiceRefreshOnReload refreshed_mask; OOMPolicy oom_policy; From 21292677f61505c4b1615241d193a0b2bd1dc8af Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Sun, 14 Dec 2025 17:55:04 +0100 Subject: [PATCH 14/17] core/service: if RefreshOnReload= is explicitly enabled, allow reload even without exec/notify-reload This was originally brought up by @poettering. If the process loads stuff on demand and flushes them out after each use, or actively monitors file changes, they can be reloaded by merely refreshing the resources. --- src/core/service.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/service.c b/src/core/service.c index 49dd82ebb8c..255e86cfb56 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -3297,7 +3297,10 @@ static bool service_can_reload(Unit *u) { Service *s = ASSERT_PTR(SERVICE(u)); return s->exec_command[SERVICE_EXEC_RELOAD] || - s->type == SERVICE_NOTIFY_RELOAD; + s->type == SERVICE_NOTIFY_RELOAD || + (s->refresh_on_reload_set && + (service_get_effective_reload_extensions(s) || + service_get_effective_reload_credentials(s))); } static unsigned service_exec_command_index(Unit *u, ServiceExecCommand id, const ExecCommand *current) { From 24f458da814659f0d34a0081d9192a8f872ebf4d Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Mon, 15 Dec 2025 23:58:07 +0100 Subject: [PATCH 15/17] man: document RefreshOnReload= --- man/org.freedesktop.systemd1.xml | 11 +++++++++-- man/systemd.exec.xml | 30 ++++++++++++++---------------- man/systemd.service.xml | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index e6c2c6fdab8..fbed12c4be9 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2833,6 +2833,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { readonly as ExtraFileDescriptorNames = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly i ReloadSignal = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly as RefreshOnReload = ['...', ...]; readonly t ExecMainStartTimestamp = ...; readonly t ExecMainStartTimestampMonotonic = ...; readonly t ExecMainExitTimestamp = ...; @@ -3534,6 +3536,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -4180,6 +4184,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -12514,8 +12520,9 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ ManagedOOMKills, ExecReloadPost, and ExecReloadPostEx were added in version 259. - BindNetworkInterface, and - MemoryTHP were added in version 260. + BindNetworkInterface, + MemoryTHP, and + RefreshOnReload were added in version 260. Socket Unit Objects diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 106e1dcdb50..0ba3e011f21 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -572,14 +572,13 @@ To disable the safety check that the extension-release file name matches the image file name, the x-systemd.relax-extension-release-check mount option may be appended. - This option can be used together with a service type and - systemd.v7 - to manage configuration updates. When such a service carrying confext images is reloaded via - systemctl reload foo.service or equivalent D-Bus method, the confext itself will - be reloaded to pick up any changes. This only applies to confext extensions. Note that in case a - service has this configuration enabled at first, and then it is subsequently removed in an update - followed by a daemon-reload operation, reloading the confexts will be a no-op, and a full service - restart is required instead. See + If a service employs this option with + systemd.v7, + and has enabled (the default), the confexts will + be refreshed to pick up any changes on service reload. This only applies to confext extensions. + Note that in case a service has this configuration enabled at first, and then it is subsequently + removed in an update followed by a daemon-reload operation, reloading the confexts will be a no-op, + and a full service restart is required instead. See systemd.service5 also for details. @@ -630,14 +629,13 @@ or the host. See: os-release5. - This option can be used together with a service type and - systemd.v7 - to manage configuration updates. When such a service carrying confext directories is reloaded via - systemctl reload foo.service or equivalent D-Bus method, the confext itself will - be reloaded to pick up any changes. This only applies to confext extensions. Note that in case a - service has this configuration enabled at first, and then it is subsequently removed in an update - followed by a daemon-reload operation, reloading the confexts will be a no-op, and a full service - restart is required instead. See + If a service employs this option with + systemd.v7, + and has enabled (the default), the confexts will + be refreshed to pick up any changes on service reload. This only applies to confext extensions. + Note that in case a service has this configuration enabled at first, and then it is subsequently + removed in an update followed by a daemon-reload operation, reloading the confexts will be a no-op, + and a full service restart is required instead. See systemd.service5 also for details. diff --git a/man/systemd.service.xml b/man/systemd.service.xml index cea9384399d..8b9994a46ab 100644 --- a/man/systemd.service.xml +++ b/man/systemd.service.xml @@ -1349,6 +1349,28 @@ + + RefreshOnReload= + + Takes a boolean argument, or a list of resources defined in + systemd.exec5. + Possible values are and , separated by space. + Prepending the list with a single tilde character (~) inverts the effect. + Defaults to . An empty assignment resets the list to default. If enabled, + the corresponding resources (/ + for and // + (along with their counterparts) + for ) will be refreshed on service reload. If , + all resources listed above that are used by the service shall be refreshed. + + Specially, if this option is set explicitly, and the respective resources are in use, + the service may be reloaded without any actual reload mechanism ( + or ) for notifying the main process, in which case the reload + is considered complete immediately after refreshing. + + + + Check From d83f9cc112b15db21bf6abda67e4f512423ed865 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 10 Feb 2026 07:34:57 +0100 Subject: [PATCH 16/17] test-load-fragment: add unit test for RefreshOnReload= parser --- src/test/test-load-fragment.c | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/test/test-load-fragment.c b/src/test/test-load-fragment.c index 9ddc8a2eba2..892e471d8ab 100644 --- a/src/test/test-load-fragment.c +++ b/src/test/test-load-fragment.c @@ -21,6 +21,7 @@ #include "open-file.h" #include "pcre2-util.h" #include "rm-rf.h" +#include "service.h" #include "set.h" #include "specifier.h" #include "string-util.h" @@ -1033,6 +1034,86 @@ TEST(config_parse_open_file) { ASSERT_NULL(of); } +TEST(config_parse_service_refresh_on_reload) { + ServiceRefreshOnReload flags; + _cleanup_strv_free_ char **l = NULL; + int r; + + r = service_refresh_on_reload_from_string_many("extensions", &flags); + ASSERT_OK(r); + ASSERT_EQ(flags, SERVICE_RELOAD_EXTENSIONS); + ASSERT_OK(service_refresh_on_reload_to_strv(flags, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("extensions"))); + + l = strv_free(l); + + r = service_refresh_on_reload_from_string_many("credentials extensions", &flags); + ASSERT_OK(r); + ASSERT_EQ(flags, SERVICE_RELOAD_EXTENSIONS|SERVICE_RELOAD_CREDENTIALS); + ASSERT_OK(service_refresh_on_reload_to_strv(flags, &l)); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("extensions", "credentials"))); + + ASSERT_ERROR(service_refresh_on_reload_from_string_many("hoge", &flags), EINVAL); + + _cleanup_(manager_freep) Manager *m = NULL; + _cleanup_(unit_freep) Unit *u = NULL; + + r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m); + if (manager_errno_skip_test(r)) { + log_notice_errno(r, "Skipping test: manager_new: %m"); + return; + } + + ASSERT_OK(r); + ASSERT_OK(manager_startup(m, NULL, NULL, NULL)); + + ASSERT_NOT_NULL(u = unit_new(m, sizeof(Service))); + ASSERT_OK_ZERO(unit_add_name(u, "foobar.service")); + + ASSERT_FALSE(SERVICE(u)->refresh_on_reload_set); + + ASSERT_OK(config_parse_service_refresh_on_reload( + NULL, "fake", 1, "section", 1, + "RefreshOnReload", 0, "no", + NULL, u)); + ASSERT_TRUE(SERVICE(u)->refresh_on_reload_set); + ASSERT_EQ(SERVICE(u)->refresh_on_reload_flags, 0); + + ASSERT_OK(config_parse_service_refresh_on_reload( + NULL, "fake", 1, "section", 1, + "RefreshOnReload", 0, "yes", + NULL, u)); + ASSERT_TRUE(SERVICE(u)->refresh_on_reload_set); + ASSERT_EQ(SERVICE(u)->refresh_on_reload_flags, _SERVICE_REFRESH_ON_RELOAD_ALL); + + ASSERT_OK(config_parse_service_refresh_on_reload( + NULL, "fake", 1, "section", 1, + "RefreshOnReload", 0, "~extensions", + NULL, u)); + ASSERT_TRUE(SERVICE(u)->refresh_on_reload_set); + ASSERT_EQ(SERVICE(u)->refresh_on_reload_flags, _SERVICE_REFRESH_ON_RELOAD_ALL & ~SERVICE_RELOAD_EXTENSIONS); + + ASSERT_OK(config_parse_service_refresh_on_reload( + NULL, "fake", 1, "section", 1, + "RefreshOnReload", 0, "~extensions credentials", + NULL, u)); + ASSERT_TRUE(SERVICE(u)->refresh_on_reload_set); + ASSERT_EQ(SERVICE(u)->refresh_on_reload_flags, 0); + + ASSERT_OK(config_parse_service_refresh_on_reload( + NULL, "fake", 1, "section", 1, + "RefreshOnReload", 0, "", + NULL, u)); + ASSERT_FALSE(SERVICE(u)->refresh_on_reload_set); + + ASSERT_OK(config_parse_service_refresh_on_reload( + NULL, "fake", 1, "section", 1, + "RefreshOnReload", 0, "~extensions", + NULL, u)); + ASSERT_TRUE(SERVICE(u)->refresh_on_reload_set); + ASSERT_EQ(SERVICE(u)->refresh_on_reload_flags, SERVICE_REFRESH_ON_RELOAD_DEFAULT & ~SERVICE_RELOAD_EXTENSIONS); +} + static int intro(void) { if (enter_cgroup_subroot(NULL) == -ENOMEDIUM) return log_tests_skipped("cgroupfs not available"); From 7d551b00e3158aa2acbb44dcd8e5d5b82ceca4bb Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 10 Feb 2026 08:13:39 +0100 Subject: [PATCH 17/17] TEST-54-CREDS: add test cases for credential refreshing --- .../TEST-54-CREDS.units/refresh.sh | 29 +++++ test/meson.build | 1 + test/units/TEST-54-CREDS.sh | 102 ++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100755 test/integration-tests/TEST-54-CREDS/TEST-54-CREDS.units/refresh.sh diff --git a/test/integration-tests/TEST-54-CREDS/TEST-54-CREDS.units/refresh.sh b/test/integration-tests/TEST-54-CREDS/TEST-54-CREDS.units/refresh.sh new file mode 100755 index 00000000000..24aa5e88324 --- /dev/null +++ b/test/integration-tests/TEST-54-CREDS/TEST-54-CREDS.units/refresh.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +OUTPUT_FILE="$1" + +dump_creds_tree() { + grep . "$CREDENTIALS_DIRECTORY"/* >"$OUTPUT_FILE" +} + +on_sighup() { + systemd-notify --reloading + dump_creds_tree + systemd-notify --ready +} + +trap on_sighup SIGHUP + +export SYSTEMD_LOG_LEVEL=debug + +dump_creds_tree +systemd-notify --ready + +sleep infinity & +while :; do + wait || : +done diff --git a/test/meson.build b/test/meson.build index 6c7fd666a08..7bf557cc193 100644 --- a/test/meson.build +++ b/test/meson.build @@ -351,6 +351,7 @@ if install_tests 'integration-tests/TEST-30-ONCLOCKCHANGE/TEST-30-ONCLOCKCHANGE.units', 'integration-tests/TEST-38-FREEZER/TEST-38-FREEZER.units', 'integration-tests/TEST-52-HONORFIRSTSHUTDOWN/TEST-52-HONORFIRSTSHUTDOWN.units', + 'integration-tests/TEST-54-CREDS/TEST-54-CREDS.units', 'integration-tests/TEST-55-OOMD/TEST-55-OOMD.units', 'integration-tests/TEST-62-RESTRICT-IFACES/TEST-62-RESTRICT-IFACES.units', 'integration-tests/TEST-63-PATH/TEST-63-PATH.units', diff --git a/test/units/TEST-54-CREDS.sh b/test/units/TEST-54-CREDS.sh index ae16e5b3dfb..0eaf8a2dfb0 100755 --- a/test/units/TEST-54-CREDS.sh +++ b/test/units/TEST-54-CREDS.sh @@ -571,4 +571,106 @@ systemd-run -M testuser@ --user --wait -p ImportCredential=brummbaer \ kill "$PID" +# Now test credential refreshing + +UNIT_NAME="TEST-54-CREDS-refreshing-$RANDOM.service" +OUTPUT_FILE="/tmp/$UNIT_NAME.out" +POST_FLAG_FILE="/tmp/$UNIT_NAME.post-flag" + +cat >/run/systemd/system/"$UNIT_NAME" </run/credstore/test.creds.new-refresh-1 +[[ ! -e /run/credentials/"$UNIT_NAME"/test.creds.new-refresh-1 ]] + +systemctl reload "$UNIT_NAME" +[[ ! -e /run/credentials/"$UNIT_NAME"/test.creds.new-refresh-1 ]] +(! grep -q "test.creds.new-refresh-1" "$OUTPUT_FILE") + +echo "RefreshOnReload=credentials" >>/run/systemd/system/"$UNIT_NAME" +systemctl daemon-reload +systemctl reload "$UNIT_NAME" +diff /run/credstore/test.creds.new-refresh-1 /run/credentials/"$UNIT_NAME"/test.creds.new-refresh-1 +diff "$OUTPUT_FILE" <(grep . /run/credentials/"$UNIT_NAME"/*) + +systemctl stop "$UNIT_NAME" +cat >>/run/systemd/system/"$UNIT_NAME" </run/credstore/test.creds.new-refresh-2 +[[ ! -e /run/credentials/"$UNIT_NAME"/test.creds.new-refresh-2 ]] + +systemctl reload "$UNIT_NAME" +diff /run/credstore/test.creds.new-refresh-2 /run/credentials/"$UNIT_NAME"/test.creds.new-refresh-2 +diff "$OUTPUT_FILE" <(grep . /run/credentials/"$UNIT_NAME"/*) + +echo "3" >/run/credstore/test.creds.new-refresh-3 +[[ ! -e /run/credentials/"$UNIT_NAME"/test.creds.new-refresh-3 ]] + +rm "$OUTPUT_FILE" +systemctl edit --runtime --stdin "$UNIT_NAME" <>/run/systemd/system/"$UNIT_NAME" +systemctl daemon-reload +assert_eq "$(systemctl show "$UNIT_NAME" -P CanReload)" "no" +systemctl revert "$UNIT_NAME" +assert_eq "$(systemctl show "$UNIT_NAME" -P CanReload)" "yes" + +echo "BOGUS" >/run/credstore/test.creds.refresh-bogus +touch "$POST_FLAG_FILE" +systemctl reload "$UNIT_NAME" +diff /run/credstore/test.creds.new-refresh-3 /run/credentials/"$UNIT_NAME"/test.creds.new-refresh-3 +[[ ! -e /run/credentials/"$UNIT_NAME"/test.creds.refresh-bogus ]] +diff "$OUTPUT_FILE" <(grep . /run/credentials/"$UNIT_NAME"/*) +[[ ! -e "$POST_FLAG_FILE" ]] + +OUTPUT_FILE_USER="/tmp/TEST-54-CREDS-refreshing-user.out" + +systemd-notify --fork -- \ + systemd-run -M testuser@ --user --wait \ + --unit=brummbaer-refresh.service \ + --service-type=notify-reload \ + -p NotifyAccess=all \ + -p 'ImportCredential=brummbaer*' \ + -p RefreshOnReload=credentials \ + -p ProtectSystem=strict \ + -p ReadWritePaths=/tmp \ + /usr/lib/systemd/tests/testdata/TEST-54-CREDS.units/refresh.sh "$OUTPUT_FILE_USER" + +[[ -f "$TESTUSER_CRED_DIR/brummbaer-refresh.service/brummbaer" ]] +diff "$OUTPUT_FILE_USER" <(grep . "$TESTUSER_CRED_DIR"/brummbaer-refresh.service/*) + +run0 -u testuser --pipe -i \ + --property=EnvironmentFile=-/usr/lib/systemd/systemd-asan-env \ + 'mkdir -p .config/credstore && echo "refreshed" >.config/credstore/brummbaer.refreshed' + +systemctl -M testuser@ --user reload brummbaer-refresh.service +assert_eq "$(cat "$TESTUSER_CRED_DIR"/brummbaer-refresh.service/brummbaer.refreshed)" "refreshed" +diff "$OUTPUT_FILE_USER" <(grep . "$TESTUSER_CRED_DIR"/brummbaer-refresh.service/*) + touch /testok