diff --git a/src/core/dynamic-user.c b/src/core/dynamic-user.c index 6358fcba02..11d9a3ce37 100644 --- a/src/core/dynamic-user.c +++ b/src/core/dynamic-user.c @@ -182,33 +182,89 @@ static int make_uid_symlinks(uid_t uid, const char *name, bool b) { return r; } -static int pick_uid(const char *name, uid_t *ret_uid) { +static int pick_uid(char **suggested_paths, const char *name, uid_t *ret_uid) { + + /* Find a suitable free UID. We use the following strategy to find a suitable UID: + * + * 1. Initially, we try to read the UID of a number of specified paths. If any of these UIDs works, we use + * them. We use in order to increase the chance of UID reuse, if StateDirectory=, CacheDirectory= or + * LogDirectory= are used, as reusing the UID these directories are owned by saves us from having to + * recursively chown() them to new users. + * + * 2. If that didn't yield a currently unused UID, we hash the user name, and try to use that. This should be + * pretty good, as the use ris by default derived from the unit name, and hence the same service and same + * user should usually get the same UID as long as our hashing doesn't clash. + * + * 3. Finally, if that didn't work, we randomly pick UIDs, until we find one that is empty. + * + * Since the dynamic UID space is relatively small we'll stop trying after 100 iterations, giving up. */ + + enum { + PHASE_SUGGESTED, /* the first phase, reusing directory ownership UIDs */ + PHASE_HASHED, /* the second phase, deriving a UID from the username by hashing */ + PHASE_RANDOM, /* the last phase, randomly picking UIDs */ + } phase = PHASE_SUGGESTED; static const uint8_t hash_key[] = { 0x37, 0x53, 0x7e, 0x31, 0xcf, 0xce, 0x48, 0xf5, 0x8a, 0xbb, 0x39, 0x57, 0x8d, 0xd9, 0xec, 0x59 }; - unsigned n_tries = 100; - uid_t candidate; + unsigned n_tries = 100, current_suggested = 0; int r; - /* A static user by this name does not exist yet. Let's find a free ID then, and use that. We start with a UID - * generated as hash from the user name. */ - candidate = UID_CLAMP_INTO_RANGE(siphash24(name, strlen(name), hash_key)); - (void) mkdir("/run/systemd/dynamic-uid", 0755); for (;;) { char lock_path[strlen("/run/systemd/dynamic-uid/") + DECIMAL_STR_MAX(uid_t) + 1]; _cleanup_close_ int lock_fd = -1; + uid_t candidate; ssize_t l; if (--n_tries <= 0) /* Give up retrying eventually */ return -EBUSY; + switch (phase) { + + case PHASE_SUGGESTED: { + struct stat st; + + if (!suggested_paths || !suggested_paths[current_suggested]) { + /* We reached the end of the suggested paths list, let's try by hashing the name */ + phase = PHASE_HASHED; + continue; + } + + if (stat(suggested_paths[current_suggested++], &st) < 0) + continue; /* We can't read the UID of this path, but that doesn't matter, just try the next */ + + candidate = st.st_uid; + break; + } + + case PHASE_HASHED: + /* A static user by this name does not exist yet. Let's find a free ID then, and use that. We + * start with a UID generated as hash from the user name. */ + candidate = UID_CLAMP_INTO_RANGE(siphash24(name, strlen(name), hash_key)); + + /* If this one fails, we should proceed with random tries */ + phase = PHASE_RANDOM; + break; + + case PHASE_RANDOM: + + /* Pick another random UID, and see if that works for us. */ + random_bytes(&candidate, sizeof(candidate)); + candidate = UID_CLAMP_INTO_RANGE(candidate); + break; + + default: + assert_not_reached("unknown phase"); + } + + /* Make sure whatever we picked here actually is in the right range */ if (!uid_is_dynamic(candidate)) - goto next; + continue; xsprintf(lock_path, "/run/systemd/dynamic-uid/" UID_FMT, candidate); @@ -240,7 +296,7 @@ static int pick_uid(const char *name, uid_t *ret_uid) { /* Some superficial check whether this UID/GID might already be taken by some static user */ if (getpwuid(candidate) || getgrgid((gid_t) candidate)) { (void) unlink(lock_path); - goto next; + continue; } /* Let's store the user name in the lock file, so that we can use it for looking up the username for a UID */ @@ -264,9 +320,7 @@ static int pick_uid(const char *name, uid_t *ret_uid) { return r; next: - /* Pick another random UID, and see if that works for us. */ - random_bytes(&candidate, sizeof(candidate)); - candidate = UID_CLAMP_INTO_RANGE(candidate); + ; } } @@ -363,7 +417,7 @@ static void unlink_uid_lock(int lock_fd, uid_t uid, const char *name) { (void) make_uid_symlinks(uid, name, false); /* remove direct lookup symlinks */ } -int dynamic_user_realize(DynamicUser *d, uid_t *ret) { +int dynamic_user_realize(DynamicUser *d, char **suggested_dirs, uid_t *ret) { _cleanup_close_ int etc_passwd_lock_fd = -1, uid_lock_fd = -1; uid_t uid = UID_INVALID; @@ -421,7 +475,7 @@ int dynamic_user_realize(DynamicUser *d, uid_t *ret) { if (uid == UID_INVALID) { /* No static UID assigned yet, excellent. Let's pick a new dynamic one, and lock it. */ - uid_lock_fd = pick_uid(d->name, &uid); + uid_lock_fd = pick_uid(suggested_dirs, d->name, &uid); if (uid_lock_fd < 0) return uid_lock_fd; } @@ -744,7 +798,7 @@ int dynamic_creds_acquire(DynamicCreds *creds, Manager *m, const char *user, con return 0; } -int dynamic_creds_realize(DynamicCreds *creds, uid_t *uid, gid_t *gid) { +int dynamic_creds_realize(DynamicCreds *creds, char **suggested_paths, uid_t *uid, gid_t *gid) { uid_t u = UID_INVALID; gid_t g = GID_INVALID; int r; @@ -756,13 +810,13 @@ int dynamic_creds_realize(DynamicCreds *creds, uid_t *uid, gid_t *gid) { /* Realize both the referenced user and group */ if (creds->user) { - r = dynamic_user_realize(creds->user, &u); + r = dynamic_user_realize(creds->user, suggested_paths, &u); if (r < 0) return r; } if (creds->group && creds->group != creds->user) { - r = dynamic_user_realize(creds->group, &g); + r = dynamic_user_realize(creds->group, suggested_paths, &g); if (r < 0) return r; } else diff --git a/src/core/dynamic-user.h b/src/core/dynamic-user.h index 0b8bce1a72..e7de4f46ae 100644 --- a/src/core/dynamic-user.h +++ b/src/core/dynamic-user.h @@ -45,7 +45,7 @@ struct DynamicUser { int dynamic_user_acquire(Manager *m, const char *name, DynamicUser **ret); -int dynamic_user_realize(DynamicUser *d, uid_t *ret); +int dynamic_user_realize(DynamicUser *d, char **suggested_paths, uid_t *ret); int dynamic_user_current(DynamicUser *d, uid_t *ret); DynamicUser* dynamic_user_ref(DynamicUser *d); @@ -60,7 +60,7 @@ int dynamic_user_lookup_uid(Manager *m, uid_t uid, char **ret); int dynamic_user_lookup_name(Manager *m, const char *name, uid_t *ret); int dynamic_creds_acquire(DynamicCreds *creds, Manager *m, const char *user, const char *group); -int dynamic_creds_realize(DynamicCreds *creds, uid_t *uid, gid_t *gid); +int dynamic_creds_realize(DynamicCreds *creds, char **suggested_paths, uid_t *uid, gid_t *gid); void dynamic_creds_unref(DynamicCreds *creds); void dynamic_creds_destroy(DynamicCreds *creds); diff --git a/src/core/execute.c b/src/core/execute.c index 1faa8e34dc..e1c2a57cc6 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -2617,6 +2617,49 @@ static int acquire_home(const ExecContext *c, uid_t uid, const char** home, char return 1; } +static int compile_suggested_paths(const ExecContext *c, const ExecParameters *p, char ***ret) { + _cleanup_strv_free_ char ** list = NULL; + ExecDirectoryType t; + int r; + + assert(c); + assert(p); + assert(ret); + + assert(c->dynamic_user); + + /* Compile a list of paths that it might make sense to read the owning UID from to use as initial candidate for + * dynamic UID allocation, in order to save us from doing costly recursive chown()s of the special + * directories. */ + + for (t = 0; t < _EXEC_DIRECTORY_TYPE_MAX; t++) { + char **i; + + if (t == EXEC_DIRECTORY_CONFIGURATION) + continue; + + if (!p->prefix[t]) + continue; + + STRV_FOREACH(i, c->directories[t].paths) { + char *e; + + e = strjoin(p->prefix[t], "/private/", *i); + if (!e) + return -ENOMEM; + + r = strv_consume(&list, e); + if (r < 0) + return r; + } + } + + *ret = list; + list = NULL; + + return 0; +} + static int exec_child( Unit *unit, ExecCommand *command, @@ -2738,6 +2781,7 @@ static int exec_child( } if (context->dynamic_user && dcreds) { + _cleanup_strv_free_ char **suggested_paths = NULL; /* Make sure we bypass our own NSS module for any NSS checks */ if (putenv((char*) "SYSTEMD_NSS_DYNAMIC_BYPASS=1") != 0) { @@ -2745,7 +2789,13 @@ static int exec_child( return log_unit_error_errno(unit, errno, "Failed to update environment: %m"); } - r = dynamic_creds_realize(dcreds, &uid, &gid); + r = compile_suggested_paths(context, params, &suggested_paths); + if (r < 0) { + *exit_status = EXIT_MEMORY; + return log_oom(); + } + + r = dynamic_creds_realize(dcreds, suggested_paths, &uid, &gid); if (r < 0) { *exit_status = EXIT_USER; return log_unit_error_errno(unit, r, "Failed to update dynamic user credentials: %m");