diff --git a/src/basic/path-util.c b/src/basic/path-util.c index c463ae23ab..deffa8a425 100644 --- a/src/basic/path-util.c +++ b/src/basic/path-util.c @@ -14,6 +14,7 @@ #include "alloc-util.h" #include "extract-word.h" +#include "fd-util.h" #include "fs-util.h" #include "glob-util.h" #include "log.h" @@ -1127,3 +1128,9 @@ bool prefixed_path_strv_contains(char **l, const char *path) { return false; } + +bool credential_name_valid(const char *s) { + /* We want that credential names are both valid in filenames (since that's our primary way to pass + * them around) and as fdnames (which is how we might want to pass them around eventually) */ + return filename_is_valid(s) && fdname_is_valid(s); +} diff --git a/src/basic/path-util.h b/src/basic/path-util.h index 99c4e35782..1afbebd77f 100644 --- a/src/basic/path-util.h +++ b/src/basic/path-util.h @@ -173,3 +173,5 @@ static inline const char *empty_to_root(const char *path) { bool path_strv_contains(char **l, const char *path); bool prefixed_path_strv_contains(char **l, const char *path); + +bool credential_name_valid(const char *s); diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index c96c654ff0..9042f0baa8 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -748,6 +748,82 @@ static int property_get_log_extra_fields( return sd_bus_message_close_container(reply); } +static int property_get_set_credential( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ExecContext *c = userdata; + ExecSetCredential *sc; + Iterator iterator; + int r; + + assert(bus); + assert(c); + assert(property); + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "(say)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(sc, c->set_credentials, iterator) { + + r = sd_bus_message_open_container(reply, 'r', "say"); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "s", sc->id); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', sc->data, sc->size); + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int property_get_load_credential( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + ExecContext *c = userdata; + char **i, **j; + int r; + + assert(bus); + assert(c); + assert(property); + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "(ss)"); + if (r < 0) + return r; + + STRV_FOREACH_PAIR(i, j, c->load_credentials) { + r = sd_bus_message_append(reply, "(ss)", *i, *j); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + static int property_get_root_hash( sd_bus *bus, const char *path, @@ -965,6 +1041,8 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_PROPERTY("Group", "s", NULL, offsetof(ExecContext, group), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("DynamicUser", "b", bus_property_get_bool, offsetof(ExecContext, dynamic_user), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RemoveIPC", "b", bus_property_get_bool, offsetof(ExecContext, remove_ipc), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("SetCredential", "a(say)", property_get_set_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("LoadCredential", "a(ss)", property_get_load_credential, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("SupplementaryGroups", "as", NULL, offsetof(ExecContext, supplementary_groups), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("PAMName", "s", NULL, offsetof(ExecContext, pam_name), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ReadWritePaths", "as", NULL, offsetof(ExecContext, read_write_paths), SD_BUS_VTABLE_PROPERTY_CONST), @@ -1794,6 +1872,146 @@ int bus_exec_context_set_transient_property( return 1; + } else if (streq(name, "SetCredential")) { + bool isempty = true; + + r = sd_bus_message_enter_container(message, 'a', "(say)"); + if (r < 0) + return r; + + for (;;) { + const char *id; + const void *p; + size_t sz; + + r = sd_bus_message_enter_container(message, 'r', "say"); + if (r < 0) + return r; + if (r == 0) + break; + + r = sd_bus_message_read(message, "s", &id); + if (r < 0) + return r; + + r = sd_bus_message_read_array(message, 'y', &p, &sz); + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!credential_name_valid(id)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential ID is invalid: %s", id); + + isempty = false; + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + _cleanup_free_ char *a = NULL, *b = NULL; + _cleanup_free_ void *copy = NULL; + ExecSetCredential *old; + + copy = memdup(p, sz); + if (!copy) + return -ENOMEM; + + old = hashmap_get(c->set_credentials, id); + if (old) { + free_and_replace(old->data, copy); + old->size = sz; + } else { + _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL; + + sc = new0(ExecSetCredential, 1); + if (!sc) + return -ENOMEM; + + sc->id = strdup(id); + if (!sc->id) + return -ENOMEM; + + sc->data = TAKE_PTR(copy); + sc->size = sz; + + r = hashmap_ensure_allocated(&c->set_credentials, &exec_set_credential_hash_ops); + if (r < 0) + return r; + + r = hashmap_put(c->set_credentials, sc->id, sc); + if (r < 0) + return r; + + TAKE_PTR(sc); + } + + a = specifier_escape(id); + if (!a) + return -ENOMEM; + + b = cescape_length(p, sz); + if (!b) + return -ENOMEM; + + (void) unit_write_settingf(u, flags, name, "%s=%s:%s", name, a, b); + } + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) { + c->set_credentials = hashmap_free(c->set_credentials); + (void) unit_write_settingf(u, flags, name, "%s=", name); + } + + return 1; + + } else if (streq(name, "LoadCredential")) { + bool isempty = true; + + r = sd_bus_message_enter_container(message, 'a', "(ss)"); + if (r < 0) + return r; + + for (;;) { + const char *id, *source; + + r = sd_bus_message_read(message, "(ss)", &id, &source); + if (r < 0) + return r; + if (r == 0) + break; + + if (!credential_name_valid(id)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential ID is invalid: %s", id); + + if (!(path_is_absolute(source) ? path_is_normalized(source) : credential_name_valid(source))) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Credential source is invalid: %s", source); + + isempty = false; + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + r = strv_extend_strv(&c->load_credentials, STRV_MAKE(id, source), /* filter_duplicates = */ false); + if (r < 0) + return r; + + (void) unit_write_settingf(u, flags|UNIT_ESCAPE_SPECIFIERS, name, "%s=%s:%s", name, id, source); + } + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!UNIT_WRITE_FLAGS_NOOP(flags) && isempty) { + c->load_credentials = strv_free(c->load_credentials); + (void) unit_write_settingf(u, flags, name, "%s=", name); + } + + return 1; + } else if (streq(name, "SyslogLevel")) { int32_t level; diff --git a/src/core/execute.c b/src/core/execute.c index d5107288a1..81829007b4 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ #include "sd-messages.h" +#include "acl-util.h" #include "af-list.h" #include "alloc-util.h" #if HAVE_APPARMOR @@ -41,8 +43,8 @@ #include "barrier.h" #include "cap-list.h" #include "capability-util.h" -#include "chown-recursive.h" #include "cgroup-setup.h" +#include "chown-recursive.h" #include "cpu-set-util.h" #include "def.h" #include "env-file.h" @@ -51,6 +53,7 @@ #include "execute.h" #include "exit-status.h" #include "fd-util.h" +#include "fileio.h" #include "format-util.h" #include "fs-util.h" #include "glob-util.h" @@ -64,6 +67,7 @@ #include "memory-util.h" #include "missing_fs.h" #include "mkdir.h" +#include "mountpoint-util.h" #include "namespace.h" #include "parse-util.h" #include "path-util.h" @@ -85,6 +89,7 @@ #include "strv.h" #include "syslog-util.h" #include "terminal-util.h" +#include "tmpfile-util.h" #include "umask-util.h" #include "unit.h" #include "user-util.h" @@ -1417,6 +1422,14 @@ static bool context_has_no_new_privileges(const ExecContext *c) { c->protect_hostname; } +static bool exec_context_has_credentials(const ExecContext *context) { + + assert(context); + + return !hashmap_isempty(context->set_credentials) || + context->load_credentials; +} + #if HAVE_SECCOMP static bool skip_seccomp_unavailable(const Unit* u, const char* msg) { @@ -1725,7 +1738,7 @@ static int build_environment( assert(p); assert(ret); -#define N_ENV_VARS 15 +#define N_ENV_VARS 16 our_env = new0(char*, N_ENV_VARS + _EXEC_DIRECTORY_TYPE_MAX); if (!our_env) return -ENOMEM; @@ -1873,6 +1886,14 @@ static int build_environment( our_env[n_env++] = x; } + if (exec_context_has_credentials(c) && p->prefix[EXEC_DIRECTORY_RUNTIME]) { + x = strjoin("CREDENTIALS_DIRECTORY=", p->prefix[EXEC_DIRECTORY_RUNTIME], "/credentials/", u->id); + if (!x) + return -ENOMEM; + + our_env[n_env++] = x; + } + our_env[n_env++] = NULL; assert(n_env <= N_ENV_VARS + _EXEC_DIRECTORY_TYPE_MAX); #undef N_ENV_VARS @@ -2378,6 +2399,437 @@ fail: return r; } +static int write_credential( + int dfd, + const char *id, + const void *data, + size_t size, + uid_t uid, + bool ownership_ok) { + + _cleanup_(unlink_and_freep) char *tmp = NULL; + _cleanup_close_ int fd = -1; + int r; + + r = tempfn_random_child("", "cred", &tmp); + if (r < 0) + return r; + + fd = openat(dfd, tmp, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL|O_NOFOLLOW|O_NOCTTY, 0600); + if (fd < 0) { + tmp = mfree(tmp); + return -errno; + } + + r = loop_write(fd, data, size, /* do_pool = */ false); + if (r < 0) + return r; + + if (fchmod(fd, 0400) < 0) /* Take away "w" bit */ + return -errno; + + if (uid_is_valid(uid) && uid != getuid()) { +#if HAVE_ACL + r = fd_add_uid_acl_permission(fd, uid, /* read = */ true, /* write = */ false, /* execute = */ false); +#else + r = -EOPNOTSUPP; +#endif + if (r < 0) { + if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r)) + return r; + + if (!ownership_ok) /* Ideally we use ACLs, since we can neatly express what we want + * to express: that the user gets read access and nothing + * else. But if the backing fs can't support that (e.g. ramfs) + * then we can use file ownership instead. But that's only safe if + * we can then re-mount the whole thing read-only, so that the + * user can no longer chmod() the file to gain write access. */ + return r; + + if (fchown(fd, uid, (gid_t) -1) < 0) + return -errno; + } + } + + if (renameat(dfd, tmp, dfd, id) < 0) + return -errno; + + tmp = mfree(tmp); + return 0; +} + +#define CREDENTIALS_BYTES_MAX (1024LU * 1024LU) /* Refuse to pass more than 1M, after all this is unswappable memory */ + +static int acquire_credentials( + const ExecContext *context, + const ExecParameters *params, + const char *p, + uid_t uid, + bool ownership_ok) { + + uint64_t left = CREDENTIALS_BYTES_MAX; + _cleanup_close_ int dfd = -1; + ExecSetCredential *sc; + char **id, **fn; + Iterator iterator; + int r; + + assert(context); + assert(p); + + dfd = open(p, O_DIRECTORY|O_CLOEXEC); + if (dfd < 0) + return -errno; + + /* First we use the literally specified credentials. Note that they might be overriden again below, + * and thus act as a "default" if the same credential is specified multiple times */ + HASHMAP_FOREACH(sc, context->set_credentials, iterator) { + size_t add; + + add = strlen(sc->id) + sc->size; + if (add > left) + return -E2BIG; + + r = write_credential(dfd, sc->id, sc->data, sc->size, uid, ownership_ok); + if (r < 0) + return r; + + left -= add; + } + + /* Then, load credential off disk (or acquire via AF_UNIX socket) */ + STRV_FOREACH_PAIR(id, fn, context->load_credentials) { + ReadFullFileFlags flags = READ_FULL_FILE_SECURE; + _cleanup_(erase_and_freep) char *data = NULL; + _cleanup_free_ char *j = NULL; + const char *source; + size_t size, add; + + if (path_is_absolute(*fn)) { + /* If this is an absolute path, read the data directly from it, and support AF_UNIX sockets */ + source = *fn; + flags |= READ_FULL_FILE_CONNECT_SOCKET; + } else if (params->received_credentials) { + /* If this is a relative path, take it relative to the credentials 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. */ + j = path_join(params->received_credentials, *fn); + if (!j) + return -ENOMEM; + + source = j; + } else + source = NULL; + + if (source) + r = read_full_file_full(AT_FDCWD, source, flags, &data, &size); + else + r = -ENOENT; + if (r == -ENOENT && + faccessat(dfd, *id, F_OK, AT_SYMLINK_NOFOLLOW) >= 0) /* If the source file doesn't exist, but we already acquired the key otherwise, then don't fail */ + continue; + if (r < 0) + return r; + + add = strlen(*id) + size; + if (add > left) + return -E2BIG; + + r = write_credential(dfd, *id, data, size, uid, ownership_ok); + if (r < 0) + return r; + + left -= add; + } + + if (fchmod(dfd, 0500) < 0) /* Now take away the "w" bit */ + return -errno; + + /* After we created all keys with the right perms, also make sure the credential store as a whole is + * accessible */ + + if (uid_is_valid(uid) && uid != getuid()) { +#if HAVE_ACL + r = fd_add_uid_acl_permission(dfd, uid, /* read = */ true, /* write = */ false, /* execute = */ true); +#else + r = -EOPNOTSUPP; +#endif + if (r < 0) { + if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r)) + return r; + + if (!ownership_ok) + return r; + + if (fchown(dfd, uid, (gid_t) -1) < 0) + return -errno; + } + } + + return 0; +} + +static int setup_credentials_internal( + const ExecContext *context, + const ExecParameters *params, + const char *final, /* This is where the credential store shall eventually end up at */ + const char *workspace, /* This is where we can prepare it before moving it to the final place */ + bool reuse_workspace, /* Whether to reuse any existing workspace mount if it already is a mount */ + bool must_mount, /* Whether to require that we mount something, it's not OK to use the plain directory fall back */ + uid_t uid) { + + int r, workspace_mounted; /* negative if we don't know yet whether we have/can mount something; true + * if we mounted something; false if we definitely can't mount anything */ + bool final_mounted; + const char *where; + + assert(context); + assert(final); + assert(workspace); + + if (reuse_workspace) { + r = path_is_mount_point(workspace, NULL, 0); + if (r < 0) + return r; + if (r > 0) + workspace_mounted = true; /* If this is already a mount, and we are supposed to reuse it, let's keep this in mind */ + else + workspace_mounted = -1; /* We need to figure out if we can mount something to the workspace */ + } else + workspace_mounted = -1; /* ditto */ + + r = path_is_mount_point(final, NULL, 0); + if (r < 0) + return r; + if (r > 0) { + /* If the final place already has something mounted, we use that. If the workspace also has + * something mounted we assume it's actually the same mount (but with MS_RDONLY + * different). */ + final_mounted = true; + + if (workspace_mounted < 0) { + /* If the final place is mounted, but the workspace we isn't, then let's bind mount + * the final version to the workspace, and make it writable, so that we can make + * changes */ + + if (mount(final, workspace, NULL, MS_BIND|MS_REC, NULL) < 0) + return -errno; + + if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0) + return -errno; + + workspace_mounted = true; + } + } else + final_mounted = false; + + if (workspace_mounted < 0) { + /* Nothing is mounted on the workspace yet, let's try to mount something now */ + for (int try = 0;; try++) { + + if (try == 0) { + /* Try "ramfs" first, since it's not swap backed */ + if (mount("ramfs", workspace, "ramfs", MS_NODEV|MS_NOEXEC|MS_NOSUID, "mode=0700") >= 0) { + workspace_mounted = true; + break; + } + + } else if (try == 1) { + _cleanup_free_ char *opts = NULL; + + if (asprintf(&opts, "mode=0700,nr_inodes=1024,size=%lu", CREDENTIALS_BYTES_MAX) < 0) + return -ENOMEM; + + /* Fall back to "tmpfs" otherwise */ + if (mount("tmpfs", workspace, "tmpfs", MS_NODEV|MS_NOEXEC|MS_NOSUID, opts) >= 0) { + workspace_mounted = true; + break; + } + + } else { + /* If that didn't work, try to make a bind mount from the final to the workspace, so that we can make it writable there. */ + if (mount(final, workspace, NULL, MS_BIND|MS_REC, NULL) < 0) { + if (!ERRNO_IS_PRIVILEGE(errno)) /* Propagate anything that isn't a permission problem */ + return -errno; + + if (must_mount) /* If we it's not OK to use the plain directory + * fallback, propagate all errors too */ + return -errno; + + /* If we lack privileges to bind mount stuff, then let's gracefully + * proceed for compat with container envs, and just use the final dir + * as is. */ + + workspace_mounted = false; + break; + } + + /* Make the new bind mount writable (i.e. drop MS_RDONLY) */ + if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0) + return -errno; + + workspace_mounted = true; + break; + } + } + } + + assert(!must_mount || workspace_mounted > 0); + where = workspace_mounted ? workspace : final; + + r = acquire_credentials(context, params, where, uid, workspace_mounted); + if (r < 0) + return r; + + if (workspace_mounted) { + /* Make workspace read-only now, so that any bind mount we make from it defaults to read-only too */ + if (mount(NULL, workspace, NULL, MS_BIND|MS_REMOUNT|MS_RDONLY|MS_NODEV|MS_NOEXEC|MS_NOSUID, NULL) < 0) + return -errno; + + /* And mount it to the final place, read-only */ + if (final_mounted) { + if (umount2(workspace, MNT_DETACH|UMOUNT_NOFOLLOW) < 0) + return -errno; + } else { + if (mount(workspace, final, NULL, MS_MOVE, NULL) < 0) + return -errno; + } + } else { + _cleanup_free_ char *parent = NULL; + + /* If we do not have our own mount put used the plain directory fallback, then we need to + * open access to the top-level credential directory and the per-service directory now */ + + parent = dirname_malloc(final); + if (!parent) + return -ENOMEM; + if (chmod(parent, 0755) < 0) + return -errno; + } + + return 0; +} + +static int setup_credentials( + const ExecContext *context, + const ExecParameters *params, + const char *unit, + uid_t uid) { + + _cleanup_free_ char *p = NULL, *q = NULL; + const char *i; + int r; + + assert(context); + assert(params); + + if (!exec_context_has_credentials(context)) + return 0; + + if (!params->prefix[EXEC_DIRECTORY_RUNTIME]) + return -EINVAL; + + /* This where we'll place stuff when we are done; this main credentials directory is world-readable, + * and the subdir we mount over with a read-only file system readable by the service's user */ + q = path_join(params->prefix[EXEC_DIRECTORY_RUNTIME], "credentials"); + if (!q) + return -ENOMEM; + + r = mkdir_label(q, 0755); /* top-level dir: world readable/searchable */ + if (r < 0 && r != -EEXIST) + return r; + + p = path_join(q, unit); + if (!p) + return -ENOMEM; + + r = mkdir_label(p, 0700); /* per-unit dir: private to user */ + if (r < 0 && r != -EEXIST) + return r; + + r = safe_fork("(sd-mkdcreds)", FORK_DEATHSIG|FORK_WAIT|FORK_NEW_MOUNTNS, NULL); + if (r < 0) { + _cleanup_free_ char *t = NULL, *u = NULL; + + /* If this is not a privilege or support issue then propagate the error */ + if (!ERRNO_IS_NOT_SUPPORTED(r) && !ERRNO_IS_PRIVILEGE(r)) + return r; + + /* 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"); + if (!t) + return -ENOMEM; + + /* We can't set up a mount namespace. In that case operate on a fixed, inaccessible per-unit + * directory outside of /run/credentials/ first, and then move it over to /run/credentials/ + * after it is fully set up */ + u = path_join(t, unit); + if (!u) + return -ENOMEM; + + FOREACH_STRING(i, t, u) { + r = mkdir_label(i, 0700); + if (r < 0 && r != -EEXIST) + return r; + } + + r = setup_credentials_internal( + context, + params, + p, /* final mount point */ + u, /* temporary workspace to overmount */ + true, /* reuse the workspace if it is already a mount */ + false, /* it's OK to fall back to a plain directory if we can't mount anything */ + uid); + + (void) rmdir(u); /* remove the workspace again if we can. */ + + if (r < 0) + return r; + + } else if (r == 0) { + + /* We managed to set up a mount namespace, and are now in a child. That's great. In this case + * we can use the same directory for all cases, after turning off propagation. Question + * though is: where do we turn off propagation exactly, and where do we place the workspace + * directory? We need some place that is guaranteed to be a mount point in the host, and + * which is guaranteed to have a subdir we can mount over. /run/ is not suitable for this, + * since we ultimately want to move the resulting file system there, i.e. we need propagation + * for /run/ eventually. We could use our own /run/systemd/bind mount on itself, but that + * would be visible in the host mount table all the time, which we want to avoid. Hence, what + * we do here instead we use /dev/ and /dev/shm/ for our purposes. We know for sure that + * /dev/ is a mount point and we now for sure that /dev/shm/ exists. Hence we can turn off + * propagation on the former, and then overmount the latter. + * + * Yes it's nasty playing games with /dev/ and /dev/shm/ like this, since it does not exist + * for this purpose, but there are few other candidates that work equally well for us, and + * given that the we do this in a privately namespaced short-lived single-threaded process + * that noone else sees this should be OK to do.*/ + + if (mount(NULL, "/dev", NULL, MS_SLAVE|MS_REC, NULL) < 0) /* Turn off propagation from our namespace to host */ + goto child_fail; + + r = setup_credentials_internal( + context, + params, + p, /* final mount point */ + "/dev/shm", /* temporary workspace to overmount */ + false, /* do not reuse /dev/shm if it is already a mount, under no circumstances */ + true, /* insist that something is mounted, do not allow fallback to plain directory */ + uid); + if (r < 0) + goto child_fail; + + _exit(EXIT_SUCCESS); + + child_fail: + _exit(EXIT_FAILURE); + } + + return 0; +} + #if ENABLE_SMACK static int setup_smack( const ExecContext *context, @@ -3489,6 +3941,14 @@ static int exec_child( return log_unit_error_errno(unit, r, "Failed to set up special execution directory in %s: %m", params->prefix[dt]); } + if (FLAGS_SET(params->flags, EXEC_WRITE_CREDENTIALS)) { + r = setup_credentials(context, params, unit->id, uid); + if (r < 0) { + *exit_status = EXIT_CREDENTIALS; + return log_unit_error_errno(unit, r, "Failed to set up credentials: %m"); + } + } + r = build_environment( unit, context, @@ -4276,6 +4736,9 @@ void exec_context_done(ExecContext *c) { c->network_namespace_path = mfree(c->network_namespace_path); c->log_namespace = mfree(c->log_namespace); + + c->load_credentials = strv_free(c->load_credentials); + c->set_credentials = hashmap_free(c->set_credentials); } int exec_context_destroy_runtime_directory(const ExecContext *c, const char *runtime_prefix) { @@ -4304,6 +4767,26 @@ int exec_context_destroy_runtime_directory(const ExecContext *c, const char *run return 0; } +int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_prefix, const char *unit) { + _cleanup_free_ char *p = NULL; + + assert(c); + + if (!runtime_prefix || !unit) + return 0; + + p = path_join(runtime_prefix, "credentials", unit); + if (!p) + return -ENOMEM; + + /* This is either a tmpfs/ramfs of its own, or a plain directory. Either way, let's first try to + * unmount it, and afterwards remove the mount point */ + (void) umount2(p, MNT_DETACH|UMOUNT_NOFOLLOW); + (void) rm_rf(p, REMOVE_ROOT|REMOVE_CHMOD); + + return 0; +} + static void exec_command_done(ExecCommand *c) { assert(c); @@ -5812,6 +6295,17 @@ void exec_params_clear(ExecParameters *p) { p->exec_fd = safe_close(p->exec_fd); } +ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc) { + if (!sc) + return NULL; + + free(sc->id); + free(sc->data); + return mfree(sc); +} + +DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(exec_set_credential_hash_ops, char, string_hash_func, string_compare_func, ExecSetCredential, exec_set_credential_free); + static const char* const exec_input_table[_EXEC_INPUT_MAX] = { [EXEC_INPUT_NULL] = "null", [EXEC_INPUT_TTY] = "tty", diff --git a/src/core/execute.h b/src/core/execute.h index 1ea7e51fd7..810e585fa8 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -145,6 +145,13 @@ typedef enum ExecCleanMask { _EXEC_CLEAN_MASK_INVALID = -1, } ExecCleanMask; +/* A credential configured with SetCredential= */ +typedef struct ExecSetCredential { + char *id; + void *data; + size_t size; +} ExecSetCredential; + /* Encodes configuration parameters applied to invoked commands. Does not carry runtime data, but only configuration * changes sourced from unit files and suchlike. ExecContext objects are usually embedded into Unit objects, and do not * change after being loaded. */ @@ -303,6 +310,9 @@ struct ExecContext { ExecDirectory directories[_EXEC_DIRECTORY_TYPE_MAX]; ExecPreserveMode runtime_directory_preserve_mode; usec_t timeout_clean_usec; + + Hashmap *set_credentials; /* output id → ExecSetCredential */ + char **load_credentials; /* pairs of output id, path/input id */ }; static inline bool exec_context_restrict_namespaces_set(const ExecContext *c) { @@ -321,11 +331,12 @@ typedef enum ExecFlags { EXEC_CGROUP_DELEGATE = 1 << 6, EXEC_IS_CONTROL = 1 << 7, EXEC_CONTROL_CGROUP = 1 << 8, /* Place the process not in the indicated cgroup but in a subcgroup '/.control', but only EXEC_CGROUP_DELEGATE and EXEC_IS_CONTROL is set, too */ + EXEC_WRITE_CREDENTIALS = 1 << 9, /* Set up the credential store logic */ /* The following are not used by execute.c, but by consumers internally */ - EXEC_PASS_FDS = 1 << 9, - EXEC_SETENV_RESULT = 1 << 10, - EXEC_SET_WATCHDOG = 1 << 11, + EXEC_PASS_FDS = 1 << 10, + EXEC_SETENV_RESULT = 1 << 11, + EXEC_SET_WATCHDOG = 1 << 12, } ExecFlags; /* Parameters for a specific invocation of a command. This structure is put together right before a command is @@ -345,6 +356,7 @@ struct ExecParameters { const char *cgroup_path; char **prefix; + const char *received_credentials; const char *confirm_spawn; @@ -386,6 +398,7 @@ void exec_context_done(ExecContext *c); void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix); int exec_context_destroy_runtime_directory(const ExecContext *c, const char *runtime_root); +int exec_context_destroy_credentials(const ExecContext *c, const char *runtime_root, const char *unit); const char* exec_context_fdname(const ExecContext *c, int fd_index); @@ -418,6 +431,11 @@ void exec_params_clear(ExecParameters *p); bool exec_context_get_cpu_affinity_from_numa(const ExecContext *c); +ExecSetCredential *exec_set_credential_free(ExecSetCredential *sc); +DEFINE_TRIVIAL_CLEANUP_FUNC(ExecSetCredential*, exec_set_credential_free); + +extern const struct hash_ops exec_set_credential_hash_ops; + const char* exec_output_to_string(ExecOutput i) _const_; ExecOutput exec_output_from_string(const char *s) _pure_; diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4 index 9a1fcca46e..45147f0d57 100644 --- a/src/core/load-fragment-gperf.gperf.m4 +++ b/src/core/load-fragment-gperf.gperf.m4 @@ -147,6 +147,8 @@ $1.LogsDirectoryMode, config_parse_mode, 0, $1.LogsDirectory, config_parse_exec_directories, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_LOGS].paths) $1.ConfigurationDirectoryMode, config_parse_mode, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].mode) $1.ConfigurationDirectory, config_parse_exec_directories, 0, offsetof($1, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].paths) +$1.SetCredential, config_parse_set_credential, 0, offsetof($1, exec_context) +$1.LoadCredential, config_parse_load_credential, 0, offsetof($1, exec_context) $1.TimeoutCleanSec, config_parse_sec, 0, offsetof($1, exec_context.timeout_clean_usec) $1.ProtectHostname, config_parse_bool, 0, offsetof($1, exec_context.protect_hostname) m4_ifdef(`HAVE_PAM', diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index df93fbb28f..a8eee28502 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -60,6 +60,7 @@ #include "unit-name.h" #include "unit-printf.h" #include "user-util.h" +#include "utf8.h" #include "web-util.h" static int parse_socket_protocol(const char *s) { @@ -4268,6 +4269,155 @@ int config_parse_exec_directories( } } +int config_parse_set_credential( + 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) { + + _cleanup_free_ char *word = NULL, *k = NULL, *unescaped = NULL; + ExecContext *context = data; + ExecSetCredential *old; + Unit *u = userdata; + const char *p; + int r, l; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(context); + + if (isempty(rvalue)) { + /* Empty assignment resets the list */ + context->set_credentials = hashmap_free(context->set_credentials); + return 0; + } + + p = rvalue; + r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == -ENOMEM) + return log_oom(); + if (r <= 0 || !p) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue); + return 0; + } + + r = unit_full_printf(u, word, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", word); + return 0; + } + if (!credential_name_valid(k)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name \"%s\" not valid, ignoring.", k); + return 0; + } + + /* We support escape codes here, so that users can insert trailing \n if they like */ + l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped); + if (l < 0) { + log_syntax(unit, LOG_WARNING, filename, line, l, "Can't unescape \"%s\", ignoring: %m", p); + return 0; + } + + old = hashmap_get(context->set_credentials, k); + if (old) { + free_and_replace(old->data, unescaped); + old->size = l; + } else { + _cleanup_(exec_set_credential_freep) ExecSetCredential *sc = NULL; + + sc = new0(ExecSetCredential, 1); + if (!sc) + return log_oom(); + + sc->id = TAKE_PTR(k); + sc->data = TAKE_PTR(unescaped); + sc->size = l; + + r = hashmap_ensure_allocated(&context->set_credentials, &exec_set_credential_hash_ops); + if (r < 0) + return r; + + r = hashmap_put(context->set_credentials, sc->id, sc); + if (r < 0) + return log_oom(); + + TAKE_PTR(sc); + } + + return 0; +} + +int config_parse_load_credential( + 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) { + + _cleanup_free_ char *word = NULL, *k = NULL, *q = NULL; + ExecContext *context = data; + Unit *u = userdata; + const char *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(context); + + if (isempty(rvalue)) { + /* Empty assignment resets the list */ + context->load_credentials = strv_free(context->load_credentials); + return 0; + } + + p = rvalue; + r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == -ENOMEM) + return log_oom(); + if (r <= 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue); + return 0; + } + + r = unit_full_printf(u, word, &k); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", word); + return 0; + } + if (!credential_name_valid(k)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Credential name \"%s\" not valid, ignoring.", k); + return 0; + } + r = unit_full_printf(u, p, &q); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to resolve unit specifiers in \"%s\", ignoring: %m", p); + return 0; + } + if (path_is_absolute(q) ? !path_is_normalized(q) : !credential_name_valid(q)) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Credential source \"%s\" not valid, ignoring.", q); + return 0; + } + + r = strv_consume_pair(&context->load_credentials, TAKE_PTR(k), TAKE_PTR(q)); + if (r < 0) + return log_oom(); + + 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 ae134610b1..e90953b80f 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -90,6 +90,8 @@ CONFIG_PARSER_PROTOTYPE(config_parse_exec_smack_process_label); CONFIG_PARSER_PROTOTYPE(config_parse_address_families); CONFIG_PARSER_PROTOTYPE(config_parse_runtime_preserve_mode); CONFIG_PARSER_PROTOTYPE(config_parse_exec_directories); +CONFIG_PARSER_PROTOTYPE(config_parse_set_credential); +CONFIG_PARSER_PROTOTYPE(config_parse_load_credential); 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 b7ca184081..49bf5419d8 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -591,6 +591,7 @@ static char** sanitize_environment(char **l) { l, "CACHE_DIRECTORY", "CONFIGURATION_DIRECTORY", + "CREDENTIALS_DIRECTORY", "EXIT_CODE", "EXIT_STATUS", "INVOCATION_ID", @@ -754,6 +755,7 @@ static int manager_setup_sigchld_event_source(Manager *m) { int manager_new(UnitFileScope scope, ManagerTestRunFlags test_run_flags, Manager **_m) { _cleanup_(manager_freep) Manager *m = NULL; + const char *e; int r; assert(_m); @@ -857,6 +859,13 @@ int manager_new(UnitFileScope scope, ManagerTestRunFlags test_run_flags, Manager if (r < 0) return r; + e = secure_getenv("CREDENTIALS_DIRECTORY"); + if (e) { + m->received_credentials = strdup(e); + if (!m->received_credentials) + return -ENOMEM; + } + r = sd_event_default(&m->event); if (r < 0) return r; @@ -1420,6 +1429,7 @@ Manager* manager_free(Manager *m) { for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) m->prefix[dt] = mfree(m->prefix[dt]); + free(m->received_credentials); return mfree(m); } diff --git a/src/core/manager.h b/src/core/manager.h index 81b0c13a95..d507dc1f3b 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -424,6 +424,7 @@ struct Manager { /* Prefixes of e.g. RuntimeDirectory= */ char *prefix[_EXEC_DIRECTORY_TYPE_MAX]; + char *received_credentials; /* Used in the SIGCHLD and sd_notify() message invocation logic to avoid that we dispatch the same event * multiple times on the same unit. */ diff --git a/src/core/meson.build b/src/core/meson.build index fa95108523..72a00f1955 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -178,7 +178,8 @@ libcore = static_library( libkmod, libapparmor, libselinux, - libmount]) + libmount, + libacl]) systemd_sources = files('main.c') diff --git a/src/core/mount.c b/src/core/mount.c index 337e94e90e..6e9d61ff18 100644 --- a/src/core/mount.c +++ b/src/core/mount.c @@ -869,7 +869,7 @@ static void mount_enter_dead(Mount *m, MountResult f) { m->exec_runtime = exec_runtime_unref(m->exec_runtime, true); - unit_destroy_runtime_directory(UNIT(m), &m->exec_context); + unit_destroy_runtime_data(UNIT(m), &m->exec_context); unit_unref_uid_gid(UNIT(m), true); diff --git a/src/core/service.c b/src/core/service.c index 641391752a..dec6b2c9ea 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -1801,7 +1801,7 @@ static void service_enter_dead(Service *s, ServiceResult f, bool allow_restart) s->exec_runtime = exec_runtime_unref(s->exec_runtime, true); /* Also, remove the runtime directory */ - unit_destroy_runtime_directory(UNIT(s), &s->exec_context); + unit_destroy_runtime_data(UNIT(s), &s->exec_context); /* Get rid of the IPC bits of the user */ unit_unref_uid_gid(UNIT(s), true); @@ -2156,7 +2156,7 @@ static void service_enter_start(Service *s) { r = service_spawn(s, c, timeout, - EXEC_PASS_FDS|EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT|EXEC_APPLY_TTY_STDIN|EXEC_SET_WATCHDOG, + EXEC_PASS_FDS|EXEC_APPLY_SANDBOXING|EXEC_APPLY_CHROOT|EXEC_APPLY_TTY_STDIN|EXEC_SET_WATCHDOG|EXEC_WRITE_CREDENTIALS, &pid); if (r < 0) goto fail; diff --git a/src/core/socket.c b/src/core/socket.c index 127195c9fe..0588a34e38 100644 --- a/src/core/socket.c +++ b/src/core/socket.c @@ -2080,7 +2080,7 @@ static void socket_enter_dead(Socket *s, SocketResult f) { s->exec_runtime = exec_runtime_unref(s->exec_runtime, true); - unit_destroy_runtime_directory(UNIT(s), &s->exec_context); + unit_destroy_runtime_data(UNIT(s), &s->exec_context); unit_unref_uid_gid(UNIT(s), true); diff --git a/src/core/swap.c b/src/core/swap.c index 20179de2d2..83ae66c964 100644 --- a/src/core/swap.c +++ b/src/core/swap.c @@ -706,7 +706,7 @@ static void swap_enter_dead(Swap *s, SwapResult f) { s->exec_runtime = exec_runtime_unref(s->exec_runtime, true); - unit_destroy_runtime_directory(UNIT(s), &s->exec_context); + unit_destroy_runtime_data(UNIT(s), &s->exec_context); unit_unref_uid_gid(UNIT(s), true); diff --git a/src/core/unit.c b/src/core/unit.c index 0e66c13294..e81ef2fd9c 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -5429,6 +5429,8 @@ int unit_set_exec_params(Unit *u, ExecParameters *p) { p->cgroup_path = u->cgroup_path; SET_FLAG(p->flags, EXEC_CGROUP_DELEGATE, unit_cgroup_delegate(u)); + p->received_credentials = u->manager->received_credentials; + return 0; } @@ -6123,10 +6125,15 @@ int unit_test_trigger_loaded(Unit *u) { return 0; } -void unit_destroy_runtime_directory(Unit *u, const ExecContext *context) { +void unit_destroy_runtime_data(Unit *u, const ExecContext *context) { + assert(u); + assert(context); + if (context->runtime_directory_preserve_mode == EXEC_PRESERVE_NO || (context->runtime_directory_preserve_mode == EXEC_PRESERVE_RESTART && !unit_will_restart(u))) exec_context_destroy_runtime_directory(context, u->manager->prefix[EXEC_DIRECTORY_RUNTIME]); + + exec_context_destroy_credentials(context, u->manager->prefix[EXEC_DIRECTORY_RUNTIME], u->id); } int unit_clean(Unit *u, ExecCleanMask mask) { diff --git a/src/core/unit.h b/src/core/unit.h index d5e4c65989..e217cec219 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -880,7 +880,7 @@ int unit_failure_action_exit_status(Unit *u); int unit_test_trigger_loaded(Unit *u); -void unit_destroy_runtime_directory(Unit *u, const ExecContext *context); +void unit_destroy_runtime_data(Unit *u, const ExecContext *context); int unit_clean(Unit *u, ExecCleanMask mask); int unit_can_clean(Unit *u, ExecCleanMask *ret_mask); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index d010d3bf3e..2ad196e824 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -973,6 +973,117 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con return 1; } + if (streq(field, "SetCredential")) { + 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', "SetCredential"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'v', "a(say)"); + if (r < 0) + return bus_log_create_error(r); + + if (isempty(eq)) + r = sd_bus_message_append(m, "a(say)", 0); + else { + _cleanup_free_ char *word = NULL, *unescaped = NULL; + const char *p = eq; + int l; + + r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(r, "Failed to parse SetCredential= parameter: %s", eq); + if (r == 0 || !p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to SetCredential=."); + + l = cunescape(p, UNESCAPE_ACCEPT_NUL, &unescaped); + if (l < 0) + return log_error_errno(l, "Failed to unescape SetCredential= value: %s", p); + + r = sd_bus_message_open_container(m, 'a', "(say)"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'r', "say"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", word); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_array(m, 'y', unescaped, l); + 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); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + return 1; + } + + if (streq(field, "LoadCredential")) { + 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', "LoadCredential"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'v', "a(ss)"); + if (r < 0) + return bus_log_create_error(r); + + if (isempty(eq)) + r = sd_bus_message_append(m, "a(ss)", 0); + else { + _cleanup_free_ char *word = NULL; + const char *p = eq; + + r = extract_first_word(&p, &word, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(r, "Failed to parse LoadCredential= parameter: %s", eq); + if (r == 0 || !p) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing argument to LoadCredential=."); + + r = sd_bus_message_append(m, "a(ss)", 1, word, p); + } + 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; + } + if (streq(field, "LogExtraFields")) { r = sd_bus_message_open_container(m, 'r', "sv"); if (r < 0) diff --git a/src/shared/exit-status.c b/src/shared/exit-status.c index 82b1422b2d..e2b6c67953 100644 --- a/src/shared/exit-status.c +++ b/src/shared/exit-status.c @@ -70,6 +70,8 @@ const ExitStatusMapping exit_status_mappings[256] = { [EXIT_LOGS_DIRECTORY] = { "LOGS_DIRECTORY", EXIT_STATUS_SYSTEMD }, [EXIT_CONFIGURATION_DIRECTORY] = { "CONFIGURATION_DIRECTORY", EXIT_STATUS_SYSTEMD }, [EXIT_NUMA_POLICY] = { "NUMA_POLICY", EXIT_STATUS_SYSTEMD }, + [EXIT_CREDENTIALS] = { "CREDENTIALS", EXIT_STATUS_SYSTEMD }, + [EXIT_EXCEPTION] = { "EXCEPTION", EXIT_STATUS_SYSTEMD }, [EXIT_INVALIDARGUMENT] = { "INVALIDARGUMENT", EXIT_STATUS_LSB }, diff --git a/src/shared/exit-status.h b/src/shared/exit-status.h index 9ea147c842..7ac99a4810 100644 --- a/src/shared/exit-status.h +++ b/src/shared/exit-status.h @@ -70,6 +70,7 @@ enum { EXIT_LOGS_DIRECTORY, /* 240 */ EXIT_CONFIGURATION_DIRECTORY, EXIT_NUMA_POLICY, + EXIT_CREDENTIALS, EXIT_EXCEPTION = 255, /* Whenever we want to propagate an abnormal/signal exit, in line with bash */ };