| /* SPDX-License-Identifier: LGPL-2.1+ */ |
| |
| #include <grp.h> |
| #include <linux/fs.h> |
| #include <linux/magic.h> |
| #include <openssl/pem.h> |
| #include <pwd.h> |
| #include <sys/ioctl.h> |
| #include <sys/quota.h> |
| #include <sys/stat.h> |
| |
| #include "btrfs-util.h" |
| #include "bus-common-errors.h" |
| #include "bus-error.h" |
| #include "bus-log-control-api.h" |
| #include "bus-polkit.h" |
| #include "clean-ipc.h" |
| #include "conf-files.h" |
| #include "device-util.h" |
| #include "dirent-util.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "format-util.h" |
| #include "fs-util.h" |
| #include "gpt.h" |
| #include "home-util.h" |
| #include "homed-conf.h" |
| #include "homed-home-bus.h" |
| #include "homed-home.h" |
| #include "homed-manager-bus.h" |
| #include "homed-manager.h" |
| #include "homed-varlink.h" |
| #include "io-util.h" |
| #include "mkdir.h" |
| #include "process-util.h" |
| #include "quota-util.h" |
| #include "random-util.h" |
| #include "socket-util.h" |
| #include "stat-util.h" |
| #include "strv.h" |
| #include "tmpfile-util.h" |
| #include "udev-util.h" |
| #include "user-record-sign.h" |
| #include "user-record-util.h" |
| #include "user-record.h" |
| #include "user-util.h" |
| |
| /* Where to look for private/public keys that are used to sign the user records. We are not using |
| * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that |
| * since we want to auto-generate a persistent private/public key pair if we need to. */ |
| #define KEY_PATHS_NULSTR \ |
| "/etc/systemd/home/\0" \ |
| "/run/systemd/home/\0" \ |
| "/var/lib/systemd/home/\0" \ |
| "/usr/local/lib/systemd/home/\0" \ |
| "/usr/lib/systemd/home/\0" |
| |
| static bool uid_is_home(uid_t uid) { |
| return uid >= HOME_UID_MIN && uid <= HOME_UID_MAX; |
| } |
| /* Takes a value generated randomly or by hashing and turns it into a UID in the right range */ |
| |
| #define UID_CLAMP_INTO_HOME_RANGE(rnd) (((uid_t) (rnd) % (HOME_UID_MAX - HOME_UID_MIN + 1)) + HOME_UID_MIN) |
| |
| DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_uid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); |
| DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_name_hash_ops, char, string_hash_func, string_compare_func, Home, home_free); |
| DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_worker_pid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); |
| DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, path_hash_func, path_compare, Home, home_free); |
| |
| static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata); |
| static int manager_gc_images(Manager *m); |
| static int manager_enumerate_images(Manager *m); |
| static int manager_assess_image(Manager *m, int dir_fd, const char *dir_path, const char *dentry_name); |
| static void manager_revalidate_image(Manager *m, Home *h); |
| |
| static void manager_watch_home(Manager *m) { |
| struct statfs sfs; |
| int r; |
| |
| assert(m); |
| |
| m->inotify_event_source = sd_event_source_unref(m->inotify_event_source); |
| m->scan_slash_home = false; |
| |
| if (statfs("/home/", &sfs) < 0) { |
| log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, |
| "Failed to statfs() /home/ directory, disabling automatic scanning."); |
| return; |
| } |
| |
| if (is_network_fs(&sfs)) { |
| log_info("/home/ is a network file system, disabling automatic scanning."); |
| return; |
| } |
| |
| if (is_fs_type(&sfs, AUTOFS_SUPER_MAGIC)) { |
| log_info("/home/ is on autofs, disabling automatic scanning."); |
| return; |
| } |
| |
| m->scan_slash_home = true; |
| |
| r = sd_event_add_inotify(m->event, &m->inotify_event_source, "/home/", IN_CREATE|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF|IN_ONLYDIR|IN_MOVED_TO|IN_MOVED_FROM|IN_DELETE, on_home_inotify, m); |
| if (r < 0) |
| log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, |
| "Failed to create inotify watch on /home/, ignoring."); |
| |
| (void) sd_event_source_set_description(m->inotify_event_source, "home-inotify"); |
| } |
| |
| static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) { |
| Manager *m = userdata; |
| const char *e, *n; |
| |
| assert(m); |
| assert(event); |
| |
| if ((event->mask & (IN_Q_OVERFLOW|IN_MOVE_SELF|IN_DELETE_SELF|IN_IGNORED|IN_UNMOUNT)) != 0) { |
| |
| if (FLAGS_SET(event->mask, IN_Q_OVERFLOW)) |
| log_debug("/home/ inotify queue overflow, rescanning."); |
| else if (FLAGS_SET(event->mask, IN_MOVE_SELF)) |
| log_info("/home/ moved or renamed, recreating watch and rescanning."); |
| else if (FLAGS_SET(event->mask, IN_DELETE_SELF)) |
| log_info("/home/ deleted, recreating watch and rescanning."); |
| else if (FLAGS_SET(event->mask, IN_UNMOUNT)) |
| log_info("/home/ unmounted, recreating watch and rescanning."); |
| else if (FLAGS_SET(event->mask, IN_IGNORED)) |
| log_info("/home/ watch invalidated, recreating watch and rescanning."); |
| |
| manager_watch_home(m); |
| (void) manager_gc_images(m); |
| (void) manager_enumerate_images(m); |
| (void) bus_manager_emit_auto_login_changed(m); |
| return 0; |
| } |
| |
| /* For the other inotify events, let's ignore all events for file names that don't match our |
| * expectations */ |
| if (isempty(event->name)) |
| return 0; |
| e = endswith(event->name, FLAGS_SET(event->mask, IN_ISDIR) ? ".homedir" : ".home"); |
| if (!e) |
| return 0; |
| |
| n = strndupa(event->name, e - event->name); |
| if (!suitable_user_name(n)) |
| return 0; |
| |
| if ((event->mask & (IN_CREATE|IN_CLOSE_WRITE|IN_MOVED_TO)) != 0) { |
| if (FLAGS_SET(event->mask, IN_CREATE)) |
| log_debug("/home/%s has been created, having a look.", event->name); |
| else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE)) |
| log_debug("/home/%s has been modified, having a look.", event->name); |
| else if (FLAGS_SET(event->mask, IN_MOVED_TO)) |
| log_debug("/home/%s has been moved in, having a look.", event->name); |
| |
| (void) manager_assess_image(m, -1, "/home/", event->name); |
| (void) bus_manager_emit_auto_login_changed(m); |
| } |
| |
| if ((event->mask & (IN_DELETE|IN_MOVED_FROM|IN_DELETE)) != 0) { |
| Home *h; |
| |
| if (FLAGS_SET(event->mask, IN_DELETE)) |
| log_debug("/home/%s has been deleted, revalidating.", event->name); |
| else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE)) |
| log_debug("/home/%s has been closed after writing, revalidating.", event->name); |
| else if (FLAGS_SET(event->mask, IN_MOVED_FROM)) |
| log_debug("/home/%s has been moved away, revalidating.", event->name); |
| |
| h = hashmap_get(m->homes_by_name, n); |
| if (h) { |
| manager_revalidate_image(m, h); |
| (void) bus_manager_emit_auto_login_changed(m); |
| } |
| } |
| |
| return 0; |
| } |
| |
| int manager_new(Manager **ret) { |
| _cleanup_(manager_freep) Manager *m = NULL; |
| int r; |
| |
| assert(ret); |
| |
| m = new(Manager, 1); |
| if (!m) |
| return -ENOMEM; |
| |
| *m = (Manager) { |
| .default_storage = _USER_STORAGE_INVALID, |
| }; |
| |
| r = manager_parse_config_file(m); |
| if (r < 0) |
| return r; |
| |
| r = sd_event_default(&m->event); |
| if (r < 0) |
| return r; |
| |
| r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL); |
| if (r < 0) |
| return r; |
| |
| r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL); |
| if (r < 0) |
| return r; |
| |
| (void) sd_event_set_watchdog(m->event, true); |
| |
| m->homes_by_uid = hashmap_new(&homes_by_uid_hash_ops); |
| if (!m->homes_by_uid) |
| return -ENOMEM; |
| |
| m->homes_by_name = hashmap_new(&homes_by_name_hash_ops); |
| if (!m->homes_by_name) |
| return -ENOMEM; |
| |
| m->homes_by_worker_pid = hashmap_new(&homes_by_worker_pid_hash_ops); |
| if (!m->homes_by_worker_pid) |
| return -ENOMEM; |
| |
| m->homes_by_sysfs = hashmap_new(&homes_by_sysfs_hash_ops); |
| if (!m->homes_by_sysfs) |
| return -ENOMEM; |
| |
| *ret = TAKE_PTR(m); |
| return 0; |
| } |
| |
| Manager* manager_free(Manager *m) { |
| assert(m); |
| |
| hashmap_free(m->homes_by_uid); |
| hashmap_free(m->homes_by_name); |
| hashmap_free(m->homes_by_worker_pid); |
| hashmap_free(m->homes_by_sysfs); |
| |
| m->inotify_event_source = sd_event_source_unref(m->inotify_event_source); |
| |
| bus_verify_polkit_async_registry_free(m->polkit_registry); |
| |
| sd_bus_flush_close_unref(m->bus); |
| sd_event_unref(m->event); |
| |
| m->notify_socket_event_source = sd_event_source_unref(m->notify_socket_event_source); |
| m->device_monitor = sd_device_monitor_unref(m->device_monitor); |
| |
| m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source); |
| m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source); |
| m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source); |
| |
| if (m->private_key) |
| EVP_PKEY_free(m->private_key); |
| |
| hashmap_free(m->public_keys); |
| |
| varlink_server_unref(m->varlink_server); |
| |
| free(m->default_file_system_type); |
| |
| return mfree(m); |
| } |
| |
| int manager_verify_user_record(Manager *m, UserRecord *hr) { |
| EVP_PKEY *pkey; |
| Iterator i; |
| int r; |
| |
| assert(m); |
| assert(hr); |
| |
| if (!m->private_key && hashmap_isempty(m->public_keys)) { |
| r = user_record_has_signature(hr); |
| if (r < 0) |
| return r; |
| |
| return r ? -ENOKEY : USER_RECORD_UNSIGNED; |
| } |
| |
| /* Is it our own? */ |
| if (m->private_key) { |
| r = user_record_verify(hr, m->private_key); |
| switch (r) { |
| |
| case USER_RECORD_FOREIGN: |
| /* This record is not signed by this key, but let's see below */ |
| break; |
| |
| case USER_RECORD_SIGNED: /* Signed by us, but also by others, let's propagate that */ |
| case USER_RECORD_SIGNED_EXCLUSIVE: /* Signed by us, and nothing else, ditto */ |
| case USER_RECORD_UNSIGNED: /* Not signed at all, ditto */ |
| default: |
| return r; |
| } |
| } |
| |
| HASHMAP_FOREACH(pkey, m->public_keys, i) { |
| r = user_record_verify(hr, pkey); |
| switch (r) { |
| |
| case USER_RECORD_FOREIGN: |
| /* This record is not signed by this key, but let's see our other keys */ |
| break; |
| |
| case USER_RECORD_SIGNED: /* It's signed by this key we are happy with, but which is not our own. */ |
| case USER_RECORD_SIGNED_EXCLUSIVE: |
| return USER_RECORD_FOREIGN; |
| |
| case USER_RECORD_UNSIGNED: /* It's not signed at all */ |
| default: |
| return r; |
| } |
| } |
| |
| return -ENOKEY; |
| } |
| |
| static int manager_add_home_by_record( |
| Manager *m, |
| const char *name, |
| int dir_fd, |
| const char *fname) { |
| |
| _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; |
| _cleanup_(user_record_unrefp) UserRecord *hr = NULL; |
| unsigned line, column; |
| int r, is_signed; |
| struct stat st; |
| Home *h; |
| |
| assert(m); |
| assert(name); |
| assert(fname); |
| |
| if (fstatat(dir_fd, fname, &st, 0) < 0) |
| return log_error_errno(errno, "Failed to stat identity record %s: %m", fname); |
| |
| if (!S_ISREG(st.st_mode)) { |
| log_debug("Identity record file %s is not a regular file, ignoring.", fname); |
| return 0; |
| } |
| |
| if (st.st_size == 0) |
| goto unlink_this_file; |
| |
| r = json_parse_file_at(NULL, dir_fd, fname, JSON_PARSE_SENSITIVE, &v, &line, &column); |
| if (r < 0) |
| return log_error_errno(r, "Failed to parse identity record at %s:%u%u: %m", fname, line, column); |
| |
| if (json_variant_is_blank_object(v)) |
| goto unlink_this_file; |
| |
| hr = user_record_new(); |
| if (!hr) |
| return log_oom(); |
| |
| r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG); |
| if (r < 0) |
| return r; |
| |
| if (!streq_ptr(hr->user_name, name)) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Identity's user name %s does not match file name %s, refusing.", hr->user_name, name); |
| |
| is_signed = manager_verify_user_record(m, hr); |
| switch (is_signed) { |
| |
| case -ENOKEY: |
| return log_warning_errno(is_signed, "User record %s is not signed by any accepted key, ignoring.", fname); |
| case USER_RECORD_UNSIGNED: |
| return log_warning_errno(SYNTHETIC_ERRNO(EPERM), "User record %s is not signed at all, ignoring.", fname); |
| case USER_RECORD_SIGNED: |
| log_info("User record %s is signed by us (and others), accepting.", fname); |
| break; |
| case USER_RECORD_SIGNED_EXCLUSIVE: |
| log_info("User record %s is signed only by us, accepting.", fname); |
| break; |
| case USER_RECORD_FOREIGN: |
| log_info("User record %s is signed by registered key from others, accepting.", fname); |
| break; |
| default: |
| assert(is_signed < 0); |
| return log_error_errno(is_signed, "Failed to verify signature of user record in %s: %m", fname); |
| } |
| |
| h = hashmap_get(m->homes_by_name, name); |
| if (h) { |
| r = home_set_record(h, hr); |
| if (r < 0) |
| return log_error_errno(r, "Failed to update home record for %s: %m", name); |
| |
| /* If we acquired a record now for a previously unallocated entry, then reset the state. This |
| * makes sure home_get_state() will check for the availability of the image file dynamically |
| * in order to detect to distinguish HOME_INACTIVE and HOME_ABSENT. */ |
| if (h->state == HOME_UNFIXATED) |
| h->state = _HOME_STATE_INVALID; |
| } else { |
| r = home_new(m, hr, NULL, &h); |
| if (r < 0) |
| return log_error_errno(r, "Failed to allocate new home object: %m"); |
| |
| log_info("Added registered home for user %s.", hr->user_name); |
| } |
| |
| /* Only entries we exclusively signed are writable to us, hence remember the result */ |
| h->signed_locally = is_signed == USER_RECORD_SIGNED_EXCLUSIVE; |
| |
| return 1; |
| |
| unlink_this_file: |
| /* If this is an empty file, then let's just remove it. An empty file is not useful in any case, and |
| * apparently xfs likes to leave empty files around when not unmounted cleanly (see |
| * https://github.com/systemd/systemd/issues/15178 for example). Note that we don't delete non-empty |
| * files even if they are invalid, because that's just too risky, we might delete data the user still |
| * needs. But empty files are never useful, hence let's just remove them. */ |
| |
| if (unlinkat(dir_fd, fname, 0) < 0) |
| return log_error_errno(errno, "Failed to remove empty user record file %s: %m", fname); |
| |
| log_notice("Discovered empty user record file /var/lib/systemd/home/%s, removed automatically.", fname); |
| return 0; |
| } |
| |
| static int manager_enumerate_records(Manager *m) { |
| _cleanup_closedir_ DIR *d = NULL; |
| struct dirent *de; |
| |
| assert(m); |
| |
| d = opendir("/var/lib/systemd/home/"); |
| if (!d) |
| return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, |
| "Failed to open /var/lib/systemd/home/: %m"); |
| |
| FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read record directory: %m")) { |
| _cleanup_free_ char *n = NULL; |
| const char *e; |
| |
| if (!dirent_is_file(de)) |
| continue; |
| |
| e = endswith(de->d_name, ".identity"); |
| if (!e) |
| continue; |
| |
| n = strndup(de->d_name, e - de->d_name); |
| if (!n) |
| return log_oom(); |
| |
| if (!suitable_user_name(n)) |
| continue; |
| |
| (void) manager_add_home_by_record(m, n, dirfd(d), de->d_name); |
| } |
| |
| return 0; |
| } |
| |
| static int search_quota(uid_t uid, const char *exclude_quota_path) { |
| struct stat exclude_st = {}; |
| dev_t previous_devno = 0; |
| const char *where; |
| int r; |
| |
| /* Checks whether the specified UID owns any files on the files system, but ignore any file system |
| * backing the specified file. The file is used when operating on home directories, where it's OK if |
| * the UID of them already owns files. */ |
| |
| if (exclude_quota_path && stat(exclude_quota_path, &exclude_st) < 0) { |
| if (errno != ENOENT) |
| return log_warning_errno(errno, "Failed to stat %s, ignoring: %m", exclude_quota_path); |
| } |
| |
| /* Check a few usual suspects where regular users might own files. Note that this is by no means |
| * comprehensive, but should cover most cases. Note that in an ideal world every user would be |
| * registered in NSS and avoid our own UID range, but for all other cases, it's a good idea to be |
| * paranoid and check quota if we can. */ |
| FOREACH_STRING(where, "/home/", "/tmp/", "/var/", "/var/mail/", "/var/tmp/", "/var/spool/") { |
| struct dqblk req; |
| struct stat st; |
| |
| if (stat(where, &st) < 0) { |
| log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, |
| "Failed to stat %s, ignoring: %m", where); |
| continue; |
| } |
| |
| if (major(st.st_dev) == 0) { |
| log_debug("Directory %s is not on a real block device, not checking quota for UID use.", where); |
| continue; |
| } |
| |
| if (st.st_dev == exclude_st.st_dev) { /* If an exclude path is specified, then ignore quota |
| * reported on the same block device as that path. */ |
| log_debug("Directory %s is where the home directory is located, not checking quota for UID use.", where); |
| continue; |
| } |
| |
| if (st.st_dev == previous_devno) { /* Does this directory have the same devno as the previous |
| * one we tested? If so, there's no point in testing this |
| * again. */ |
| log_debug("Directory %s is on same device as previous tested directory, not checking quota for UID use a second time.", where); |
| continue; |
| } |
| |
| previous_devno = st.st_dev; |
| |
| r = quotactl_devno(QCMD_FIXED(Q_GETQUOTA, USRQUOTA), st.st_dev, uid, &req); |
| if (r < 0) { |
| if (ERRNO_IS_NOT_SUPPORTED(r)) |
| log_debug_errno(r, "No UID quota support on %s, ignoring.", where); |
| else |
| log_warning_errno(r, "Failed to query quota on %s, ignoring: %m", where); |
| |
| continue; |
| } |
| |
| if ((FLAGS_SET(req.dqb_valid, QIF_SPACE) && req.dqb_curspace > 0) || |
| (FLAGS_SET(req.dqb_valid, QIF_INODES) && req.dqb_curinodes > 0)) { |
| log_debug_errno(errno, "Quota reports UID " UID_FMT " occupies disk space on %s.", uid, where); |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int manager_acquire_uid( |
| Manager *m, |
| uid_t start_uid, |
| const char *user_name, |
| const char *exclude_quota_path, |
| uid_t *ret) { |
| |
| static const uint8_t hash_key[] = { |
| 0xa3, 0xb8, 0x82, 0x69, 0x9a, 0x71, 0xf7, 0xa9, |
| 0xe0, 0x7c, 0xf6, 0xf1, 0x21, 0x69, 0xd2, 0x1e |
| }; |
| |
| enum { |
| PHASE_SUGGESTED, |
| PHASE_HASHED, |
| PHASE_RANDOM |
| } phase = PHASE_SUGGESTED; |
| |
| unsigned n_tries = 100; |
| int r; |
| |
| assert(m); |
| assert(ret); |
| |
| for (;;) { |
| struct passwd *pw; |
| struct group *gr; |
| uid_t candidate; |
| Home *other; |
| |
| if (--n_tries <= 0) |
| return -EBUSY; |
| |
| switch (phase) { |
| |
| case PHASE_SUGGESTED: |
| phase = PHASE_HASHED; |
| |
| if (!uid_is_home(start_uid)) |
| continue; |
| |
| candidate = start_uid; |
| break; |
| |
| case PHASE_HASHED: |
| phase = PHASE_RANDOM; |
| |
| if (!user_name) |
| continue; |
| |
| candidate = UID_CLAMP_INTO_HOME_RANGE(siphash24(user_name, strlen(user_name), hash_key)); |
| break; |
| |
| case PHASE_RANDOM: |
| random_bytes(&candidate, sizeof(candidate)); |
| candidate = UID_CLAMP_INTO_HOME_RANGE(candidate); |
| break; |
| |
| default: |
| assert_not_reached("unknown phase"); |
| } |
| |
| other = hashmap_get(m->homes_by_uid, UID_TO_PTR(candidate)); |
| if (other) { |
| log_debug("Candidate UID " UID_FMT " already used by another home directory (%s), let's try another.", candidate, other->user_name); |
| continue; |
| } |
| |
| pw = getpwuid(candidate); |
| if (pw) { |
| log_debug("Candidate UID " UID_FMT " already registered by another user in NSS (%s), let's try another.", candidate, pw->pw_name); |
| continue; |
| } |
| |
| gr = getgrgid((gid_t) candidate); |
| if (gr) { |
| log_debug("Candidate UID " UID_FMT " already registered by another group in NSS (%s), let's try another.", candidate, gr->gr_name); |
| continue; |
| } |
| |
| r = search_ipc(candidate, (gid_t) candidate); |
| if (r < 0) |
| continue; |
| if (r > 0) { |
| log_debug_errno(r, "Candidate UID " UID_FMT " already owns IPC objects, let's try another: %m", candidate); |
| continue; |
| } |
| |
| r = search_quota(candidate, exclude_quota_path); |
| if (r != 0) |
| continue; |
| |
| *ret = candidate; |
| return 0; |
| } |
| } |
| |
| static int manager_add_home_by_image( |
| Manager *m, |
| const char *user_name, |
| const char *realm, |
| const char *image_path, |
| const char *sysfs, |
| UserStorage storage, |
| uid_t start_uid) { |
| |
| _cleanup_(user_record_unrefp) UserRecord *hr = NULL; |
| uid_t uid; |
| Home *h; |
| int r; |
| |
| assert(m); |
| |
| assert(m); |
| assert(user_name); |
| assert(image_path); |
| assert(storage >= 0); |
| assert(storage < _USER_STORAGE_MAX); |
| |
| h = hashmap_get(m->homes_by_name, user_name); |
| if (h) { |
| bool same; |
| |
| if (h->state != HOME_UNFIXATED) { |
| log_debug("Found an image for user %s which already has a record, skipping.", user_name); |
| return 0; /* ignore images that synthesize a user we already have a record for */ |
| } |
| |
| same = user_record_storage(h->record) == storage; |
| if (same) { |
| if (h->sysfs && sysfs) |
| same = path_equal(h->sysfs, sysfs); |
| else if (!!h->sysfs != !!sysfs) |
| same = false; |
| else { |
| const char *p; |
| |
| p = user_record_image_path(h->record); |
| same = p && path_equal(p, image_path); |
| } |
| } |
| |
| if (!same) { |
| log_debug("Found multiple images for user '%s', ignoring image '%s'.", user_name, image_path); |
| return 0; |
| } |
| } else { |
| /* Check NSS, in case there's another user or group by this name */ |
| if (getpwnam(user_name) || getgrnam(user_name)) { |
| log_debug("Found an existing user or group by name '%s', ignoring image '%s'.", user_name, image_path); |
| return 0; |
| } |
| } |
| |
| if (h && uid_is_valid(h->uid)) |
| uid = h->uid; |
| else { |
| r = manager_acquire_uid(m, start_uid, user_name, IN_SET(storage, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT) ? image_path : NULL, &uid); |
| if (r < 0) |
| return log_warning_errno(r, "Failed to acquire unused UID for %s: %m", user_name); |
| } |
| |
| hr = user_record_new(); |
| if (!hr) |
| return log_oom(); |
| |
| r = user_record_synthesize(hr, user_name, realm, image_path, storage, uid, (gid_t) uid); |
| if (r < 0) |
| return log_error_errno(r, "Failed to synthesize home record for %s (image %s): %m", user_name, image_path); |
| |
| if (h) { |
| r = home_set_record(h, hr); |
| if (r < 0) |
| return log_error_errno(r, "Failed to update home record for %s: %m", user_name); |
| } else { |
| r = home_new(m, hr, sysfs, &h); |
| if (r < 0) |
| return log_error_errno(r, "Failed to allocate new home object: %m"); |
| |
| h->state = HOME_UNFIXATED; |
| |
| log_info("Discovered new home for user %s through image %s.", user_name, image_path); |
| } |
| |
| return 1; |
| } |
| |
| int manager_augment_record_with_uid( |
| Manager *m, |
| UserRecord *hr) { |
| |
| const char *exclude_quota_path = NULL; |
| uid_t start_uid = UID_INVALID, uid; |
| int r; |
| |
| assert(m); |
| assert(hr); |
| |
| if (uid_is_valid(hr->uid)) |
| return 0; |
| |
| if (IN_SET(hr->storage, USER_CLASSIC, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT)) { |
| const char * ip; |
| |
| ip = user_record_image_path(hr); |
| if (ip) { |
| struct stat st; |
| |
| if (stat(ip, &st) < 0) { |
| if (errno != ENOENT) |
| log_warning_errno(errno, "Failed to stat(%s): %m", ip); |
| } else if (uid_is_home(st.st_uid)) { |
| start_uid = st.st_uid; |
| exclude_quota_path = ip; |
| } |
| } |
| } |
| |
| r = manager_acquire_uid(m, start_uid, hr->user_name, exclude_quota_path, &uid); |
| if (r < 0) |
| return r; |
| |
| log_debug("Acquired new UID " UID_FMT " for %s.", uid, hr->user_name); |
| |
| r = user_record_add_binding( |
| hr, |
| _USER_STORAGE_INVALID, |
| NULL, |
| SD_ID128_NULL, |
| SD_ID128_NULL, |
| SD_ID128_NULL, |
| NULL, |
| NULL, |
| UINT64_MAX, |
| NULL, |
| NULL, |
| uid, |
| (gid_t) uid); |
| if (r < 0) |
| return r; |
| |
| return 1; |
| } |
| |
| static int manager_assess_image( |
| Manager *m, |
| int dir_fd, |
| const char *dir_path, |
| const char *dentry_name) { |
| |
| char *luks_suffix, *directory_suffix; |
| _cleanup_free_ char *path = NULL; |
| struct stat st; |
| int r; |
| |
| assert(m); |
| assert(dir_path); |
| assert(dentry_name); |
| |
| luks_suffix = endswith(dentry_name, ".home"); |
| if (luks_suffix) |
| directory_suffix = NULL; |
| else |
| directory_suffix = endswith(dentry_name, ".homedir"); |
| |
| /* Early filter out: by name */ |
| if (!luks_suffix && !directory_suffix) |
| return 0; |
| |
| path = path_join(dir_path, dentry_name); |
| if (!path) |
| return log_oom(); |
| |
| /* Follow symlinks here, to allow people to link in stuff to make them available locally. */ |
| if (dir_fd >= 0) |
| r = fstatat(dir_fd, dentry_name, &st, 0); |
| else |
| r = stat(path, &st); |
| if (r < 0) |
| return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, |
| "Failed to stat() directory entry '%s', ignoring: %m", dentry_name); |
| |
| if (S_ISREG(st.st_mode)) { |
| _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; |
| |
| if (!luks_suffix) |
| return 0; |
| |
| n = strndup(dentry_name, luks_suffix - dentry_name); |
| if (!n) |
| return log_oom(); |
| |
| r = split_user_name_realm(n, &user_name, &realm); |
| if (r == -EINVAL) /* Not the right format: ignore */ |
| return 0; |
| if (r < 0) |
| return log_error_errno(r, "Failed to split image name into user name/realm: %m"); |
| |
| return manager_add_home_by_image(m, user_name, realm, path, NULL, USER_LUKS, UID_INVALID); |
| } |
| |
| if (S_ISDIR(st.st_mode)) { |
| _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; |
| _cleanup_close_ int fd = -1; |
| UserStorage storage; |
| |
| if (!directory_suffix) |
| return 0; |
| |
| n = strndup(dentry_name, directory_suffix - dentry_name); |
| if (!n) |
| return log_oom(); |
| |
| r = split_user_name_realm(n, &user_name, &realm); |
| if (r == -EINVAL) /* Not the right format: ignore */ |
| return 0; |
| if (r < 0) |
| return log_error_errno(r, "Failed to split image name into user name/realm: %m"); |
| |
| if (dir_fd >= 0) |
| fd = openat(dir_fd, dentry_name, O_DIRECTORY|O_RDONLY|O_CLOEXEC); |
| else |
| fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); |
| if (fd < 0) |
| return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, |
| "Failed to open directory '%s', ignoring: %m", path); |
| |
| if (fstat(fd, &st) < 0) |
| return log_warning_errno(errno, "Failed to fstat() %s, ignoring: %m", path); |
| |
| assert(S_ISDIR(st.st_mode)); /* Must hold, we used O_DIRECTORY above */ |
| |
| r = btrfs_is_subvol_fd(fd); |
| if (r < 0) |
| return log_warning_errno(errno, "Failed to determine whether %s is a btrfs subvolume: %m", path); |
| if (r > 0) |
| storage = USER_SUBVOLUME; |
| else { |
| struct fscrypt_policy policy; |
| |
| if (ioctl(fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { |
| |
| if (errno == ENODATA) |
| log_debug_errno(errno, "Determined %s is not fscrypt encrypted.", path); |
| else if (ERRNO_IS_NOT_SUPPORTED(errno)) |
| log_debug_errno(errno, "Determined %s is not fscrypt encrypted because kernel or file system doesn't support it.", path); |
| else |
| log_debug_errno(errno, "FS_IOC_GET_ENCRYPTION_POLICY failed with unexpected error code on %s, ignoring: %m", path); |
| |
| storage = USER_DIRECTORY; |
| } else |
| storage = USER_FSCRYPT; |
| } |
| |
| return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid); |
| } |
| |
| return 0; |
| } |
| |
| int manager_enumerate_images(Manager *m) { |
| _cleanup_closedir_ DIR *d = NULL; |
| struct dirent *de; |
| |
| assert(m); |
| |
| if (!m->scan_slash_home) |
| return 0; |
| |
| d = opendir("/home/"); |
| if (!d) |
| return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, |
| "Failed to open /home/: %m"); |
| |
| FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read /home/ directory: %m")) |
| (void) manager_assess_image(m, dirfd(d), "/home", de->d_name); |
| |
| return 0; |
| } |
| |
| static int manager_connect_bus(Manager *m) { |
| int r; |
| |
| assert(m); |
| assert(!m->bus); |
| |
| r = sd_bus_default_system(&m->bus); |
| if (r < 0) |
| return log_error_errno(r, "Failed to connect to system bus: %m"); |
| |
| r = bus_add_implementation(m->bus, &manager_object, m); |
| if (r < 0) |
| return r; |
| |
| r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.home1", 0, NULL, NULL); |
| if (r < 0) |
| return log_error_errno(r, "Failed to request name: %m"); |
| |
| r = sd_bus_attach_event(m->bus, m->event, 0); |
| if (r < 0) |
| return log_error_errno(r, "Failed to attach bus to event loop: %m"); |
| |
| (void) sd_bus_set_exit_on_disconnect(m->bus, true); |
| |
| return 0; |
| } |
| |
| static int manager_bind_varlink(Manager *m) { |
| int r; |
| |
| assert(m); |
| assert(!m->varlink_server); |
| |
| r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID); |
| if (r < 0) |
| return log_error_errno(r, "Failed to allocate varlink server object: %m"); |
| |
| varlink_server_set_userdata(m->varlink_server, m); |
| |
| r = varlink_server_bind_method_many( |
| m->varlink_server, |
| "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, |
| "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record, |
| "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships); |
| if (r < 0) |
| return log_error_errno(r, "Failed to register varlink methods: %m"); |
| |
| (void) mkdir_p("/run/systemd/userdb", 0755); |
| |
| r = varlink_server_listen_address(m->varlink_server, "/run/systemd/userdb/io.systemd.Home", 0666); |
| if (r < 0) |
| return log_error_errno(r, "Failed to bind to varlink socket: %m"); |
| |
| r = varlink_server_attach_event(m->varlink_server, m->event, SD_EVENT_PRIORITY_NORMAL); |
| if (r < 0) |
| return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); |
| |
| return 0; |
| } |
| |
| static ssize_t read_datagram(int fd, struct ucred *ret_sender, void **ret) { |
| _cleanup_free_ void *buffer = NULL; |
| ssize_t n, m; |
| |
| assert(fd >= 0); |
| assert(ret_sender); |
| assert(ret); |
| |
| n = next_datagram_size_fd(fd); |
| if (n < 0) |
| return n; |
| |
| buffer = malloc(n + 2); |
| if (!buffer) |
| return -ENOMEM; |
| |
| if (ret_sender) { |
| CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control; |
| bool found_ucred = false; |
| struct cmsghdr *cmsg; |
| struct msghdr mh; |
| struct iovec iov; |
| |
| /* Pass one extra byte, as a size check */ |
| iov = IOVEC_MAKE(buffer, n + 1); |
| |
| mh = (struct msghdr) { |
| .msg_iov = &iov, |
| .msg_iovlen = 1, |
| .msg_control = &control, |
| .msg_controllen = sizeof(control), |
| }; |
| |
| m = recvmsg_safe(fd, &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC); |
| if (m < 0) |
| return m; |
| |
| cmsg_close_all(&mh); |
| |
| /* Ensure the size matches what we determined before */ |
| if (m != n) |
| return -EMSGSIZE; |
| |
| CMSG_FOREACH(cmsg, &mh) |
| if (cmsg->cmsg_level == SOL_SOCKET && |
| cmsg->cmsg_type == SCM_CREDENTIALS && |
| cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred))) { |
| |
| memcpy(ret_sender, CMSG_DATA(cmsg), sizeof(struct ucred)); |
| found_ucred = true; |
| } |
| |
| if (!found_ucred) |
| *ret_sender = (struct ucred) { |
| .pid = 0, |
| .uid = UID_INVALID, |
| .gid = GID_INVALID, |
| }; |
| } else { |
| m = recv(fd, buffer, n + 1, MSG_DONTWAIT); |
| if (m < 0) |
| return -errno; |
| |
| /* Ensure the size matches what we determined before */ |
| if (m != n) |
| return -EMSGSIZE; |
| } |
| |
| /* For safety reasons: let's always NUL terminate. */ |
| ((char*) buffer)[n] = 0; |
| *ret = TAKE_PTR(buffer); |
| |
| return 0; |
| } |
| |
| static int on_notify_socket(sd_event_source *s, int fd, uint32_t revents, void *userdata) { |
| _cleanup_strv_free_ char **l = NULL; |
| _cleanup_free_ void *datagram = NULL; |
| struct ucred sender; |
| Manager *m = userdata; |
| ssize_t n; |
| Home *h; |
| |
| assert(s); |
| assert(m); |
| |
| n = read_datagram(fd, &sender, &datagram); |
| if (IN_SET(n, -EAGAIN, -EINTR)) |
| return 0; |
| if (n < 0) |
| return log_error_errno(n, "Failed to read notify datagram: %m"); |
| |
| if (sender.pid <= 0) { |
| log_warning("Received notify datagram without valid sender PID, ignoring."); |
| return 0; |
| } |
| |
| h = hashmap_get(m->homes_by_worker_pid, PID_TO_PTR(sender.pid)); |
| if (!h) { |
| log_warning("Received notify datagram of unknown process, ignoring."); |
| return 0; |
| } |
| |
| l = strv_split(datagram, "\n"); |
| if (!l) |
| return log_oom(); |
| |
| home_process_notify(h, l); |
| return 0; |
| } |
| |
| static int manager_listen_notify(Manager *m) { |
| _cleanup_close_ int fd = -1; |
| union sockaddr_union sa = { |
| .un.sun_family = AF_UNIX, |
| .un.sun_path = "/run/systemd/home/notify", |
| }; |
| int r; |
| |
| assert(m); |
| assert(!m->notify_socket_event_source); |
| |
| fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); |
| if (fd < 0) |
| return log_error_errno(errno, "Failed to create listening socket: %m"); |
| |
| (void) mkdir_parents(sa.un.sun_path, 0755); |
| (void) sockaddr_un_unlink(&sa.un); |
| |
| if (bind(fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0) |
| return log_error_errno(errno, "Failed to bind to socket: %m"); |
| |
| r = setsockopt_int(fd, SOL_SOCKET, SO_PASSCRED, true); |
| if (r < 0) |
| return r; |
| |
| r = sd_event_add_io(m->event, &m->notify_socket_event_source, fd, EPOLLIN, on_notify_socket, m); |
| if (r < 0) |
| return log_error_errno(r, "Failed to allocate event source for notify socket: %m"); |
| |
| (void) sd_event_source_set_description(m->notify_socket_event_source, "notify-socket"); |
| |
| /* Make sure we process sd_notify() before SIGCHLD for any worker, so that we always know the error |
| * number of a client before it exits. */ |
| r = sd_event_source_set_priority(m->notify_socket_event_source, SD_EVENT_PRIORITY_NORMAL - 5); |
| if (r < 0) |
| return log_error_errno(r, "Failed to alter priority of NOTIFY_SOCKET event source: %m"); |
| |
| r = sd_event_source_set_io_fd_own(m->notify_socket_event_source, true); |
| if (r < 0) |
| return log_error_errno(r, "Failed to pass ownership of notify socket: %m"); |
| |
| return TAKE_FD(fd); |
| } |
| |
| static int manager_add_device(Manager *m, sd_device *d) { |
| _cleanup_free_ char *user_name = NULL, *realm = NULL, *node = NULL; |
| const char *tabletype, *parttype, *partname, *partuuid, *sysfs; |
| sd_id128_t id; |
| int r; |
| |
| assert(m); |
| assert(d); |
| |
| r = sd_device_get_syspath(d, &sysfs); |
| if (r < 0) |
| return log_error_errno(r, "Failed to acquire sysfs path of device: %m"); |
| |
| r = sd_device_get_property_value(d, "ID_PART_TABLE_TYPE", &tabletype); |
| if (r == -ENOENT) |
| return 0; |
| if (r < 0) |
| return log_error_errno(r, "Failed to acquire ID_PART_TABLE_TYPE device property, ignoring: %m"); |
| |
| if (!streq(tabletype, "gpt")) { |
| log_debug("Found partition (%s) on non-GPT table, ignoring.", sysfs); |
| return 0; |
| } |
| |
| r = sd_device_get_property_value(d, "ID_PART_ENTRY_TYPE", &parttype); |
| if (r == -ENOENT) |
| return 0; |
| if (r < 0) |
| return log_error_errno(r, "Failed to acquire ID_PART_ENTRY_TYPE device property, ignoring: %m"); |
| r = sd_id128_from_string(parttype, &id); |
| if (r < 0) |
| return log_debug_errno(r, "Failed to parse ID_PART_ENTRY_TYPE field '%s', ignoring: %m", parttype); |
| if (!sd_id128_equal(id, GPT_USER_HOME)) { |
| log_debug("Found partition (%s) we don't care about, ignoring.", sysfs); |
| return 0; |
| } |
| |
| r = sd_device_get_property_value(d, "ID_PART_ENTRY_NAME", &partname); |
| if (r < 0) |
| return log_warning_errno(r, "Failed to acquire ID_PART_ENTRY_NAME device property, ignoring: %m"); |
| |
| r = split_user_name_realm(partname, &user_name, &realm); |
| if (r == -EINVAL) |
| return log_warning_errno(r, "Found partition with correct partition type but a non-parsable partition name '%s', ignoring.", partname); |
| if (r < 0) |
| return log_error_errno(r, "Failed to validate partition name '%s': %m", partname); |
| |
| r = sd_device_get_property_value(d, "ID_FS_UUID", &partuuid); |
| if (r < 0) |
| return log_warning_errno(r, "Failed to acquire ID_FS_UUID device property, ignoring: %m"); |
| |
| r = sd_id128_from_string(partuuid, &id); |
| if (r < 0) |
| return log_warning_errno(r, "Failed to parse ID_FS_UUID field '%s', ignoring: %m", partuuid); |
| |
| if (asprintf(&node, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(id)) < 0) |
| return log_oom(); |
| |
| return manager_add_home_by_image(m, user_name, realm, node, sysfs, USER_LUKS, UID_INVALID); |
| } |
| |
| static int manager_on_device(sd_device_monitor *monitor, sd_device *d, void *userdata) { |
| Manager *m = userdata; |
| int r; |
| |
| assert(m); |
| assert(d); |
| |
| if (device_for_action(d, DEVICE_ACTION_REMOVE)) { |
| const char *sysfs; |
| Home *h; |
| |
| r = sd_device_get_syspath(d, &sysfs); |
| if (r < 0) { |
| log_warning_errno(r, "Failed to acquire sysfs path from device: %m"); |
| return 0; |
| } |
| |
| log_info("block device %s has been removed.", sysfs); |
| |
| /* Let's see if we previously synthesized a home record from this device, if so, let's just |
| * revalidate that. Otherwise let's revalidate them all, but asynchronously. */ |
| h = hashmap_get(m->homes_by_sysfs, sysfs); |
| if (h) |
| manager_revalidate_image(m, h); |
| else |
| manager_enqueue_gc(m, NULL); |
| } else |
| (void) manager_add_device(m, d); |
| |
| (void) bus_manager_emit_auto_login_changed(m); |
| return 0; |
| } |
| |
| static int manager_watch_devices(Manager *m) { |
| int r; |
| |
| assert(m); |
| assert(!m->device_monitor); |
| |
| r = sd_device_monitor_new(&m->device_monitor); |
| if (r < 0) |
| return log_error_errno(r, "Failed to allocate device monitor: %m"); |
| |
| r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "block", NULL); |
| if (r < 0) |
| return log_error_errno(r, "Failed to configure device monitor match: %m"); |
| |
| r = sd_device_monitor_attach_event(m->device_monitor, m->event); |
| if (r < 0) |
| return log_error_errno(r, "Failed to attach device monitor to event loop: %m"); |
| |
| r = sd_device_monitor_start(m->device_monitor, manager_on_device, m); |
| if (r < 0) |
| return log_error_errno(r, "Failed to start device monitor: %m"); |
| |
| return 0; |
| } |
| |
| static int manager_enumerate_devices(Manager *m) { |
| _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; |
| sd_device *d; |
| int r; |
| |
| assert(m); |
| |
| r = sd_device_enumerator_new(&e); |
| if (r < 0) |
| return r; |
| |
| r = sd_device_enumerator_add_match_subsystem(e, "block", true); |
| if (r < 0) |
| return r; |
| |
| FOREACH_DEVICE(e, d) |
| (void) manager_add_device(m, d); |
| |
| return 0; |
| } |
| |
| static int manager_load_key_pair(Manager *m) { |
| _cleanup_(fclosep) FILE *f = NULL; |
| struct stat st; |
| int r; |
| |
| assert(m); |
| |
| if (m->private_key) { |
| EVP_PKEY_free(m->private_key); |
| m->private_key = NULL; |
| } |
| |
| r = search_and_fopen_nulstr("local.private", "re", NULL, KEY_PATHS_NULSTR, &f); |
| if (r == -ENOENT) |
| return 0; |
| if (r < 0) |
| return log_error_errno(r, "Failed to read private key file: %m"); |
| |
| if (fstat(fileno(f), &st) < 0) |
| return log_error_errno(errno, "Failed to stat private key file: %m"); |
| |
| r = stat_verify_regular(&st); |
| if (r < 0) |
| return log_error_errno(r, "Private key file is not regular: %m"); |
| |
| if (st.st_uid != 0 || (st.st_mode & 0077) != 0) |
| return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Private key file is readable by more than the root user"); |
| |
| m->private_key = PEM_read_PrivateKey(f, NULL, NULL, NULL); |
| if (!m->private_key) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to load private key pair"); |
| |
| log_info("Successfully loaded private key pair."); |
| |
| return 1; |
| } |
| |
| DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY_CTX*, EVP_PKEY_CTX_free); |
| |
| static int manager_generate_key_pair(Manager *m) { |
| _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; |
| _cleanup_(unlink_and_freep) char *temp_public = NULL, *temp_private = NULL; |
| _cleanup_fclose_ FILE *fpublic = NULL, *fprivate = NULL; |
| int r; |
| |
| if (m->private_key) { |
| EVP_PKEY_free(m->private_key); |
| m->private_key = NULL; |
| } |
| |
| ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); |
| if (!ctx) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate Ed25519 key generation context."); |
| |
| if (EVP_PKEY_keygen_init(ctx) <= 0) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize Ed25519 key generation context."); |
| |
| log_info("Generating key pair for signing local user identity records."); |
| |
| if (EVP_PKEY_keygen(ctx, &m->private_key) <= 0) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate Ed25519 key pair"); |
| |
| log_info("Successfully created Ed25519 key pair."); |
| |
| (void) mkdir_p("/var/lib/systemd/home", 0755); |
| |
| /* Write out public key (note that we only do that as a help to the user, we don't make use of this ever */ |
| r = fopen_temporary("/var/lib/systemd/home/local.public", &fpublic, &temp_public); |
| if (r < 0) |
| return log_error_errno(errno, "Failed to open key file for writing: %m"); |
| |
| if (PEM_write_PUBKEY(fpublic, m->private_key) <= 0) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key."); |
| |
| r = fflush_sync_and_check(fpublic); |
| if (r < 0) |
| return log_error_errno(r, "Failed to write private key: %m"); |
| |
| fpublic = safe_fclose(fpublic); |
| |
| /* Write out the private key (this actually writes out both private and public, OpenSSL is confusing) */ |
| r = fopen_temporary("/var/lib/systemd/home/local.private", &fprivate, &temp_private); |
| if (r < 0) |
| return log_error_errno(errno, "Failed to open key file for writing: %m"); |
| |
| if (PEM_write_PrivateKey(fprivate, m->private_key, NULL, NULL, 0, NULL, 0) <= 0) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write private key pair."); |
| |
| r = fflush_sync_and_check(fprivate); |
| if (r < 0) |
| return log_error_errno(r, "Failed to write private key: %m"); |
| |
| fprivate = safe_fclose(fprivate); |
| |
| /* Both are written now, move them into place */ |
| |
| if (rename(temp_public, "/var/lib/systemd/home/local.public") < 0) |
| return log_error_errno(errno, "Failed to move public key file into place: %m"); |
| temp_public = mfree(temp_public); |
| |
| if (rename(temp_private, "/var/lib/systemd/home/local.private") < 0) { |
| (void) unlink_noerrno("/var/lib/systemd/home/local.public"); /* try to remove the file we already created */ |
| return log_error_errno(errno, "Failed to move private key file into place: %m"); |
| } |
| temp_private = mfree(temp_private); |
| |
| r = fsync_path_at(AT_FDCWD, "/var/lib/systemd/home/"); |
| if (r < 0) |
| log_warning_errno(r, "Failed to sync /var/lib/systemd/home/, ignoring: %m"); |
| |
| return 1; |
| } |
| |
| int manager_acquire_key_pair(Manager *m) { |
| int r; |
| |
| assert(m); |
| |
| /* Already there? */ |
| if (m->private_key) |
| return 1; |
| |
| /* First try to load key off disk */ |
| r = manager_load_key_pair(m); |
| if (r != 0) |
| return r; |
| |
| /* Didn't work, generate a new one */ |
| return manager_generate_key_pair(m); |
| } |
| |
| int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error) { |
| int r; |
| |
| assert(m); |
| assert(u); |
| assert(ret); |
| |
| r = manager_acquire_key_pair(m); |
| if (r < 0) |
| return r; |
| if (r == 0) |
| return sd_bus_error_setf(error, BUS_ERROR_NO_PRIVATE_KEY, "Can't sign without local key."); |
| |
| return user_record_sign(u, m->private_key, ret); |
| } |
| |
| DEFINE_PRIVATE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free); |
| DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY*, EVP_PKEY_free); |
| |
| static int manager_load_public_key_one(Manager *m, const char *path) { |
| _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; |
| _cleanup_fclose_ FILE *f = NULL; |
| _cleanup_free_ char *fn = NULL; |
| struct stat st; |
| int r; |
| |
| assert(m); |
| |
| if (streq(basename(path), "local.public")) /* we already loaded the private key, which includes the public one */ |
| return 0; |
| |
| f = fopen(path, "re"); |
| if (!f) { |
| if (errno == ENOENT) |
| return 0; |
| |
| return log_error_errno(errno, "Failed to open public key %s: %m", path); |
| } |
| |
| if (fstat(fileno(f), &st) < 0) |
| return log_error_errno(errno, "Failed to stat public key %s: %m", path); |
| |
| r = stat_verify_regular(&st); |
| if (r < 0) |
| return log_error_errno(r, "Public key file %s is not a regular file: %m", path); |
| |
| if (st.st_uid != 0 || (st.st_mode & 0022) != 0) |
| return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Public key file %s is writable by more than the root user, refusing.", path); |
| |
| r = hashmap_ensure_allocated(&m->public_keys, &public_key_hash_ops); |
| if (r < 0) |
| return log_oom(); |
| |
| pkey = PEM_read_PUBKEY(f, &pkey, NULL, NULL); |
| if (!pkey) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key file %s.", path); |
| |
| fn = strdup(basename(path)); |
| if (!fn) |
| return log_oom(); |
| |
| r = hashmap_put(m->public_keys, fn, pkey); |
| if (r < 0) |
| return log_error_errno(r, "Failed to add public key to set: %m"); |
| |
| TAKE_PTR(fn); |
| TAKE_PTR(pkey); |
| |
| return 0; |
| } |
| |
| static int manager_load_public_keys(Manager *m) { |
| _cleanup_strv_free_ char **files = NULL; |
| char **i; |
| int r; |
| |
| assert(m); |
| |
| m->public_keys = hashmap_free(m->public_keys); |
| |
| r = conf_files_list_nulstr( |
| &files, |
| ".public", |
| NULL, |
| CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, |
| KEY_PATHS_NULSTR); |
| if (r < 0) |
| return log_error_errno(r, "Failed to assemble list of public key directories: %m"); |
| |
| STRV_FOREACH(i, files) |
| (void) manager_load_public_key_one(m, *i); |
| |
| return 0; |
| } |
| |
| int manager_startup(Manager *m) { |
| int r; |
| |
| assert(m); |
| |
| r = manager_listen_notify(m); |
| if (r < 0) |
| return r; |
| |
| r = manager_connect_bus(m); |
| if (r < 0) |
| return r; |
| |
| r = manager_bind_varlink(m); |
| if (r < 0) |
| return r; |
| |
| r = manager_load_key_pair(m); /* only try to load it, don't generate any */ |
| if (r < 0) |
| return r; |
| |
| r = manager_load_public_keys(m); |
| if (r < 0) |
| return r; |
| |
| manager_watch_home(m); |
| (void) manager_watch_devices(m); |
| |
| (void) manager_enumerate_records(m); |
| (void) manager_enumerate_images(m); |
| (void) manager_enumerate_devices(m); |
| |
| /* Let's clean up home directories whose devices got removed while we were not running */ |
| (void) manager_enqueue_gc(m, NULL); |
| |
| return 0; |
| } |
| |
| void manager_revalidate_image(Manager *m, Home *h) { |
| int r; |
| |
| assert(m); |
| assert(h); |
| |
| /* Frees an automatically discovered image, if it's synthetic and its image disappeared. Unmounts any |
| * image if it's mounted but it's image vanished. */ |
| |
| if (h->current_operation || !ordered_set_isempty(h->pending_operations)) |
| return; |
| |
| if (h->state == HOME_UNFIXATED) { |
| r = user_record_test_image_path(h->record); |
| if (r < 0) |
| log_warning_errno(r, "Can't determine if image of %s exists, freeing unfixated user: %m", h->user_name); |
| else if (r == USER_TEST_ABSENT) |
| log_info("Image for %s disappeared, freeing unfixated user.", h->user_name); |
| else |
| return; |
| |
| home_free(h); |
| |
| } else if (h->state < 0) { |
| |
| r = user_record_test_home_directory(h->record); |
| if (r < 0) { |
| log_warning_errno(r, "Unable to determine state of home directory, ignoring: %m"); |
| return; |
| } |
| |
| if (r == USER_TEST_MOUNTED) { |
| r = user_record_test_image_path(h->record); |
| if (r < 0) { |
| log_warning_errno(r, "Unable to determine state of image path, ignoring: %m"); |
| return; |
| } |
| |
| if (r == USER_TEST_ABSENT) { |
| _cleanup_(operation_unrefp) Operation *o = NULL; |
| |
| log_notice("Backing image disappeared while home directory %s was mounted, unmounting it forcibly.", h->user_name); |
| /* Wowza, the thing is mounted, but the device is gone? Act on it. */ |
| |
| r = home_killall(h); |
| if (r < 0) |
| log_warning_errno(r, "Failed to kill processes of user %s, ignoring: %m", h->user_name); |
| |
| /* We enqueue the operation here, after all the home directory might |
| * currently already run some operation, and we can deactivate it only after |
| * that's complete. */ |
| o = operation_new(OPERATION_DEACTIVATE_FORCE, NULL); |
| if (!o) { |
| log_oom(); |
| return; |
| } |
| |
| r = home_schedule_operation(h, o, NULL); |
| if (r < 0) |
| log_warning_errno(r, "Failed to enqueue forced home directory %s deactivation, ignoring: %m", h->user_name); |
| } |
| } |
| } |
| } |
| |
| int manager_gc_images(Manager *m) { |
| Home *h; |
| |
| assert_se(m); |
| |
| if (m->gc_focus) { |
| /* Focus on a specific home */ |
| |
| h = TAKE_PTR(m->gc_focus); |
| manager_revalidate_image(m, h); |
| } else { |
| /* Gc all */ |
| Iterator i; |
| |
| HASHMAP_FOREACH(h, m->homes_by_name, i) |
| manager_revalidate_image(m, h); |
| } |
| |
| return 0; |
| } |
| |
| static int on_deferred_rescan(sd_event_source *s, void *userdata) { |
| Manager *m = userdata; |
| |
| assert(m); |
| |
| m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source); |
| |
| manager_enumerate_devices(m); |
| manager_enumerate_images(m); |
| return 0; |
| } |
| |
| int manager_enqueue_rescan(Manager *m) { |
| int r; |
| |
| assert(m); |
| |
| if (m->deferred_rescan_event_source) |
| return 0; |
| |
| if (!m->event) |
| return 0; |
| |
| if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) |
| return 0; |
| |
| r = sd_event_add_defer(m->event, &m->deferred_rescan_event_source, on_deferred_rescan, m); |
| if (r < 0) |
| return log_error_errno(r, "Failed to allocate rescan event source: %m"); |
| |
| r = sd_event_source_set_priority(m->deferred_rescan_event_source, SD_EVENT_PRIORITY_IDLE+1); |
| if (r < 0) |
| log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); |
| |
| (void) sd_event_source_set_description(m->deferred_rescan_event_source, "deferred-rescan"); |
| return 1; |
| } |
| |
| static int on_deferred_gc(sd_event_source *s, void *userdata) { |
| Manager *m = userdata; |
| |
| assert(m); |
| |
| m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source); |
| |
| manager_gc_images(m); |
| return 0; |
| } |
| |
| int manager_enqueue_gc(Manager *m, Home *focus) { |
| int r; |
| |
| assert(m); |
| |
| /* This enqueues a request to GC dead homes. It may be called with focus=NULL in which case all homes |
| * will be scanned, or with the parameter set, in which case only that home is checked. */ |
| |
| if (!m->event) |
| return 0; |
| |
| if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) |
| return 0; |
| |
| /* If a focus home is specified, then remember to focus just on this home. Otherwise invalidate any |
| * focus that might be set to look at all homes. */ |
| |
| if (m->deferred_gc_event_source) { |
| if (m->gc_focus != focus) /* not the same focus, then look at everything */ |
| m->gc_focus = NULL; |
| |
| return 0; |
| } else |
| m->gc_focus = focus; /* start focused */ |
| |
| r = sd_event_add_defer(m->event, &m->deferred_gc_event_source, on_deferred_gc, m); |
| if (r < 0) |
| return log_error_errno(r, "Failed to allocate GC event source: %m"); |
| |
| r = sd_event_source_set_priority(m->deferred_gc_event_source, SD_EVENT_PRIORITY_IDLE); |
| if (r < 0) |
| log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); |
| |
| (void) sd_event_source_set_description(m->deferred_gc_event_source, "deferred-gc"); |
| return 1; |
| } |