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 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); 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); } 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/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/exec-credential.c b/src/core/exec-credential.c index 1971aafee88..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) { @@ -310,6 +316,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 +352,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 +369,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 +384,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 +407,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 +452,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 +470,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 +478,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 +527,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 +537,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 +616,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 +632,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 +661,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 +672,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; @@ -695,17 +715,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->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 + * 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); @@ -722,7 +745,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; @@ -779,38 +802,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; @@ -849,10 +861,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; @@ -868,7 +880,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; @@ -886,7 +898,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) { @@ -934,13 +946,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; @@ -948,13 +955,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; @@ -962,7 +969,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; @@ -973,7 +980,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; @@ -995,7 +1002,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"); @@ -1003,43 +1010,47 @@ 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); - 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 (may_reuse) { + 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.", context->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."); - 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"); @@ -1050,11 +1061,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"); @@ -1099,7 +1110,6 @@ int exec_setup_credentials( const ExecContext *context, const CGroupContext *cgroup_context, const ExecParameters *params, - const char *unit, uid_t uid, gid_t gid) { @@ -1108,7 +1118,6 @@ int exec_setup_credentials( assert(context); assert(params); - assert(unit); if (!exec_params_need_credentials(params) || !exec_context_has_credentials(context)) return 0; @@ -1126,7 +1135,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; @@ -1134,12 +1143,208 @@ 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); - /* 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; } + +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 056e5173588..89fd12e5bd5 100644 --- a/src/core/exec-credential.h +++ b/src/core/exec-credential.h @@ -57,8 +57,9 @@ int exec_setup_credentials( const ExecContext *context, const CGroupContext *cgroup_context, const ExecParameters *params, - const char *unit, uid_t uid, gid_t gid); +int unit_refresh_credentials(Unit *u); + bool mount_point_is_credentials(const char *runtime_prefix, const char *path); 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"); 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/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/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, }; } diff --git a/src/core/service.c b/src/core/service.c index ec3fd2ed8c3..255e86cfb56 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, @@ -134,11 +136,14 @@ 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, 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); @@ -148,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, @@ -159,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); } @@ -754,6 +760,15 @@ 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); + + 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; } @@ -891,6 +906,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. */ @@ -1296,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, @@ -1367,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: @@ -1667,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); @@ -1703,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; @@ -2228,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) { @@ -2340,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) { @@ -2425,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) { @@ -2535,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) { @@ -2595,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) { @@ -2631,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) { @@ -2735,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) { @@ -2810,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) { @@ -2823,28 +2856,114 @@ static void service_enter_reload(Service *s) { service_enter_reload_signal(s); } -static bool service_should_reload_extensions(Service *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 (!pidref_is_set(&s->main_pid)) { - log_unit_debug(UNIT(s), "Not reloading extensions for service without main PID."); - return false; + 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 = 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; + 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); // 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; } @@ -2857,9 +2976,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; @@ -2955,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) { @@ -2986,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) { @@ -3124,6 +3243,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: @@ -3166,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); @@ -3176,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) { @@ -3386,6 +3510,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; } @@ -3775,7 +3914,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; @@ -4188,6 +4331,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: @@ -4307,7 +4451,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; @@ -4396,10 +4540,21 @@ static void service_sigchld_event(Unit *u, pid_t pid, int code, int status) { break; case SERVICE_REFRESH_EXTENSIONS: - if (f == SERVICE_SUCCESS) - /* Remounting extensions asynchronously done, proceed to reload */ + if (f == SERVICE_SUCCESS) { + s->refreshed_mask |= SERVICE_RELOAD_EXTENSIONS; + 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 */ + s->refreshed_mask |= f == SERVICE_SUCCESS ? SERVICE_RELOAD_CREDENTIALS : 0; service_enter_reload(s); - else + } else service_reload_finish(s, f); break; @@ -4515,6 +4670,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: @@ -4838,7 +4994,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; @@ -4935,7 +5093,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))) { @@ -5204,6 +5363,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, @@ -5390,6 +5550,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, @@ -5888,6 +6049,73 @@ 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" }, + { SERVICE_RELOAD_CREDENTIALS, "credentials" }, +}; + +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..9750b19ce28 100644 --- a/src/core/service.h +++ b/src/core/service.h @@ -96,6 +96,15 @@ typedef enum ServiceRestartMode { _SERVICE_RESTART_MODE_INVALID = -EINVAL, } ServiceRestartMode; +typedef enum ServiceRefreshOnReload { + SERVICE_RELOAD_EXTENSIONS = 1 << 0, + SERVICE_RELOAD_CREDENTIALS = 1 << 1, + _SERVICE_REFRESH_ON_RELOAD_ALL = (1 << 2) - 1, + _SERVICE_REFRESH_ON_RELOAD_INVALID = -EINVAL, +} ServiceRefreshOnReload; + +#define SERVICE_REFRESH_ON_RELOAD_DEFAULT SERVICE_RELOAD_EXTENSIONS + typedef struct ServiceFDStore { Service *service; @@ -237,6 +246,10 @@ typedef struct Service { int reload_signal; usec_t reload_begin_usec; + bool refresh_on_reload_set; + ServiceRefreshOnReload refresh_on_reload_flags; + ServiceRefreshOnReload refreshed_mask; + OOMPolicy oom_policy; char *usb_function_descriptors; @@ -288,6 +301,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/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; \ 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; } 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 }, {} }; 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; 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"); 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