| |
| |
| #include <grp.h> |
| #include <pwd.h> |
| #include <sys/file.h> |
| |
| #include "clean-ipc.h" |
| #include "dynamic-user.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "fs-util.h" |
| #include "io-util.h" |
| #include "parse-util.h" |
| #include "random-util.h" |
| #include "socket-util.h" |
| #include "stdio-util.h" |
| #include "string-util.h" |
| #include "user-util.h" |
| |
| |
| #define UID_CLAMP_INTO_RANGE(rnd) (((uid_t) (rnd) % (DYNAMIC_UID_MAX - DYNAMIC_UID_MIN + 1)) + DYNAMIC_UID_MIN) |
| |
| static DynamicUser* dynamic_user_free(DynamicUser *d) { |
| if (!d) |
| return NULL; |
| |
| if (d->manager) |
| (void) hashmap_remove(d->manager->dynamic_users, d->name); |
| |
| safe_close_pair(d->storage_socket); |
| return mfree(d); |
| } |
| |
| static int dynamic_user_add(Manager *m, const char *name, int storage_socket[2], DynamicUser **ret) { |
| DynamicUser *d = NULL; |
| int r; |
| |
| assert(m); |
| assert(name); |
| assert(storage_socket); |
| |
| r = hashmap_ensure_allocated(&m->dynamic_users, &string_hash_ops); |
| if (r < 0) |
| return r; |
| |
| d = malloc0(offsetof(DynamicUser, name) + strlen(name) + 1); |
| if (!d) |
| return -ENOMEM; |
| |
| strcpy(d->name, name); |
| |
| d->storage_socket[0] = storage_socket[0]; |
| d->storage_socket[1] = storage_socket[1]; |
| |
| r = hashmap_put(m->dynamic_users, d->name, d); |
| if (r < 0) { |
| free(d); |
| return r; |
| } |
| |
| d->manager = m; |
| |
| if (ret) |
| *ret = d; |
| |
| return 0; |
| } |
| |
| static int dynamic_user_acquire(Manager *m, const char *name, DynamicUser** ret) { |
| _cleanup_close_pair_ int storage_socket[2] = { -1, -1 }; |
| DynamicUser *d; |
| int r; |
| |
| assert(m); |
| assert(name); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| d = hashmap_get(m->dynamic_users, name); |
| if (d) { |
| |
| d->n_ref++; |
| *ret = d; |
| return 0; |
| } |
| |
| if (!valid_user_group_name_or_id(name)) |
| return -EINVAL; |
| |
| if (socketpair(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0, storage_socket) < 0) |
| return -errno; |
| |
| r = dynamic_user_add(m, name, storage_socket, &d); |
| if (r < 0) |
| return r; |
| |
| storage_socket[0] = storage_socket[1] = -1; |
| |
| if (ret) { |
| d->n_ref++; |
| *ret = d; |
| } |
| |
| return 1; |
| } |
| |
| static int make_uid_symlinks(uid_t uid, const char *name, bool b) { |
| |
| char path1[STRLEN("/run/systemd/dynamic-uid/direct:") + DECIMAL_STR_MAX(uid_t) + 1]; |
| const char *path2; |
| int r = 0, k; |
| |
| |
| |
| |
| |
| |
| |
| |
| xsprintf(path1, "/run/systemd/dynamic-uid/direct:" UID_FMT, uid); |
| if (unlink(path1) < 0 && errno != ENOENT) |
| r = -errno; |
| |
| if (b && symlink(name, path1) < 0) { |
| k = log_warning_errno(errno, "Failed to symlink \"%s\": %m", path1); |
| if (r == 0) |
| r = k; |
| } |
| |
| path2 = strjoina("/run/systemd/dynamic-uid/direct:", name); |
| if (unlink(path2) < 0 && errno != ENOENT) { |
| k = -errno; |
| if (r == 0) |
| r = k; |
| } |
| |
| if (b && symlink(path1 + STRLEN("/run/systemd/dynamic-uid/direct:"), path2) < 0) { |
| k = log_warning_errno(errno, "Failed to symlink \"%s\": %m", path2); |
| if (r == 0) |
| r = k; |
| } |
| |
| return r; |
| } |
| |
| static int pick_uid(char **suggested_paths, const char *name, uid_t *ret_uid) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| enum { |
| PHASE_SUGGESTED, |
| PHASE_HASHED, |
| PHASE_RANDOM, |
| } 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, current_suggested = 0; |
| int r; |
| |
| (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) |
| return -EBUSY; |
| |
| switch (phase) { |
| |
| case PHASE_SUGGESTED: { |
| struct stat st; |
| |
| if (!suggested_paths || !suggested_paths[current_suggested]) { |
| |
| phase = PHASE_HASHED; |
| continue; |
| } |
| |
| if (stat(suggested_paths[current_suggested++], &st) < 0) |
| continue; |
| |
| candidate = st.st_uid; |
| break; |
| } |
| |
| case PHASE_HASHED: |
| |
| |
| candidate = UID_CLAMP_INTO_RANGE(siphash24(name, strlen(name), hash_key)); |
| |
| |
| phase = PHASE_RANDOM; |
| break; |
| |
| case PHASE_RANDOM: |
| |
| |
| random_bytes(&candidate, sizeof(candidate)); |
| candidate = UID_CLAMP_INTO_RANGE(candidate); |
| break; |
| |
| default: |
| assert_not_reached("unknown phase"); |
| } |
| |
| |
| if (!uid_is_dynamic(candidate)) |
| continue; |
| |
| xsprintf(lock_path, "/run/systemd/dynamic-uid/" UID_FMT, candidate); |
| |
| for (;;) { |
| struct stat st; |
| |
| lock_fd = open(lock_path, O_CREAT|O_RDWR|O_NOFOLLOW|O_CLOEXEC|O_NOCTTY, 0600); |
| if (lock_fd < 0) |
| return -errno; |
| |
| r = flock(lock_fd, LOCK_EX|LOCK_NB); |
| if (r < 0) { |
| if (IN_SET(errno, EBUSY, EAGAIN)) |
| goto next; |
| |
| return -errno; |
| } |
| |
| if (fstat(lock_fd, &st) < 0) |
| return -errno; |
| if (st.st_nlink > 0) |
| break; |
| |
| |
| |
| lock_fd = safe_close(lock_fd); |
| } |
| |
| |
| if (getpwuid(candidate) || |
| getgrgid((gid_t) candidate) || |
| search_ipc(candidate, (gid_t) candidate) != 0) { |
| (void) unlink(lock_path); |
| continue; |
| } |
| |
| |
| l = pwritev(lock_fd, |
| (struct iovec[2]) { |
| IOVEC_INIT_STRING(name), |
| IOVEC_INIT((char[1]) { '\n' }, 1), |
| }, 2, 0); |
| if (l < 0) { |
| r = -errno; |
| (void) unlink(lock_path); |
| return r; |
| } |
| |
| (void) ftruncate(lock_fd, l); |
| (void) make_uid_symlinks(candidate, name, true); |
| |
| *ret_uid = candidate; |
| return TAKE_FD(lock_fd); |
| |
| next: |
| ; |
| } |
| } |
| |
| static int dynamic_user_pop(DynamicUser *d, uid_t *ret_uid, int *ret_lock_fd) { |
| uid_t uid = UID_INVALID; |
| struct iovec iov = IOVEC_INIT(&uid, sizeof(uid)); |
| union { |
| struct cmsghdr cmsghdr; |
| uint8_t buf[CMSG_SPACE(sizeof(int))]; |
| } control = {}; |
| struct msghdr mh = { |
| .msg_control = &control, |
| .msg_controllen = sizeof(control), |
| .msg_iov = &iov, |
| .msg_iovlen = 1, |
| }; |
| struct cmsghdr *cmsg; |
| |
| ssize_t k; |
| int lock_fd = -1; |
| |
| assert(d); |
| assert(ret_uid); |
| assert(ret_lock_fd); |
| |
| |
| |
| |
| k = recvmsg(d->storage_socket[0], &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC); |
| if (k < 0) |
| return -errno; |
| |
| cmsg = cmsg_find(&mh, SOL_SOCKET, SCM_RIGHTS, CMSG_LEN(sizeof(int))); |
| if (cmsg) |
| lock_fd = *(int*) CMSG_DATA(cmsg); |
| else |
| cmsg_close_all(&mh); |
| |
| *ret_uid = uid; |
| *ret_lock_fd = lock_fd; |
| |
| return 0; |
| } |
| |
| static int dynamic_user_push(DynamicUser *d, uid_t uid, int lock_fd) { |
| struct iovec iov = IOVEC_INIT(&uid, sizeof(uid)); |
| union { |
| struct cmsghdr cmsghdr; |
| uint8_t buf[CMSG_SPACE(sizeof(int))]; |
| } control = {}; |
| struct msghdr mh = { |
| .msg_control = &control, |
| .msg_controllen = sizeof(control), |
| .msg_iov = &iov, |
| .msg_iovlen = 1, |
| }; |
| ssize_t k; |
| |
| assert(d); |
| |
| |
| |
| if (lock_fd >= 0) { |
| struct cmsghdr *cmsg; |
| |
| cmsg = CMSG_FIRSTHDR(&mh); |
| cmsg->cmsg_level = SOL_SOCKET; |
| cmsg->cmsg_type = SCM_RIGHTS; |
| cmsg->cmsg_len = CMSG_LEN(sizeof(int)); |
| memcpy(CMSG_DATA(cmsg), &lock_fd, sizeof(int)); |
| |
| mh.msg_controllen = CMSG_SPACE(sizeof(int)); |
| } else { |
| mh.msg_control = NULL; |
| mh.msg_controllen = 0; |
| } |
| |
| k = sendmsg(d->storage_socket[1], &mh, MSG_DONTWAIT|MSG_NOSIGNAL); |
| if (k < 0) |
| return -errno; |
| |
| return 0; |
| } |
| |
| static void unlink_uid_lock(int lock_fd, uid_t uid, const char *name) { |
| char lock_path[STRLEN("/run/systemd/dynamic-uid/") + DECIMAL_STR_MAX(uid_t) + 1]; |
| |
| if (lock_fd < 0) |
| return; |
| |
| xsprintf(lock_path, "/run/systemd/dynamic-uid/" UID_FMT, uid); |
| (void) unlink(lock_path); |
| |
| (void) make_uid_symlinks(uid, name, false); |
| } |
| |
| static int lockfp(int fd, int *fd_lock) { |
| if (lockf(fd, F_LOCK, 0) < 0) |
| return -errno; |
| *fd_lock = fd; |
| return 0; |
| } |
| |
| static void unlockfp(int *fd_lock) { |
| if (*fd_lock < 0) |
| return; |
| lockf(*fd_lock, F_ULOCK, 0); |
| *fd_lock = -1; |
| } |
| |
| static int dynamic_user_realize( |
| DynamicUser *d, |
| char **suggested_dirs, |
| uid_t *ret_uid, gid_t *ret_gid, |
| bool is_user) { |
| |
| _cleanup_(unlockfp) int storage_socket0_lock = -1; |
| _cleanup_close_ int uid_lock_fd = -1; |
| _cleanup_close_ int etc_passwd_lock_fd = -1; |
| uid_t num = UID_INVALID; |
| gid_t gid = GID_INVALID; |
| int r; |
| |
| assert(d); |
| assert(is_user == !!ret_uid); |
| assert(ret_gid); |
| |
| |
| |
| |
| r = lockfp(d->storage_socket[0], &storage_socket0_lock); |
| if (r < 0) |
| return r; |
| |
| r = dynamic_user_pop(d, &num, &uid_lock_fd); |
| if (r < 0) { |
| int new_uid_lock_fd; |
| uid_t new_uid; |
| |
| if (r != -EAGAIN) |
| return r; |
| |
| |
| |
| unlockfp(&storage_socket0_lock); |
| |
| |
| |
| |
| etc_passwd_lock_fd = take_etc_passwd_lock(NULL); |
| if (etc_passwd_lock_fd < 0 && etc_passwd_lock_fd != -EROFS) |
| return etc_passwd_lock_fd; |
| |
| |
| r = parse_uid(d->name, &num); |
| if (r < 0) { |
| struct passwd *p; |
| struct group *g; |
| |
| if (is_user) { |
| |
| p = getpwnam(d->name); |
| if (p) { |
| num = p->pw_uid; |
| gid = p->pw_gid; |
| } else { |
| |
| g = getgrnam(d->name); |
| if (g) |
| return -EILSEQ; |
| } |
| } else { |
| |
| g = getgrnam(d->name); |
| if (g) |
| num = (uid_t) g->gr_gid; |
| else { |
| |
| p = getpwnam(d->name); |
| if (p) |
| return -EILSEQ; |
| } |
| } |
| } |
| |
| if (num == UID_INVALID) { |
| |
| |
| uid_lock_fd = pick_uid(suggested_dirs, d->name, &num); |
| if (uid_lock_fd < 0) |
| return uid_lock_fd; |
| } |
| |
| |
| r = lockfp(d->storage_socket[0], &storage_socket0_lock); |
| if (r < 0) { |
| unlink_uid_lock(uid_lock_fd, num, d->name); |
| return r; |
| } |
| |
| r = dynamic_user_pop(d, &new_uid, &new_uid_lock_fd); |
| if (r < 0) { |
| if (r != -EAGAIN) { |
| |
| unlink_uid_lock(uid_lock_fd, num, d->name); |
| return r; |
| } |
| |
| |
| } else { |
| |
| |
| |
| unlink_uid_lock(uid_lock_fd, num, d->name); |
| safe_close(uid_lock_fd); |
| |
| num = new_uid; |
| uid_lock_fd = new_uid_lock_fd; |
| } |
| } |
| |
| |
| |
| |
| r = dynamic_user_push(d, num, uid_lock_fd); |
| if (r < 0) |
| return r; |
| |
| if (is_user) { |
| *ret_uid = num; |
| *ret_gid = gid != GID_INVALID ? gid : num; |
| } else |
| *ret_gid = num; |
| |
| return 0; |
| } |
| |
| int dynamic_user_current(DynamicUser *d, uid_t *ret) { |
| _cleanup_(unlockfp) int storage_socket0_lock = -1; |
| _cleanup_close_ int lock_fd = -1; |
| uid_t uid; |
| int r; |
| |
| assert(d); |
| assert(ret); |
| |
| |
| |
| r = lockfp(d->storage_socket[0], &storage_socket0_lock); |
| if (r < 0) |
| return r; |
| |
| r = dynamic_user_pop(d, &uid, &lock_fd); |
| if (r < 0) |
| return r; |
| |
| r = dynamic_user_push(d, uid, lock_fd); |
| if (r < 0) |
| return r; |
| |
| *ret = uid; |
| return 0; |
| } |
| |
| static DynamicUser* dynamic_user_ref(DynamicUser *d) { |
| if (!d) |
| return NULL; |
| |
| assert(d->n_ref > 0); |
| d->n_ref++; |
| |
| return d; |
| } |
| |
| static DynamicUser* dynamic_user_unref(DynamicUser *d) { |
| if (!d) |
| return NULL; |
| |
| |
| |
| |
| |
| assert(d->n_ref > 0); |
| d->n_ref--; |
| |
| return NULL; |
| } |
| |
| static int dynamic_user_close(DynamicUser *d) { |
| _cleanup_(unlockfp) int storage_socket0_lock = -1; |
| _cleanup_close_ int lock_fd = -1; |
| uid_t uid; |
| int r; |
| |
| |
| |
| |
| r = lockfp(d->storage_socket[0], &storage_socket0_lock); |
| if (r < 0) |
| return r; |
| |
| r = dynamic_user_pop(d, &uid, &lock_fd); |
| if (r == -EAGAIN) |
| |
| return 0; |
| if (r < 0) |
| return r; |
| |
| |
| unlink_uid_lock(lock_fd, uid, d->name); |
| return 1; |
| } |
| |
| static DynamicUser* dynamic_user_destroy(DynamicUser *d) { |
| if (!d) |
| return NULL; |
| |
| |
| |
| |
| |
| |
| dynamic_user_unref(d); |
| |
| if (d->n_ref > 0) |
| return NULL; |
| |
| (void) dynamic_user_close(d); |
| return dynamic_user_free(d); |
| } |
| |
| int dynamic_user_serialize(Manager *m, FILE *f, FDSet *fds) { |
| DynamicUser *d; |
| Iterator i; |
| |
| assert(m); |
| assert(f); |
| assert(fds); |
| |
| |
| |
| HASHMAP_FOREACH(d, m->dynamic_users, i) { |
| int copy0, copy1; |
| |
| copy0 = fdset_put_dup(fds, d->storage_socket[0]); |
| if (copy0 < 0) |
| return copy0; |
| |
| copy1 = fdset_put_dup(fds, d->storage_socket[1]); |
| if (copy1 < 0) |
| return copy1; |
| |
| fprintf(f, "dynamic-user=%s %i %i\n", d->name, copy0, copy1); |
| } |
| |
| return 0; |
| } |
| |
| void dynamic_user_deserialize_one(Manager *m, const char *value, FDSet *fds) { |
| _cleanup_free_ char *name = NULL, *s0 = NULL, *s1 = NULL; |
| int r, fd0, fd1; |
| |
| assert(m); |
| assert(value); |
| assert(fds); |
| |
| |
| |
| r = extract_many_words(&value, NULL, 0, &name, &s0, &s1, NULL); |
| if (r != 3 || !isempty(value)) { |
| log_debug("Unable to parse dynamic user line."); |
| return; |
| } |
| |
| if (safe_atoi(s0, &fd0) < 0 || !fdset_contains(fds, fd0)) { |
| log_debug("Unable to process dynamic user fd specification."); |
| return; |
| } |
| |
| if (safe_atoi(s1, &fd1) < 0 || !fdset_contains(fds, fd1)) { |
| log_debug("Unable to process dynamic user fd specification."); |
| return; |
| } |
| |
| r = dynamic_user_add(m, name, (int[]) { fd0, fd1 }, NULL); |
| if (r < 0) { |
| log_debug_errno(r, "Failed to add dynamic user: %m"); |
| return; |
| } |
| |
| (void) fdset_remove(fds, fd0); |
| (void) fdset_remove(fds, fd1); |
| } |
| |
| void dynamic_user_vacuum(Manager *m, bool close_user) { |
| DynamicUser *d; |
| Iterator i; |
| |
| assert(m); |
| |
| |
| |
| |
| |
| HASHMAP_FOREACH(d, m->dynamic_users, i) { |
| if (d->n_ref > 0) |
| continue; |
| |
| if (close_user) { |
| log_debug("Removing orphaned dynamic user %s", d->name); |
| (void) dynamic_user_close(d); |
| } |
| |
| dynamic_user_free(d); |
| } |
| } |
| |
| int dynamic_user_lookup_uid(Manager *m, uid_t uid, char **ret) { |
| char lock_path[STRLEN("/run/systemd/dynamic-uid/") + DECIMAL_STR_MAX(uid_t) + 1]; |
| _cleanup_free_ char *user = NULL; |
| uid_t check_uid; |
| int r; |
| |
| assert(m); |
| assert(ret); |
| |
| |
| if (!uid_is_dynamic(uid)) |
| return -ESRCH; |
| |
| xsprintf(lock_path, "/run/systemd/dynamic-uid/" UID_FMT, uid); |
| r = read_one_line_file(lock_path, &user); |
| if (r == -ENOENT) |
| return -ESRCH; |
| if (r < 0) |
| return r; |
| |
| |
| r = dynamic_user_lookup_name(m, user, &check_uid); |
| if (r < 0) |
| return r; |
| if (check_uid != uid) |
| return -ESRCH; |
| |
| *ret = TAKE_PTR(user); |
| |
| return 0; |
| } |
| |
| int dynamic_user_lookup_name(Manager *m, const char *name, uid_t *ret) { |
| DynamicUser *d; |
| int r; |
| |
| assert(m); |
| assert(name); |
| assert(ret); |
| |
| |
| |
| d = hashmap_get(m->dynamic_users, name); |
| if (!d) |
| return -ESRCH; |
| |
| r = dynamic_user_current(d, ret); |
| if (r == -EAGAIN) |
| return -ESRCH; |
| |
| return r; |
| } |
| |
| int dynamic_creds_acquire(DynamicCreds *creds, Manager *m, const char *user, const char *group) { |
| bool acquired = false; |
| int r; |
| |
| assert(creds); |
| assert(m); |
| |
| |
| |
| |
| |
| if (!creds->user && user) { |
| r = dynamic_user_acquire(m, user, &creds->user); |
| if (r < 0) |
| return r; |
| |
| acquired = true; |
| } |
| |
| if (!creds->group) { |
| |
| if (creds->user && (!group || streq_ptr(user, group))) |
| creds->group = dynamic_user_ref(creds->user); |
| else { |
| r = dynamic_user_acquire(m, group, &creds->group); |
| if (r < 0) { |
| if (acquired) |
| creds->user = dynamic_user_unref(creds->user); |
| return r; |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| 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; |
| |
| assert(creds); |
| assert(uid); |
| assert(gid); |
| |
| |
| |
| if (creds->user) { |
| r = dynamic_user_realize(creds->user, suggested_paths, &u, &g, true); |
| if (r < 0) |
| return r; |
| } |
| |
| if (creds->group && creds->group != creds->user) { |
| r = dynamic_user_realize(creds->group, suggested_paths, NULL, &g, false); |
| if (r < 0) |
| return r; |
| } |
| |
| *uid = u; |
| *gid = g; |
| return 0; |
| } |
| |
| void dynamic_creds_unref(DynamicCreds *creds) { |
| assert(creds); |
| |
| creds->user = dynamic_user_unref(creds->user); |
| creds->group = dynamic_user_unref(creds->group); |
| } |
| |
| void dynamic_creds_destroy(DynamicCreds *creds) { |
| assert(creds); |
| |
| creds->user = dynamic_user_destroy(creds->user); |
| creds->group = dynamic_user_destroy(creds->group); |
| } |