From 7062ae5ab9fc1df1e23057a39c90890a4f89b4ef Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 19 Aug 2019 18:06:03 +0200 Subject: [PATCH 001/109] ordered-set: add ordered_set_first() helper --- src/basic/ordered-set.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/basic/ordered-set.h b/src/basic/ordered-set.h index ba43451e27d4a..383a729cab869 100644 --- a/src/basic/ordered-set.h +++ b/src/basic/ordered-set.h @@ -50,6 +50,10 @@ static inline void* ordered_set_remove(OrderedSet *s, void *p) { return ordered_hashmap_remove((OrderedHashmap*) s, p); } +static inline void* ordered_set_first(OrderedSet *s) { + return ordered_hashmap_first((OrderedHashmap*) s); +} + static inline void* ordered_set_steal_first(OrderedSet *s) { return ordered_hashmap_steal_first((OrderedHashmap*) s); } From f39bbe7554308132b84aa0d620709332bdc21ba9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 15 Jul 2019 13:32:03 +0200 Subject: [PATCH 002/109] errno: add new ERRNO_IS_NOT_SUPPORTED() and ERRNO_IS_DISK_SPACE() helpers --- src/basic/errno-util.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/basic/errno-util.h b/src/basic/errno-util.h index 6053cde62dd42..50afe278def74 100644 --- a/src/basic/errno-util.h +++ b/src/basic/errno-util.h @@ -86,3 +86,19 @@ static inline bool ERRNO_IS_RESOURCE(int r) { ENFILE, ENOMEM); } + +/* Three different errors for "operation/system call/ioctl not supported" */ +static inline bool ERRNO_IS_NOT_SUPPORTED(int r) { + return IN_SET(abs(r), + EOPNOTSUPP, + ENOTTY, + ENOSYS); +} + +/* Three difference errors for "not enough disk space" */ +static inline bool ERRNO_IS_DISK_SPACE(int r) { + return IN_SET(abs(r), + ENOSPC, + EDQUOT, + EFBIG); +} From 2fcfeeaeac3ed1511bea075df2de4a830f4e3e99 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Aug 2019 16:33:04 +0200 Subject: [PATCH 003/109] errno-util: add ERRNO_IS_PRIVILEGE() helper --- src/basic/errno-util.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/basic/errno-util.h b/src/basic/errno-util.h index 50afe278def74..670772623b299 100644 --- a/src/basic/errno-util.h +++ b/src/basic/errno-util.h @@ -102,3 +102,10 @@ static inline bool ERRNO_IS_DISK_SPACE(int r) { EDQUOT, EFBIG); } + +/* Two different errors for access problems */ +static inline bool ERRNO_IS_PRIVILEGE(int r) { + return IN_SET(abs(r), + EACCES, + EPERM); +} From e94058bb51dd1858e840028bd92cca1dc9273861 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Aug 2019 19:53:17 +0200 Subject: [PATCH 004/109] memory-util: introduce erase_and_free() helper --- src/basic/memory-util.h | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/basic/memory-util.h b/src/basic/memory-util.h index 9cb8ac3c10fca..66c64bdf0e779 100644 --- a/src/basic/memory-util.h +++ b/src/basic/memory-util.h @@ -79,14 +79,21 @@ static inline void* explicit_bzero_safe(void *p, size_t l) { void *explicit_bzero_safe(void *p, size_t l); #endif -static inline void erase_and_freep(void *p) { - void *ptr = *(void**) p; +static inline void* erase_and_free(void *p) { + size_t l; + + if (!p) + return NULL; + + l = malloc_usable_size(p); + explicit_bzero_safe(p, l); + free(p); - if (ptr) { - size_t l = malloc_usable_size(ptr); - explicit_bzero_safe(ptr, l); - free(ptr); - } + return NULL; +} + +static inline void erase_and_freep(void *p) { + erase_and_free(*(void**) p); } /* Use with _cleanup_ to erase a single 'char' when leaving scope */ From d4b1045505737754c998c133a98508513ee1ed79 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 11 Jul 2019 14:50:26 +0200 Subject: [PATCH 005/109] string-util: readd string_erase() This was dropped in 8e27167cc9b8beda2bf49789b15f0b0301b95d17, but is actually useful for some usecases still. --- src/basic/string-util.c | 10 ++++++++++ src/basic/string-util.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 9586b3940eb93..418a50e2bc7e3 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1050,3 +1050,13 @@ bool string_is_safe(const char *p) { return true; } + +char* string_erase(char *x) { + if (!x) + return NULL; + + /* A delicious drop of snake-oil! To be called on memory where we stored passphrases or so, after we + * used them. */ + explicit_bzero_safe(x, strlen(x)); + return x; +} diff --git a/src/basic/string-util.h b/src/basic/string-util.h index 76767afcac49a..ddbb87e1fcf3e 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -263,3 +263,5 @@ static inline char* str_realloc(char **p) { return (*p = t); } + +char* string_erase(char *x); From b0dfc2aa40029d4dae0f97186ea1e7337a69037a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 23 Dec 2018 19:23:58 +0100 Subject: [PATCH 006/109] loop-util: accept loopback flags when creating loopback device This way callers can choose if they want partition scanning or not. --- src/core/namespace.c | 2 ++ src/dissect/dissect.c | 5 +++-- src/nspawn/nspawn.c | 2 +- src/portable/portable.c | 4 +++- src/shared/loop-util.c | 9 +++++---- src/shared/loop-util.h | 4 ++-- src/shared/machine-image.c | 5 +++-- src/test/test-dissect-image.c | 3 ++- 8 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index 973b64007cf87..89b776f175dd7 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include +#include #include #include #include @@ -1215,6 +1216,7 @@ int setup_namespace( r = loop_device_make_by_path(root_image, dissect_image_flags & DISSECT_IMAGE_READ_ONLY ? O_RDONLY : O_RDWR, + LO_FLAGS_PARTSCAN, &loop_device); if (r < 0) return log_debug_errno(r, "Failed to create loop device for root image: %m"); diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 50de0afce6f00..c1be6c034c561 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -1,8 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include -#include #include +#include +#include #include "architecture.h" #include "dissect-image.h" @@ -171,7 +172,7 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - r = loop_device_make_by_path(arg_image, (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, &d); + r = loop_device_make_by_path(arg_image, (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, LO_FLAGS_PARTSCAN, &d); if (r < 0) return log_error_errno(r, "Failed to set up loopback device: %m"); diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 2aec8041f0079..a96ee1ccc2af7 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -4967,7 +4967,7 @@ static int run(int argc, char *argv[]) { goto finish; } - r = loop_device_make_by_path(arg_image, arg_read_only ? O_RDONLY : O_RDWR, &loop); + r = loop_device_make_by_path(arg_image, arg_read_only ? O_RDONLY : O_RDWR, LO_FLAGS_PARTSCAN, &loop); if (r < 0) { log_error_errno(r, "Failed to set up loopback block device: %m"); goto finish; diff --git a/src/portable/portable.c b/src/portable/portable.c index d37880cfd1d8f..15a4c2c618bde 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ +#include + #include "bus-common-errors.h" #include "bus-error.h" #include "conf-files.h" @@ -359,7 +361,7 @@ static int portable_extract_by_path( assert(path); - r = loop_device_make_by_path(path, O_RDONLY, &d); + r = loop_device_make_by_path(path, O_RDONLY, LO_FLAGS_PARTSCAN, &d); if (r == -EISDIR) { /* We can't turn this into a loop-back block device, and this returns EISDIR? Then this is a directory * tree and not a raw device. It's easy then. */ diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 22525d5c63303..6a86d6d0f1ea5 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -11,9 +11,10 @@ #include "loop-util.h" #include "stat-util.h" -int loop_device_make(int fd, int open_flags, LoopDevice **ret) { +int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret) { const struct loop_info64 info = { - .lo_flags = LO_FLAGS_AUTOCLEAR|LO_FLAGS_PARTSCAN|(open_flags == O_RDONLY ? LO_FLAGS_READ_ONLY : 0), + /* Use the specified flags, but configure the read-only flag from the open flags, and force autoclear */ + .lo_flags = (loop_flags & ~LO_FLAGS_READ_ONLY) | ((loop_flags & O_ACCMODE) == O_RDONLY ? LO_FLAGS_READ_ONLY : 0) | LO_FLAGS_AUTOCLEAR, }; _cleanup_close_ int control = -1, loop = -1; @@ -104,7 +105,7 @@ int loop_device_make(int fd, int open_flags, LoopDevice **ret) { return d->fd; } -int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret) { +int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret) { _cleanup_close_ int fd = -1; assert(path); @@ -115,7 +116,7 @@ int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret) if (fd < 0) return -errno; - return loop_device_make(fd, open_flags, ret); + return loop_device_make(fd, open_flags, loop_flags, ret); } LoopDevice* loop_device_unref(LoopDevice *d) { diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h index d78466c5ee683..c881a43cdc2cd 100644 --- a/src/shared/loop-util.h +++ b/src/shared/loop-util.h @@ -14,8 +14,8 @@ struct LoopDevice { bool relinquished; }; -int loop_device_make(int fd, int open_flags, LoopDevice **ret); -int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret); +int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret); +int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret); LoopDevice* loop_device_unref(LoopDevice *d); DEFINE_TRIVIAL_CLEANUP_FUNC(LoopDevice*, loop_device_unref); diff --git a/src/shared/machine-image.c b/src/shared/machine-image.c index 7007374192796..4e0bea41d1a57 100644 --- a/src/shared/machine-image.c +++ b/src/shared/machine-image.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -10,7 +12,6 @@ #include #include #include -#include #include "alloc-util.h" #include "btrfs-util.h" @@ -1168,7 +1169,7 @@ int image_read_metadata(Image *i) { _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; - r = loop_device_make_by_path(i->path, O_RDONLY, &d); + r = loop_device_make_by_path(i->path, O_RDONLY, LO_FLAGS_PARTSCAN, &d); if (r < 0) return r; diff --git a/src/test/test-dissect-image.c b/src/test/test-dissect-image.c index 7b32e8373f920..12685dad1373d 100644 --- a/src/test/test-dissect-image.c +++ b/src/test/test-dissect-image.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include +#include #include #include "dissect-image.h" @@ -21,7 +22,7 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - r = loop_device_make_by_path(argv[1], O_RDONLY, &d); + r = loop_device_make_by_path(argv[1], O_RDONLY, LO_FLAGS_PARTSCAN, &d); if (r < 0) { log_error_errno(r, "Failed to set up loopback device: %m"); return EXIT_FAILURE; From 682b899c4f8a5e0bbc2ac35f63137dacfc96fd99 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Dec 2018 16:56:14 +0100 Subject: [PATCH 007/109] loop-util: add API to refresh loopback device size and opening existing loopback block devices --- src/shared/loop-util.c | 50 ++++++++++++++++++++++++++++++++++++++++++ src/shared/loop-util.h | 3 +++ 2 files changed, 53 insertions(+) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 6a86d6d0f1ea5..1055ad559a3da 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -158,3 +158,53 @@ void loop_device_relinquish(LoopDevice *d) { d->relinquished = true; } + +int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { + _cleanup_close_ int loop_fd = -1; + _cleanup_free_ char *p = NULL; + struct stat st; + LoopDevice *d; + + assert(loop_path); + assert(ret); + + loop_fd = open(loop_path, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|open_flags); + if (loop_fd < 0) + return -errno; + + if (fstat(loop_fd, &st) < 0) + return -errno; + + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + p = strdup(loop_path); + if (!p) + return -ENOMEM; + + d = new(LoopDevice, 1); + if (!d) + return -ENOMEM; + + *d = (LoopDevice) { + .fd = TAKE_FD(loop_fd), + .nr = -1, + .node = TAKE_PTR(p), + .relinquished = true, /* It's not ours, don't try to destroy it when this object is freed */ + }; + + *ret = d; + return d->fd; +} + +int loop_device_refresh_size(LoopDevice *d) { + assert(d); + + if (d->fd < 0) + return -EBADF; + + if (ioctl(d->fd, LOOP_SET_CAPACITY) < 0) + return -errno; + + return 0; +} diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h index c881a43cdc2cd..80a114f9d996a 100644 --- a/src/shared/loop-util.h +++ b/src/shared/loop-util.h @@ -16,8 +16,11 @@ struct LoopDevice { int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret); int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret); +int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret); LoopDevice* loop_device_unref(LoopDevice *d); DEFINE_TRIVIAL_CLEANUP_FUNC(LoopDevice*, loop_device_unref); void loop_device_relinquish(LoopDevice *d); + +int loop_device_refresh_size(LoopDevice *d); From 260101e291c59e8ed266389d3f5cb5e853397e3b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 17:57:29 +0200 Subject: [PATCH 008/109] loop-util: allow creating loopback block devices with offset/length --- src/shared/loop-util.c | 60 +++++++++++++++++++++++++----------------- src/shared/loop-util.h | 6 ++++- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 1055ad559a3da..c2ea476a80c85 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -11,15 +11,18 @@ #include "loop-util.h" #include "stat-util.h" -int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret) { - const struct loop_info64 info = { - /* Use the specified flags, but configure the read-only flag from the open flags, and force autoclear */ - .lo_flags = (loop_flags & ~LO_FLAGS_READ_ONLY) | ((loop_flags & O_ACCMODE) == O_RDONLY ? LO_FLAGS_READ_ONLY : 0) | LO_FLAGS_AUTOCLEAR, - }; +int loop_device_make_full( + int fd, + int open_flags, + uint64_t offset, + uint64_t size, + uint32_t loop_flags, + LoopDevice **ret) { _cleanup_close_ int control = -1, loop = -1; _cleanup_free_ char *loopdev = NULL; unsigned n_attempts = 0; + struct loop_info64 info; struct stat st; LoopDevice *d; int nr, r; @@ -32,32 +35,34 @@ int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **r return -errno; if (S_ISBLK(st.st_mode)) { - int copy; + if (offset == 0 && IN_SET(size, 0, UINT64_MAX)) { + int copy; - /* If this is already a block device, store a copy of the fd as it is */ + /* If this is already a block device, store a copy of the fd as it is */ - copy = fcntl(fd, F_DUPFD_CLOEXEC, 3); - if (copy < 0) - return -errno; + copy = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (copy < 0) + return -errno; - d = new0(LoopDevice, 1); - if (!d) - return -ENOMEM; + d = new0(LoopDevice, 1); + if (!d) + return -ENOMEM; - *d = (LoopDevice) { - .fd = copy, - .nr = -1, - .relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */ - }; + *d = (LoopDevice) { + .fd = copy, + .nr = -1, + .relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */ + }; - *ret = d; - return d->fd; + *ret = d; + return d->fd; + } + } else { + r = stat_verify_regular(&st); + if (r < 0) + return r; } - r = stat_verify_regular(&st); - if (r < 0) - return r; - control = open("/dev/loop-control", O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); if (control < 0) return -errno; @@ -88,6 +93,13 @@ int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **r loop = safe_close(loop); } + info = (struct loop_info64) { + /* Use the specified flags, but configure the read-only flag from the open flags, and force autoclear */ + .lo_flags = (loop_flags & ~LO_FLAGS_READ_ONLY) | ((loop_flags & O_ACCMODE) == O_RDONLY ? LO_FLAGS_READ_ONLY : 0) | LO_FLAGS_AUTOCLEAR, + .lo_offset = offset, + .lo_sizelimit = size == UINT64_MAX ? 0 : size, + }; + if (ioctl(loop, LOOP_SET_STATUS64, &info) < 0) return -errno; diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h index 80a114f9d996a..50b959fe25147 100644 --- a/src/shared/loop-util.h +++ b/src/shared/loop-util.h @@ -14,7 +14,11 @@ struct LoopDevice { bool relinquished; }; -int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret); +int loop_device_make_full(int fd, int open_flags, uint64_t offset, uint64_t size, uint32_t loop_flags, LoopDevice **ret); +static inline int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret) { + return loop_device_make_full(fd, open_flags, 0, 0, loop_flags, ret); +} + int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret); int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret); From 9a7923a04a0893014cd611179888a01fbda33d26 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 20 May 2019 16:15:22 +0200 Subject: [PATCH 009/109] loop-util: allow refreshing offset --- src/shared/loop-util.c | 18 ++++++++++++++++-- src/shared/loop-util.h | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index c2ea476a80c85..acdacd3a48dfa 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -209,13 +209,27 @@ int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { return d->fd; } -int loop_device_refresh_size(LoopDevice *d) { +int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size) { + struct loop_info64 info; assert(d); if (d->fd < 0) return -EBADF; - if (ioctl(d->fd, LOOP_SET_CAPACITY) < 0) + if (ioctl(d->fd, LOOP_GET_STATUS64, &info) < 0) + return -errno; + + if (size == UINT64_MAX && offset == UINT64_MAX) + return 0; + if (info.lo_sizelimit == size && info.lo_offset == offset) + return 0; + + if (size != UINT64_MAX) + info.lo_sizelimit = size; + if (offset != UINT64_MAX) + info.lo_offset = offset; + + if (ioctl(d->fd, LOOP_SET_STATUS64, &info) < 0) return -errno; return 0; diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h index 50b959fe25147..e5a0ae75b8cf0 100644 --- a/src/shared/loop-util.h +++ b/src/shared/loop-util.h @@ -27,4 +27,4 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(LoopDevice*, loop_device_unref); void loop_device_relinquish(LoopDevice *d); -int loop_device_refresh_size(LoopDevice *d); +int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size); From 5210ce8b65f7dd023cf8e53be9d5600e849e6c46 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 21 May 2019 18:04:04 +0200 Subject: [PATCH 010/109] loop-util: add api for locking the block device with flock() --- src/shared/loop-util.c | 13 +++++++++++++ src/shared/loop-util.h | 2 ++ 2 files changed, 15 insertions(+) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index acdacd3a48dfa..a039e9090c2c7 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -234,3 +235,15 @@ int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size) { return 0; } + +int loop_device_flock(LoopDevice *d, int operation) { + assert(d); + + if (d->fd < 0) + return -EBADF; + + if (flock(d->fd, operation) < 0) + return -errno; + + return 0; +} diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h index e5a0ae75b8cf0..5156b46ad611a 100644 --- a/src/shared/loop-util.h +++ b/src/shared/loop-util.h @@ -28,3 +28,5 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(LoopDevice*, loop_device_unref); void loop_device_relinquish(LoopDevice *d); int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size); + +int loop_device_flock(LoopDevice *d, int operation); From d38e8aa0e0df38f3b787e04aecda331b6bec74b0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 21 May 2019 18:05:29 +0200 Subject: [PATCH 011/109] loop-util: optionally also resize partitions --- src/shared/loop-util.c | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index a039e9090c2c7..26693b512a0c0 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -9,7 +11,9 @@ #include "alloc-util.h" #include "fd-util.h" +#include "fileio.h" #include "loop-util.h" +#include "parse-util.h" #include "stat-util.h" int loop_device_make_full( @@ -210,13 +214,116 @@ int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { return d->fd; } +static int resize_partition(int partition_fd, uint64_t offset, uint64_t size) { + _cleanup_free_ char *sysfs = NULL, *whole = NULL, *buffer = NULL; + uint64_t current_offset, current_size, partno; + _cleanup_close_ int whole_fd = -1; + struct blkpg_ioctl_arg ba; + struct blkpg_partition bp; + struct stat st; + dev_t devno; + int r; + + assert(partition_fd >= 0); + + /* Resizes the partition the loopback device refer to (assuming it refers to one instead of an actual + * loopback device), and changes the offset, if needed. This is a fancy wrapper around + * BLKPG_RESIZE_PARTITION. */ + + if (fstat(partition_fd, &st) < 0) + return -errno; + + assert(S_ISBLK(st.st_mode)); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return -ENOMEM; + + r = read_one_line_file(sysfs, &buffer); + if (r == -ENOENT) /* not a partition, cannot resize */ + return -ENOTTY; + if (r < 0) + return r; + r = safe_atou64(buffer, &partno); + if (r < 0) + return r; + + sysfs = mfree(sysfs); + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0) + return -ENOMEM; + + buffer = mfree(buffer); + r = read_one_line_file(sysfs, &buffer); + if (r < 0) + return r; + r = safe_atou64(buffer, ¤t_offset); + if (r < 0) + return r; + if (current_offset > UINT64_MAX/512U) + return -EINVAL; + current_offset *= 512U; + + if (ioctl(partition_fd, BLKGETSIZE64, ¤t_size) < 0) + return -EINVAL; + + if (size == UINT64_MAX && offset == UINT64_MAX) + return 0; + if (current_size == size && current_offset == offset) + return 0; + + sysfs = mfree(sysfs); + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/../dev", major(st.st_rdev), minor(st.st_rdev)) < 0) + return -ENOMEM; + + buffer = mfree(buffer); + r = read_one_line_file(sysfs, &buffer); + if (r < 0) + return r; + r = parse_dev(buffer, &devno); + if (r < 0) + return r; + + r = device_path_make_major_minor(S_IFBLK, devno, &whole); + if (r < 0) + return r; + + whole_fd = open(whole, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (whole_fd < 0) + return -errno; + + bp = (struct blkpg_partition) { + .pno = partno, + .start = offset == UINT64_MAX ? current_offset : offset, + .length = size == UINT64_MAX ? current_size : size, + }; + + ba = (struct blkpg_ioctl_arg) { + .op = BLKPG_RESIZE_PARTITION, + .data = &bp, + .datalen = sizeof(bp), + }; + + if (ioctl(whole_fd, BLKPG, &ba) < 0) + return -errno; + + return 0; +} + int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size) { struct loop_info64 info; assert(d); + /* Changes the offset/start of the loop device relative to the beginning of the underlying file or + * block device. If this loop device actually refers to a partition and not a loopback device, we'll + * try to adjust the partition offsets instead. + * + * If either offset or size is UINT64_MAX we won't change that parameter. */ + if (d->fd < 0) return -EBADF; + if (d->nr < 0) /* not a loopback device */ + return resize_partition(d->fd, offset, size); + if (ioctl(d->fd, LOOP_GET_STATUS64, &info) < 0) return -errno; From b1d7789ea342bf701b33fb9b641efa4d63b61c7b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 20:11:52 +0200 Subject: [PATCH 012/109] loop-util: fill in the loopback number, even a posteriori --- src/shared/loop-util.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 26693b512a0c0..409cf462574e1 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -30,7 +30,7 @@ int loop_device_make_full( struct loop_info64 info; struct stat st; LoopDevice *d; - int nr, r; + int nr = -1, r; assert(fd >= 0); assert(ret); @@ -40,6 +40,14 @@ int loop_device_make_full( return -errno; if (S_ISBLK(st.st_mode)) { + if (ioctl(loop, LOOP_GET_STATUS64, &info) >= 0) { + /* Oh! This is a loopback device? That's interesting! */ + nr = info.lo_number; + + if (asprintf(&loopdev, "/dev/loop%i", nr) < 0) + return -ENOMEM; + } + if (offset == 0 && IN_SET(size, 0, UINT64_MAX)) { int copy; @@ -55,7 +63,8 @@ int loop_device_make_full( *d = (LoopDevice) { .fd = copy, - .nr = -1, + .nr = nr, + .node = TAKE_PTR(loopdev), .relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */ }; @@ -179,8 +188,10 @@ void loop_device_relinquish(LoopDevice *d) { int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { _cleanup_close_ int loop_fd = -1; _cleanup_free_ char *p = NULL; + struct loop_info64 info; struct stat st; LoopDevice *d; + int nr; assert(loop_path); assert(ret); @@ -191,10 +202,14 @@ int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { if (fstat(loop_fd, &st) < 0) return -errno; - if (!S_ISBLK(st.st_mode)) return -ENOTBLK; + if (ioctl(loop_fd, LOOP_GET_STATUS64, &info) >= 0) + nr = info.lo_number; + else + nr = -1; + p = strdup(loop_path); if (!p) return -ENOMEM; @@ -205,7 +220,7 @@ int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { *d = (LoopDevice) { .fd = TAKE_FD(loop_fd), - .nr = -1, + .nr = nr, .node = TAKE_PTR(p), .relinquished = true, /* It's not ours, don't try to destroy it when this object is freed */ }; From 62777db49498fa8fbaebc4cee08f9f891a460961 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 20:12:13 +0200 Subject: [PATCH 013/109] loop-util: if we fail to fully set up a loop device, detach it again --- src/shared/loop-util.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 409cf462574e1..6952a7f654de0 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -28,8 +28,8 @@ int loop_device_make_full( _cleanup_free_ char *loopdev = NULL; unsigned n_attempts = 0; struct loop_info64 info; + LoopDevice *d = NULL; struct stat st; - LoopDevice *d; int nr = -1, r; assert(fd >= 0); @@ -114,12 +114,16 @@ int loop_device_make_full( .lo_sizelimit = size == UINT64_MAX ? 0 : size, }; - if (ioctl(loop, LOOP_SET_STATUS64, &info) < 0) - return -errno; + if (ioctl(loop, LOOP_SET_STATUS64, &info) < 0) { + r = -errno; + goto fail; + } d = new(LoopDevice, 1); - if (!d) - return -ENOMEM; + if (!d) { + r = -ENOMEM; + goto fail; + } *d = (LoopDevice) { .fd = TAKE_FD(loop), @@ -129,6 +133,14 @@ int loop_device_make_full( *ret = d; return d->fd; + +fail: + if (fd >= 0) + (void) ioctl(fd, LOOP_CLR_FD); + if (d && d->fd >= 0) + (void) ioctl(d->fd, LOOP_CLR_FD); + + return r; } int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret) { From 614bc3c092fc5767ac7edab1b342e1bdd8a55ad8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 23 Dec 2018 19:31:29 +0100 Subject: [PATCH 014/109] =?UTF-8?q?chown-recursive:=20move=20src/core/chow?= =?UTF-8?q?n-recursive.[ch]=20=E2=86=92=20src/shared/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We want to use it outside of the core, hence let's moved it to the shared code directory. --- src/core/meson.build | 2 -- src/{core => shared}/chown-recursive.c | 0 src/{core => shared}/chown-recursive.h | 0 src/shared/meson.build | 2 ++ 4 files changed, 2 insertions(+), 2 deletions(-) rename src/{core => shared}/chown-recursive.c (100%) rename src/{core => shared}/chown-recursive.h (100%) diff --git a/src/core/meson.build b/src/core/meson.build index fb6820e109a9e..e0f08c057e8d8 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -22,8 +22,6 @@ libcore_sources = ''' bpf-firewall.h cgroup.c cgroup.h - chown-recursive.c - chown-recursive.h dbus-automount.c dbus-automount.h dbus-cgroup.c diff --git a/src/core/chown-recursive.c b/src/shared/chown-recursive.c similarity index 100% rename from src/core/chown-recursive.c rename to src/shared/chown-recursive.c diff --git a/src/core/chown-recursive.h b/src/shared/chown-recursive.h similarity index 100% rename from src/core/chown-recursive.h rename to src/shared/chown-recursive.h diff --git a/src/shared/meson.build b/src/shared/meson.build index e9005a30e3a52..b5f4f06d0f963 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -35,6 +35,8 @@ shared_sources = files(''' calendarspec.h cgroup-show.c cgroup-show.h + chown-recursive.c + chown-recursive.h clean-ipc.c clean-ipc.h clock-util.c From 37eb258396b2f724ed0e0a11b57e98366a0d835e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Apr 2019 18:44:28 +0200 Subject: [PATCH 015/109] chown-recursive: add fd based API --- src/shared/chown-recursive.c | 37 ++++++++++++++++++++++++++++++++++++ src/shared/chown-recursive.h | 2 ++ 2 files changed, 39 insertions(+) diff --git a/src/shared/chown-recursive.c b/src/shared/chown-recursive.c index 24cdf25b838b2..1aebac307ac30 100644 --- a/src/shared/chown-recursive.c +++ b/src/shared/chown-recursive.c @@ -139,3 +139,40 @@ int path_chown_recursive( return chown_recursive_internal(TAKE_FD(fd), &st, uid, gid, mask); /* we donate the fd to the call, regardless if it succeeded or failed */ } + +int fd_chown_recursive( + int fd, + uid_t uid, + gid_t gid, + mode_t mask) { + + int duplicated_fd = -1; + struct stat st; + + /* Note that the slightly different order of fstat() and the checks here and in + * path_chown_recursive(). That's because when we open the dirctory ourselves we can specify + * O_DIRECTORY and we always want to ensure we are operating on a directory before deciding whether + * the operation is otherwise redundant. */ + + if (fstat(fd, &st) < 0) + return -errno; + + if (!S_ISDIR(st.st_mode)) + return -ENOTDIR; + + if (!uid_is_valid(uid) && !gid_is_valid(gid) && (mask & 07777) == 07777) + return 0; /* nothing to do */ + + /* Shortcut, as above */ + if ((!uid_is_valid(uid) || st.st_uid == uid) && + (!gid_is_valid(gid) || st.st_gid == gid) && + ((st.st_mode & ~mask & 07777) == 0)) + return 0; + + /* Let's duplicate the fd here, as opendir() wants to take possession of it and close it afterwards */ + duplicated_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (duplicated_fd < 0) + return -errno; + + return chown_recursive_internal(duplicated_fd, &st, uid, gid, mask); /* fd donated even on failure */ +} diff --git a/src/shared/chown-recursive.h b/src/shared/chown-recursive.h index bfee05f3be561..14a79733f531e 100644 --- a/src/shared/chown-recursive.h +++ b/src/shared/chown-recursive.h @@ -4,3 +4,5 @@ #include int path_chown_recursive(const char *path, uid_t uid, gid_t gid, mode_t mask); + +int fd_chown_recursive(int fd, uid_t uid, gid_t gid, mode_t mask); From 07631a8ce36719dceb9908232571d157fc6c5731 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Dec 2018 19:01:53 +0100 Subject: [PATCH 016/109] missing: add XFS magic --- src/basic/missing_magic.h | 5 +++++ src/basic/missing_xfs.h | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/basic/missing_xfs.h diff --git a/src/basic/missing_magic.h b/src/basic/missing_magic.h index 4910cd368f90a..80bcfcca68edb 100644 --- a/src/basic/missing_magic.h +++ b/src/basic/missing_magic.h @@ -32,3 +32,8 @@ #ifndef MQUEUE_MAGIC #define MQUEUE_MAGIC 0x19800202 #endif + +/* Not exposed yet (4.20). Defined in fs/xfs/libxfs/xfs_format.h */ +#ifndef XFS_SB_MAGIC +#define XFS_SB_MAGIC 0x58465342 +#endif diff --git a/src/basic/missing_xfs.h b/src/basic/missing_xfs.h new file mode 100644 index 0000000000000..9eac76dd67c4e --- /dev/null +++ b/src/basic/missing_xfs.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +/* This is currently not exported in the public kernel headers, but the libxfs library code part of xfsprogs + * defines it as public header */ + +#ifndef XFS_IOC_FSGEOMETRY +#define XFS_IOC_FSGEOMETRY _IOR ('X', 124, struct xfs_fsop_geom) + +typedef struct xfs_fsop_geom { + uint32_t blocksize; + uint32_t rtextsize; + uint32_t agblocks; + uint32_t agcount; + uint32_t logblocks; + uint32_t sectsize; + uint32_t inodesize; + uint32_t imaxpct; + uint64_t datablocks; + uint64_t rtblocks; + uint64_t rtextents; + uint64_t logstart; + unsigned char uuid[16]; + uint32_t sunit; + uint32_t swidth; + int32_t version; + uint32_t flags; + uint32_t logsectsize; + uint32_t rtsectsize; + uint32_t dirblocksize; + uint32_t logsunit; +} xfs_fsop_geom_t; +#endif + +#ifndef XFS_IOC_FSGROWFSDATA +#define XFS_IOC_FSGROWFSDATA _IOW ('X', 110, struct xfs_growfs_data) + +typedef struct xfs_growfs_data { + uint64_t newblocks; + uint32_t imaxpct; +} xfs_growfs_data_t; +#endif From 40df083aee25087649af25d0cf3e0bc2a72c2a10 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 27 Dec 2018 14:31:27 +0100 Subject: [PATCH 017/109] shared: add new wrapper for online fs resizing ioctls --- src/shared/meson.build | 2 + src/shared/resize-fs.c | 112 +++++++++++++++++++++++++++++++++++++++++ src/shared/resize-fs.h | 15 ++++++ 3 files changed, 129 insertions(+) create mode 100644 src/shared/resize-fs.c create mode 100644 src/shared/resize-fs.h diff --git a/src/shared/meson.build b/src/shared/meson.build index b5f4f06d0f963..031f018663aa3 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -147,6 +147,8 @@ shared_sources = files(''' ptyfwd.h reboot-util.c reboot-util.h + resize-fs.c + resize-fs.h resolve-util.c resolve-util.h seccomp-util.h diff --git a/src/shared/resize-fs.c b/src/shared/resize-fs.c new file mode 100644 index 0000000000000..9f33dd77d88a3 --- /dev/null +++ b/src/shared/resize-fs.c @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include + +#include "blockdev-util.h" +#include "fs-util.h" +#include "missing_fs.h" +#include "missing_magic.h" +#include "missing_xfs.h" +#include "resize-fs.h" +#include "stat-util.h" + +int resize_fs(int fd, uint64_t sz) { + struct statfs sfs; + int r; + + assert(fd >= 0); + + /* Rounds down to next block size */ + + if (sz <= 0 || sz == UINT64_MAX) + return -ERANGE; + + if (fstatfs(fd, &sfs) < 0) + return -errno; + + if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) { + uint64_t u; + + if (sz < EXT4_MINIMAL_SIZE) + return -ERANGE; + + u = sz / sfs.f_bsize; + + if (ioctl(fd, EXT4_IOC_RESIZE_FS, &u) < 0) + return -errno; + + } else if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + struct btrfs_ioctl_vol_args args = {}; + + /* 256M is the minimize size enforced by the btrfs kernel code when resizing (which is + * strange btw, as mkfs.btrfs is fine creating file systems > 109M). It will return EINVAL in + * that case, let's catch this error beforehand though, and report a more explanatory + * error. */ + + if (sz < BTRFS_MINIMAL_SIZE) + return -ERANGE; + + r = snprintf(args.name, sizeof(args.name), "%" PRIu64, sz); + assert((size_t) r < sizeof(args.name)); + + if (ioctl(fd, BTRFS_IOC_RESIZE, &args) < 0) + return -errno; + + } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) { + xfs_fsop_geom_t geo; + xfs_growfs_data_t d; + + if (sz < XFS_MINIMAL_SIZE) + return -ERANGE; + + if (ioctl(fd, XFS_IOC_FSGEOMETRY, &geo) < 0) + return -errno; + + d = (xfs_growfs_data_t) { + .imaxpct = geo.imaxpct, + .newblocks = sz / geo.blocksize, + }; + + if (ioctl(fd, XFS_IOC_FSGROWFSDATA, &d) < 0) + return -errno; + + } else + return -EOPNOTSUPP; + + return 0; +} + +uint64_t minimal_size_by_fs_magic(statfs_f_type_t magic) { + + switch (magic) { + + case (statfs_f_type_t) EXT4_SUPER_MAGIC: + return EXT4_MINIMAL_SIZE; + + case (statfs_f_type_t) XFS_SB_MAGIC: + return XFS_MINIMAL_SIZE; + + case (statfs_f_type_t) BTRFS_SUPER_MAGIC: + return BTRFS_MINIMAL_SIZE; + + default: + return UINT64_MAX; + } +} + +uint64_t minimal_size_by_fs_name(const char *name) { + + if (streq_ptr(name, "ext4")) + return EXT4_MINIMAL_SIZE; + + if (streq_ptr(name, "xfs")) + return XFS_MINIMAL_SIZE; + + if (streq_ptr(name, "btrfs")) + return BTRFS_MINIMAL_SIZE; + + return UINT64_MAX; +} diff --git a/src/shared/resize-fs.h b/src/shared/resize-fs.h new file mode 100644 index 0000000000000..b5441765289ec --- /dev/null +++ b/src/shared/resize-fs.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "stat-util.h" + +int resize_fs(int fd, uint64_t sz); + +#define BTRFS_MINIMAL_SIZE (256U*1024U*1024U) +#define XFS_MINIMAL_SIZE (14U*1024U*1024U) +#define EXT4_MINIMAL_SIZE (1024U*1024U) + +uint64_t minimal_size_by_fs_magic(statfs_f_type_t magic); +uint64_t minimal_size_by_fs_name(const char *str); From a464b103f18b715bfe666df5e57a4ccde7fc5f1f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Jan 2019 18:23:40 +0100 Subject: [PATCH 018/109] fileio: add an openat() flavour for fopen() This adds xfopenat() which is to fopen() what xopendirat() is to opendir(), i.e. the "at" counterpart to fopen(). (Similar to the xopendir() case, we prefix this with "x", in case libc gains this natively eventually.) --- src/basic/fileio.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++ src/basic/fileio.h | 1 + 2 files changed, 76 insertions(+) diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 623e43e4caeae..895b6f839b152 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -575,6 +575,81 @@ DIR *xopendirat(int fd, const char *name, int flags) { return d; } +static int mode_to_flags(const char *mode) { + const char *p; + int flags; + + if ((p = startswith(mode, "r+"))) + flags = O_RDWR; + else if ((p = startswith(mode, "r"))) + flags = O_RDONLY; + else if ((p = startswith(mode, "w+"))) + flags = O_RDWR|O_CREAT|O_TRUNC; + else if ((p = startswith(mode, "w"))) + flags = O_WRONLY|O_CREAT|O_TRUNC; + else if ((p = startswith(mode, "a+"))) + flags = O_RDWR|O_CREAT|O_APPEND; + else if ((p = startswith(mode, "a"))) + flags = O_WRONLY|O_CREAT|O_APPEND; + else + return -EINVAL; + + for (; *p != 0; p++) { + + switch (*p) { + + case 'e': + flags |= O_CLOEXEC; + break; + + case 'x': + flags |= O_EXCL; + break; + + case 'm': + /* ignore this here, fdopen() might care later though */ + break; + + case 'c': /* not sure what to do about this one */ + default: + return -EINVAL; + } + } + + return flags; +} + +int xfopenat(int dir_fd, const char *path, const char *mode, int flags, FILE **ret) { + FILE *f; + + /* A combination of fopen() with openat() */ + + if (dir_fd == AT_FDCWD && flags == 0) { + f = fopen(path, mode); + if (!f) + return -errno; + } else { + int fd, mode_flags; + + mode_flags = mode_to_flags(mode); + if (mode_flags < 0) + return mode_flags; + + fd = openat(dir_fd, path, mode_flags | flags); + if (fd < 0) + return -errno; + + f = fdopen(fd, mode); + if (!f) { + safe_close(fd); + return -errno; + } + } + + *ret = f; + return 0; +} + static int search_and_fopen_internal(const char *path, const char *mode, const char *root, char **search, FILE **_f) { char **i; diff --git a/src/basic/fileio.h b/src/basic/fileio.h index 05f6c89da09dc..fe42336f7db53 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -68,6 +68,7 @@ int executable_is_script(const char *path, char **interpreter); int get_proc_field(const char *filename, const char *pattern, const char *terminator, char **field); DIR *xopendirat(int dirfd, const char *name, int flags); +int xfopenat(int dir_fd, const char *path, const char *mode, int flags, FILE **ret); int search_and_fopen(const char *path, const char *mode, const char *root, const char **search, FILE **_f); int search_and_fopen_nulstr(const char *path, const char *mode, const char *root, const char *search, FILE **_f); From 2130f8069d234843f8a9cbd06b99c65d22db3a0d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Jan 2019 18:29:36 +0100 Subject: [PATCH 019/109] fileio: add 'dir_fd' parameter to read_full_file_full() Let's introduce an "at" version of read_full_file(). --- src/basic/fileio.c | 6 ++++-- src/basic/fileio.h | 5 +++-- src/network/netdev/macsec.c | 2 +- src/network/netdev/wireguard.c | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 895b6f839b152..9573582438119 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -438,17 +438,19 @@ int read_full_stream_full( return r; } -int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) { +int read_full_file_full(int dir_fd, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) { _cleanup_fclose_ FILE *f = NULL; int r; assert(filename); assert(contents); - r = fopen_unlocked(filename, "re", &f); + r = xfopenat(dir_fd, filename, "re", 0, &f); if (r < 0) return r; + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + return read_full_stream_full(f, filename, flags, contents, size); } diff --git a/src/basic/fileio.h b/src/basic/fileio.h index fe42336f7db53..04a327b046dfa 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "macro.h" @@ -52,9 +53,9 @@ static inline int write_string_file(const char *fn, const char *line, WriteStrin int write_string_filef(const char *fn, WriteStringFileFlags flags, const char *format, ...) _printf_(3, 4); int read_one_line_file(const char *filename, char **line); -int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size); +int read_full_file_full(int dir_fd, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size); static inline int read_full_file(const char *filename, char **contents, size_t *size) { - return read_full_file_full(filename, 0, contents, size); + return read_full_file_full(AT_FDCWD, filename, 0, contents, size); } int read_full_stream_full(FILE *f, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size); static inline int read_full_stream(FILE *f, char **contents, size_t *size) { diff --git a/src/network/netdev/macsec.c b/src/network/netdev/macsec.c index cf281e75a6d45..56240b9586a22 100644 --- a/src/network/netdev/macsec.c +++ b/src/network/netdev/macsec.c @@ -981,7 +981,7 @@ static int macsec_read_key_file(NetDev *netdev, SecurityAssociation *sa) { if (!sa->key_file) return 0; - r = read_full_file_full(sa->key_file, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNHEX, (char **) &key, &key_len); + r = read_full_file_full(AT_FDCWD, sa->key_file, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNHEX, (char **) &key, &key_len); if (r < 0) return log_netdev_error_errno(netdev, r, "Failed to read key from '%s', ignoring: %m", diff --git a/src/network/netdev/wireguard.c b/src/network/netdev/wireguard.c index 913ee2a058977..38f47a36f8ea1 100644 --- a/src/network/netdev/wireguard.c +++ b/src/network/netdev/wireguard.c @@ -901,7 +901,7 @@ static int wireguard_read_key_file(const char *filename, uint8_t dest[static WG_ assert(dest); - r = read_full_file_full(filename, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64, &key, &key_len); + r = read_full_file_full(AT_FDCWD, filename, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64, &key, &key_len); if (r < 0) return r; From 00d9ce37b3a0950b50f9919cf282fad82f78545b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 17 May 2019 15:52:35 +0200 Subject: [PATCH 020/109] fileio: add WRITE_STRING_FILE_MODE_0600 flag for writing files usually we want to create new files with mode 0666 (modulated by the umask). Sometimes we want more restrictive access though, let's add an explicit flag support for that. (Note that we don't bother with arbitrary access modes to keep things simple: just "open as umask permits" and "private to me", nothing else) --- src/basic/fileio.c | 43 +++++++++++++++++++++---------------------- src/basic/fileio.h | 1 + 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 9573582438119..eb7e92f88d811 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -138,16 +138,21 @@ static int write_string_file_atomic( assert(fn); assert(line); + /* Note that we'd really like to use O_TMPFILE here, but can't really, since we want replacement + * semantics here, and O_TMPFILE can't offer that. i.e. rename() replaces but linkat() doesn't. */ + r = fopen_temporary(fn, &f, &p); if (r < 0) return r; - (void) fchmod_umask(fileno(f), 0644); - r = write_string_stream_ts(f, line, flags, ts); if (r < 0) goto fail; + r = fchmod_umask(fileno(f), FLAGS_SET(flags, WRITE_STRING_FILE_MODE_0600) ? 0600 : 0644); + if (r < 0) + goto fail; + if (rename(p, fn) < 0) { r = -errno; goto fail; @@ -167,7 +172,7 @@ int write_string_file_ts( struct timespec *ts) { _cleanup_fclose_ FILE *f = NULL; - int q, r; + int q, r, fd; assert(fn); assert(line); @@ -192,26 +197,20 @@ int write_string_file_ts( } else assert(!ts); - if (flags & WRITE_STRING_FILE_CREATE) { - r = fopen_unlocked(fn, "we", &f); - if (r < 0) - goto fail; - } else { - int fd; - - /* We manually build our own version of fopen(..., "we") that - * works without O_CREAT */ - fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY | ((flags & WRITE_STRING_FILE_NOFOLLOW) ? O_NOFOLLOW : 0)); - if (fd < 0) { - r = -errno; - goto fail; - } + /* We manually build our own version of fopen(..., "we") that works without O_CREAT and with O_NOFOLLOW if needed. */ + fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY | + (FLAGS_SET(flags, WRITE_STRING_FILE_NOFOLLOW) ? O_NOFOLLOW : 0) | + (FLAGS_SET(flags, WRITE_STRING_FILE_CREATE) ? O_CREAT : 0), + (FLAGS_SET(flags, WRITE_STRING_FILE_MODE_0600) ? 0600 : 0666)); + if (fd < 0) { + r = -errno; + goto fail; + } - r = fdopen_unlocked(fd, "w", &f); - if (r < 0) { - safe_close(fd); - goto fail; - } + r = fdopen_unlocked(fd, "w", &f); + if (r < 0) { + safe_close(fd); + goto fail; } if (flags & WRITE_STRING_FILE_DISABLE_BUFFER) diff --git a/src/basic/fileio.h b/src/basic/fileio.h index 04a327b046dfa..3cf1ab77bf6a3 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -23,6 +23,7 @@ typedef enum { WRITE_STRING_FILE_DISABLE_BUFFER = 1 << 5, WRITE_STRING_FILE_NOFOLLOW = 1 << 6, WRITE_STRING_FILE_MKDIR_0755 = 1 << 7, + WRITE_STRING_FILE_MODE_0600 = 1 << 8, /* And before you wonder, why write_string_file_atomic_label_ts() is a separate function instead of just one more flag here: it's about linking: we don't want to pull -lselinux into all users of write_string_file() From e9a4da964ac8e16b9ab44af91eb4d107ad661cb7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 3 Jan 2019 12:19:31 +0100 Subject: [PATCH 021/109] json: beef up strv parser to also accept a single string instead of an array of strings Let's be permissive in what we accept and take a single string instead of an array of strings, when a string is requested, too. --- src/shared/json.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index f1bb50cfa2c8c..5a8e906d03722 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -3421,6 +3421,16 @@ int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags return 0; } + /* Let's be flexible here: accept a single string in place of a single-item array */ + if (json_variant_is_string(variant)) { + l = strv_new(json_variant_string(variant)); + if (!l) + return log_oom(); + + strv_free_and_replace(*s, l); + return 0; + } + if (!json_variant_is_array(variant)) return json_log(variant, SYNTHETIC_ERRNO(EINVAL), flags, "JSON field '%s' is not an array.", strna(name)); From 7f84dbd07b7d1ef79da01ebee1665444116514a7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 17:38:17 +0200 Subject: [PATCH 022/109] json: add new json_variant_is_blank_object() helper --- src/shared/json.c | 7 +++++++ src/shared/json.h | 1 + 2 files changed, 8 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 5a8e906d03722..9bd24dec4612b 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -947,6 +947,13 @@ bool json_variant_is_negative(JsonVariant *v) { return false; } +bool json_variant_is_blank_object(JsonVariant *v) { + /* Returns true if the specified object is null or empty */ + return !v || + json_variant_is_null(v) || + (json_variant_is_object(v) && json_variant_elements(v) == 0); +} + JsonVariantType json_variant_type(JsonVariant *v) { if (!v) diff --git a/src/shared/json.h b/src/shared/json.h index 1f9c620ebb816..00357f72d8dad 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -120,6 +120,7 @@ static inline bool json_variant_is_null(JsonVariant *v) { } bool json_variant_is_negative(JsonVariant *v); +bool json_variant_is_blank_object(JsonVariant *v); size_t json_variant_elements(JsonVariant *v); JsonVariant *json_variant_by_index(JsonVariant *v, size_t index); From b5eeeb6730e3223db72e4a810756c430ab41ab64 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 3 Jan 2019 17:51:12 +0100 Subject: [PATCH 023/109] json: add new API json_variant_filter() for dropping fields from objects --- src/shared/json.c | 53 +++++++++++++++++++++++++++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 55 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 9bd24dec4612b..8453f7fc8d530 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1617,6 +1617,59 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha fputc('\n', f); /* In case of SSE add a second newline */ } +int json_variant_filter(JsonVariant **v, char **to_remove) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_free_ JsonVariant **array = NULL; + size_t i, n = 0, k = 0; + int r; + + assert(v); + + if (json_variant_is_blank_object(*v)) + return 0; + if (!json_variant_is_object(*v)) + return -EINVAL; + + if (strv_isempty(to_remove)) + return 0; + + for (i = 0; i < json_variant_elements(*v); i += 2) { + JsonVariant *p; + + p = json_variant_by_index(*v, i); + if (!json_variant_has_type(p, JSON_VARIANT_STRING)) + return -EINVAL; + + if (strv_contains(to_remove, json_variant_string(p))) { + if (!array) { + array = new(JsonVariant*, json_variant_elements(*v) - 2); + if (!array) + return -ENOMEM; + + for (k = 0; k < i; k++) + array[k] = json_variant_by_index(*v, k); + } + + n++; + } else if (array) { + array[k++] = p; + array[k++] = json_variant_by_index(*v, i + 1); + } + } + + if (n == 0) + return 0; + + r = json_variant_new_object(&w, array, k); + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(w); + + return (int) n; +} + static int json_variant_copy(JsonVariant **nv, JsonVariant *v) { JsonVariantType t; JsonVariant *c; diff --git a/src/shared/json.h b/src/shared/json.h index 00357f72d8dad..335a6e7520f70 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -166,6 +166,8 @@ typedef enum JsonFormatFlags { int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret); void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const char *prefix); +int json_variant_filter(JsonVariant **v, char **to_remove); + int json_parse(const char *string, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); From 9fd1d122d23f2888c6868efbbb8b89ccb7613c08 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 4 Jan 2019 13:24:32 +0100 Subject: [PATCH 024/109] json: add new json_variant_set_field() helper --- src/shared/json.c | 69 +++++++++++++++++++++++++++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 71 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 8453f7fc8d530..0809fd7465296 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1670,6 +1670,75 @@ int json_variant_filter(JsonVariant **v, char **to_remove) { return (int) n; } +int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value) { + _cleanup_(json_variant_unrefp) JsonVariant *field_variant = NULL, *w = NULL; + _cleanup_free_ JsonVariant **array = NULL; + size_t i, k = 0; + int r; + + assert(v); + assert(field); + + if (json_variant_is_blank_object(*v)) { + array = new(JsonVariant*, 2); + if (!array) + return -ENOMEM; + + } else { + if (!json_variant_is_object(*v)) + return -EINVAL; + + for (i = 0; i < json_variant_elements(*v); i += 2) { + JsonVariant *p; + + p = json_variant_by_index(*v, i); + if (!json_variant_is_string(p)) + return -EINVAL; + + if (streq(json_variant_string(p), field)) { + + if (!array) { + array = new(JsonVariant*, json_variant_elements(*v)); + if (!array) + return -ENOMEM; + + for (k = 0; k < i; k++) + array[k] = json_variant_by_index(*v, k); + } + + } else if (array) { + array[k++] = p; + array[k++] = json_variant_by_index(*v, i + 1); + } + } + + if (!array) { + array = new(JsonVariant*, json_variant_elements(*v) + 2); + if (!array) + return -ENOMEM; + + for (k = 0; k < json_variant_elements(*v); k++) + array[k] = json_variant_by_index(*v, k); + } + } + + r = json_variant_new_string(&field_variant, field); + if (r < 0) + return r; + + array[k++] = field_variant; + array[k++] = value; + + r = json_variant_new_object(&w, array, k); + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(w); + + return 1; +} + static int json_variant_copy(JsonVariant **nv, JsonVariant *v) { JsonVariantType t; JsonVariant *c; diff --git a/src/shared/json.h b/src/shared/json.h index 335a6e7520f70..a293a85ac80dd 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -168,6 +168,8 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha int json_variant_filter(JsonVariant **v, char **to_remove); +int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value); + int json_parse(const char *string, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); From 0e416fa9d5f0960968887ad0fc0f79835e0b5626 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Jan 2019 18:33:32 +0100 Subject: [PATCH 025/109] json: add a new "sensitive" flags for JsonVariant objects An object marked with this flag will be erased from memory when it is freed. This is useful for dealing with sensitive data (key material, passphrases) encoded in JSON objects. --- src/shared/json.c | 76 +++++++++++++++++++++++++++++++++++++++++++++-- src/shared/json.h | 2 ++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/shared/json.c b/src/shared/json.c index 0809fd7465296..7465faca6c79a 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -72,6 +72,9 @@ struct JsonVariant { /* While comparing two arrays, we use this for marking what we already have seen */ bool is_marked:1; + /* Ersase from memory when freeing */ + bool sensitive:1; + /* The current 'depth' of the JsonVariant, i.e. how many levels of member variants this has */ uint16_t depth; @@ -651,7 +654,46 @@ int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) { return 0; } -static void json_variant_free_inner(JsonVariant *v) { +static size_t json_variant_size(JsonVariant* v) { + + if (!json_variant_is_regular(v)) + return 0; + + if (v->is_reference) + return offsetof(JsonVariant, reference) + sizeof(JsonVariant*); + + switch (v->type) { + + case JSON_VARIANT_STRING: + return offsetof(JsonVariant, string) + strlen(v->string) + 1; + + case JSON_VARIANT_REAL: + return offsetof(JsonVariant, value) + sizeof(long double); + + case JSON_VARIANT_UNSIGNED: + return offsetof(JsonVariant, value) + sizeof(uintmax_t); + + case JSON_VARIANT_INTEGER: + return offsetof(JsonVariant, value) + sizeof(intmax_t); + + case JSON_VARIANT_BOOLEAN: + return offsetof(JsonVariant, value) + sizeof(bool); + + case JSON_VARIANT_ARRAY: + case JSON_VARIANT_OBJECT: + return offsetof(JsonVariant, n_elements) + sizeof(size_t); + + case JSON_VARIANT_NULL: + return offsetof(JsonVariant, value); + + default: + assert_not_reached("unexpected type"); + } +} + +static void json_variant_free_inner(JsonVariant *v, bool force_sensitive) { + bool sensitive; + assert(v); if (!json_variant_is_regular(v)) @@ -659,7 +701,12 @@ static void json_variant_free_inner(JsonVariant *v) { json_source_unref(v->source); + sensitive = v->sensitive || force_sensitive; + if (v->is_reference) { + if (sensitive) + json_variant_sensitive(v->reference); + json_variant_unref(v->reference); return; } @@ -668,8 +715,11 @@ static void json_variant_free_inner(JsonVariant *v) { size_t i; for (i = 0; i < v->n_elements; i++) - json_variant_free_inner(v + 1 + i); + json_variant_free_inner(v + 1 + i, sensitive); } + + if (sensitive) + explicit_bzero(v, json_variant_size(v)); } JsonVariant *json_variant_ref(JsonVariant *v) { @@ -701,7 +751,7 @@ JsonVariant *json_variant_unref(JsonVariant *v) { v->n_ref--; if (v->n_ref == 0) { - json_variant_free_inner(v); + json_variant_free_inner(v, false); free(v); } } @@ -1237,6 +1287,26 @@ bool json_variant_equal(JsonVariant *a, JsonVariant *b) { } } +void json_variant_sensitive(JsonVariant *v) { + assert(v); + + /* Marks a variant as "sensitive", so that it is erased from memory when it is destroyed. This is a + * one-way operation: as soon as it is marked this way it remains marked this way until it's + * destoryed. A magic variant is never sensitive though, even when asked, since it's too + * basic. Similar, const string variant are never sensitive either, after all they are included in + * the source code as they are, which is not suitable for inclusion of secrets. + * + * Note that this flag has a recursive effect: when we destroy an object or array we'll propagate the + * flag to all contained variants. And if those are then destroyed this is propagated further down, + * and so on. */ + + v = json_variant_normalize(v); + if (!json_variant_is_regular(v)) + return; + + v->sensitive = true; +} + int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *ret_line, unsigned *ret_column) { assert_return(v, -EINVAL); diff --git a/src/shared/json.h b/src/shared/json.h index a293a85ac80dd..9d79ff9f5e314 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -129,6 +129,8 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria bool json_variant_equal(JsonVariant *a, JsonVariant *b); +void json_variant_sensitive(JsonVariant *v); + struct json_variant_foreach_state { JsonVariant *variant; size_t idx; From 122450683367203cefb802297c0b5e11149e9e92 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Jan 2019 18:46:47 +0100 Subject: [PATCH 026/109] json: add json_parse_file_at() helper This is an "at" function, similar to json_parse_file(). --- src/shared/json.c | 4 ++-- src/shared/json.h | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/shared/json.c b/src/shared/json.c index 7465faca6c79a..873eb7f062fd5 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -2828,7 +2828,7 @@ int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, u return json_parse_internal(p, NULL, ret, ret_line, ret_column, true); } -int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { +int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { _cleanup_(json_source_unrefp) JsonSource *source = NULL; _cleanup_free_ char *text = NULL; const char *p; @@ -2837,7 +2837,7 @@ int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_ if (f) r = read_full_stream(f, &text, NULL); else if (path) - r = read_full_file(path, &text, NULL); + r = read_full_file_full(dir_fd, path, 0, &text, NULL); else return -EINVAL; if (r < 0) diff --git a/src/shared/json.h b/src/shared/json.h index 9d79ff9f5e314..9fbf34bc4fb1e 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #pragma once +#include #include #include #include @@ -174,7 +175,11 @@ int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *valu int json_parse(const char *string, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); -int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); + +static inline int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_file_at(f, AT_FDCWD, path, ret, ret_line, ret_column); +} enum { _JSON_BUILD_STRING, From 2a36786d18760b63c26447f724628021483b0d7d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 8 Jan 2019 18:48:48 +0100 Subject: [PATCH 027/109] json: add flags parameter to json_parse_file(), for parsing "sensitive" data This will call json_variant_sensitive() internally while parsing for each allocated sub-variant. This is better than calling it a posteriori at the end, because partially parsed variants will always be properly erased from memory this way. --- src/fuzz/fuzz-json.c | 2 +- src/nspawn/nspawn-oci.c | 2 +- src/shared/json.c | 21 ++++++++++++++------- src/shared/json.h | 14 +++++++++----- src/shared/varlink.c | 2 +- src/test/test-json.c | 12 ++++++------ 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/fuzz/fuzz-json.c b/src/fuzz/fuzz-json.c index ce7b69dbb99f6..c01e2a570c50a 100644 --- a/src/fuzz/fuzz-json.c +++ b/src/fuzz/fuzz-json.c @@ -18,7 +18,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { f = fmemopen_unlocked((char*) data, size, "re"); assert_se(f); - if (json_parse_file(f, NULL, &v, NULL, NULL) < 0) + if (json_parse_file(f, NULL, 0, &v, NULL, NULL) < 0) return 0; g = open_memstream_unlocked(&out, &out_size); diff --git a/src/nspawn/nspawn-oci.c b/src/nspawn/nspawn-oci.c index 4519c74b95b35..ba8b142c99ec3 100644 --- a/src/nspawn/nspawn-oci.c +++ b/src/nspawn/nspawn-oci.c @@ -2214,7 +2214,7 @@ int oci_load(FILE *f, const char *bundle, Settings **ret) { path = strjoina(bundle, "/config.json"); - r = json_parse_file(f, path, &oci, &line, &column); + r = json_parse_file(f, path, 0, &oci, &line, &column); if (r < 0) { if (line != 0 && column != 0) return log_error_errno(r, "Failed to parse '%s' at %u:%u: %m", path, line, column); diff --git a/src/shared/json.c b/src/shared/json.c index 873eb7f062fd5..633ae055827fb 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -2474,6 +2474,7 @@ static void json_stack_release(JsonStack *s) { static int json_parse_internal( const char **input, JsonSource *source, + JsonParseFlags flags, JsonVariant **ret, unsigned *line, unsigned *column, @@ -2792,6 +2793,12 @@ static int json_parse_internal( } if (add) { + /* If we are asked to make this parsed object sensitive, then let's apply this + * immediately after allocating each variant, so that when we abort half-way + * everything we already allocated that is then freed is correctly marked. */ + if (FLAGS_SET(flags, JSON_PARSE_SENSITIVE)) + json_variant_sensitive(add); + (void) json_variant_set_source(&add, source, line_token, column_token); if (!GREEDY_REALLOC(current->elements, current->n_elements_allocated, current->n_elements + 1)) { @@ -2820,15 +2827,15 @@ static int json_parse_internal( return r; } -int json_parse(const char *input, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - return json_parse_internal(&input, NULL, ret, ret_line, ret_column, false); +int json_parse(const char *input, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_internal(&input, NULL, flags, ret, ret_line, ret_column, false); } -int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - return json_parse_internal(p, NULL, ret, ret_line, ret_column, true); +int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_internal(p, NULL, flags, ret, ret_line, ret_column, true); } -int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { +int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { _cleanup_(json_source_unrefp) JsonSource *source = NULL; _cleanup_free_ char *text = NULL; const char *p; @@ -2850,7 +2857,7 @@ int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonVariant **ret, } p = text; - return json_parse_internal(&p, source, ret, ret_line, ret_column, false); + return json_parse_internal(&p, source, flags, ret, ret_line, ret_column, false); } int json_buildv(JsonVariant **ret, va_list ap) { @@ -3088,7 +3095,7 @@ int json_buildv(JsonVariant **ret, va_list ap) { /* Note that we don't care for current->n_suppress here, we should generate parsing * errors even in suppressed object properties */ - r = json_parse(l, &add, NULL, NULL); + r = json_parse(l, 0, &add, NULL, NULL); if (r < 0) goto finish; } else diff --git a/src/shared/json.h b/src/shared/json.h index 9fbf34bc4fb1e..718063f442f40 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -173,12 +173,16 @@ int json_variant_filter(JsonVariant **v, char **to_remove); int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value); -int json_parse(const char *string, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); -int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); -int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +typedef enum JsonParseFlags { + JSON_PARSE_SENSITIVE = 1 << 0, /* mark variant as "sensitive", i.e. something containing secret key material or such */ +} JsonParseFlags; -static inline int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - return json_parse_file_at(f, AT_FDCWD, path, ret, ret_line, ret_column); +int json_parse(const char *string, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); + +static inline int json_parse_file(FILE *f, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_file_at(f, AT_FDCWD, path, flags, ret, ret_line, ret_column); } enum { diff --git a/src/shared/varlink.c b/src/shared/varlink.c index 99343167f66c2..e987911d45f6f 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -578,7 +578,7 @@ static int varlink_parse_message(Varlink *v) { varlink_log(v, "New incoming message: %s", begin); - r = json_parse(begin, &v->current, NULL, NULL); + r = json_parse(begin, 0, &v->current, NULL, NULL); if (r < 0) return r; diff --git a/src/test/test-json.c b/src/test/test-json.c index a6613043b9240..90b91f6310c80 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -82,7 +82,7 @@ static void test_variant(const char *data, Test test) { _cleanup_free_ char *s = NULL; int r; - r = json_parse(data, &v, NULL, NULL); + r = json_parse(data, 0, &v, NULL, NULL); assert_se(r == 0); assert_se(v); @@ -93,7 +93,7 @@ static void test_variant(const char *data, Test test) { log_info("formatted normally: %s\n", s); - r = json_parse(data, &w, NULL, NULL); + r = json_parse(data, JSON_PARSE_SENSITIVE, &w, NULL, NULL); assert_se(r == 0); assert_se(w); assert_se(json_variant_has_type(v, json_variant_type(w))); @@ -110,7 +110,7 @@ static void test_variant(const char *data, Test test) { log_info("formatted prettily:\n%s", s); - r = json_parse(data, &w, NULL, NULL); + r = json_parse(data, 0, &w, NULL, NULL); assert_se(r == 0); assert_se(w); @@ -302,7 +302,7 @@ static void test_build(void) { assert_se(json_variant_format(a, 0, &s) >= 0); log_info("GOT: %s\n", s); - assert_se(json_parse(s, &b, NULL, NULL) >= 0); + assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0); assert_se(json_variant_equal(a, b)); a = json_variant_unref(a); @@ -313,7 +313,7 @@ static void test_build(void) { s = mfree(s); assert_se(json_variant_format(a, 0, &s) >= 0); log_info("GOT: %s\n", s); - assert_se(json_parse(s, &b, NULL, NULL) >= 0); + assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0); assert_se(json_variant_format(b, 0, &t) >= 0); log_info("GOT: %s\n", t); @@ -365,7 +365,7 @@ static void test_source(void) { assert_se(f = fmemopen_unlocked((void*) data, strlen(data), "r")); - assert_se(json_parse_file(f, "waldo", &v, NULL, NULL) >= 0); + assert_se(json_parse_file(f, "waldo", 0, &v, NULL, NULL) >= 0); printf("--- non-pretty begin ---\n"); json_variant_dump(v, 0, stdout, NULL); From 6d4230160107b3b9a6c8c50c117060c20795fad6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Apr 2019 15:25:42 +0200 Subject: [PATCH 028/109] json: optionally, make string checks stricter when dispatching strings --- src/shared/json.c | 9 +++++++++ src/shared/json.h | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/shared/json.c b/src/shared/json.c index 633ae055827fb..2019d4d9fa7af 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -3606,6 +3606,9 @@ int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFla if (!json_variant_is_string(variant)) return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant))) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + r = free_and_strdup(s, json_variant_string(variant)); if (r < 0) return json_log(variant, flags, r, "Failed to allocate string: %m"); @@ -3629,6 +3632,9 @@ int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags /* Let's be flexible here: accept a single string in place of a single-item array */ if (json_variant_is_string(variant)) { + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant))) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + l = strv_new(json_variant_string(variant)); if (!l) return log_oom(); @@ -3644,6 +3650,9 @@ int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags if (!json_variant_is_string(e)) return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string."); + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(e))) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + r = strv_extend(&l, json_variant_string(e)); if (r < 0) return json_log(e, flags, r, "Failed to append array element: %m"); diff --git a/src/shared/json.h b/src/shared/json.h index 718063f442f40..5d10207af6431 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -229,10 +229,11 @@ typedef enum JsonDispatchFlags { JSON_PERMISSIVE = 1 << 0, /* Shall parsing errors be considered fatal for this property? */ JSON_MANDATORY = 1 << 1, /* Should existence of this property be mandatory? */ JSON_LOG = 1 << 2, /* Should the parser log about errors? */ + JSON_SAFE = 1 << 3, /* Don't accept "unsafe" strings in json_dispatch_string() + json_dispatch_string() */ /* The following two may be passed into log_json() in addition to the three above */ - JSON_DEBUG = 1 << 3, /* Indicates that this log message is a debug message */ - JSON_WARNING = 1 << 4, /* Indicates that this log message is a warning message */ + JSON_DEBUG = 1 << 4, /* Indicates that this log message is a debug message */ + JSON_WARNING = 1 << 5, /* Indicates that this log message is a warning message */ } JsonDispatchFlags; typedef int (*JsonDispatchCallback)(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); From 886dda42236f38793c2f04512b03ce239c9e0d09 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Apr 2019 18:20:01 +0200 Subject: [PATCH 029/109] json: add json_variant_strv() helper that converts a json variant to an strv Only works for arrays of strings, of course. --- src/shared/json.c | 60 +++++++++++++++++++++++++++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 62 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 2019d4d9fa7af..be37a80a82155 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1809,6 +1809,66 @@ int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *valu return 1; } +int json_variant_strv(JsonVariant *v, char ***ret) { + char **l = NULL; + size_t n, i; + bool sensitive; + int r; + + assert(ret); + + if (!v || json_variant_is_null(v)) { + l = new0(char*, 1); + if (!l) + return -ENOMEM; + + *ret = l; + return 0; + } + + if (!json_variant_is_array(v)) + return -EINVAL; + + sensitive = v->sensitive; + + n = json_variant_elements(v); + l = new(char*, n+1); + if (!l) + return -ENOMEM; + + for (i = 0; i < n; i++) { + JsonVariant *e; + + assert_se(e = json_variant_by_index(v, i)); + sensitive = sensitive || e->sensitive; + + if (!json_variant_is_string(e)) { + l[i] = NULL; + r = -EINVAL; + goto fail; + } + + l[i] = strdup(json_variant_string(e)); + if (!l[i]) { + r = -ENOMEM; + goto fail; + } + } + + l[i] = NULL; + *ret = TAKE_PTR(l); + + return 0; + +fail: + if (sensitive) + strv_free_erase(l); + else + strv_free(l); + + return r; +} + static int json_variant_copy(JsonVariant **nv, JsonVariant *v) { JsonVariantType t; JsonVariant *c; diff --git a/src/shared/json.h b/src/shared/json.h index 5d10207af6431..2dcab6c5e80ec 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -173,6 +173,8 @@ int json_variant_filter(JsonVariant **v, char **to_remove); int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value); +int json_variant_strv(JsonVariant *v, char ***ret); + typedef enum JsonParseFlags { JSON_PARSE_SENSITIVE = 1 << 0, /* mark variant as "sensitive", i.e. something containing secret key material or such */ } JsonParseFlags; From b3c05e5ade17a0bbb2695b00e86863bb2863d763 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 25 Apr 2019 12:57:54 +0200 Subject: [PATCH 030/109] nspawn-oci: use new json_variant_strv() helper --- src/nspawn/nspawn-oci.c | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/nspawn/nspawn-oci.c b/src/nspawn/nspawn-oci.c index ba8b142c99ec3..782c03c539bc6 100644 --- a/src/nspawn/nspawn-oci.c +++ b/src/nspawn/nspawn-oci.c @@ -172,24 +172,13 @@ static int oci_env(const char *name, JsonVariant *v, JsonDispatchFlags flags, vo static int oci_args(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) { _cleanup_strv_free_ char **l = NULL; char ***value = userdata; - JsonVariant *e; int r; assert(value); - JSON_VARIANT_ARRAY_FOREACH(e, v) { - const char *n; - - if (!json_variant_is_string(e)) - return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL), - "Argument is not a string."); - - assert_se(n = json_variant_string(e)); - - r = strv_extend(&l, n); - if (r < 0) - return log_oom(); - } + r = json_variant_strv(v, &l); + if (r < 0) + return json_log(v, flags, r, "Cannot parse arguments as list of strings: %m"); if (strv_isempty(l)) return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL), From 4b388c26fc3f67c72720c5ef3b57c916899663d5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 17:42:00 +0200 Subject: [PATCH 031/109] json: add json_variant_set_field_string() and json_variant_set_field_unsigned() --- src/shared/json.c | 22 ++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 24 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index be37a80a82155..40538f2ada671 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1809,6 +1809,28 @@ int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *valu return 1; } +int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_string(&m, value); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + +int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t u) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_unsigned(&m, u); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + int json_variant_strv(JsonVariant *v, char ***ret) { char **l = NULL; size_t n, i; diff --git a/src/shared/json.h b/src/shared/json.h index 2dcab6c5e80ec..4a213944b47a5 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -172,6 +172,8 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha int json_variant_filter(JsonVariant **v, char **to_remove); int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value); +int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value); +int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t value); int json_variant_strv(JsonVariant *v, char ***ret); From 90123fc877b50c345078a30fc05ef312ad9a1dc3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 17:42:32 +0200 Subject: [PATCH 032/109] json: add json_variant_merge() helper --- src/shared/json.c | 63 +++++++++++++++++++++++++++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 65 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 40538f2ada671..46ed1e50aef76 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1831,6 +1831,69 @@ int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_ return json_variant_set_field(v, field, m); } +int json_variant_merge(JsonVariant **v, JsonVariant *m) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_free_ JsonVariant **array = NULL; + size_t v_elements, m_elements, i, k; + bool v_blank, m_blank; + int r; + + m = json_variant_dereference(m); + + v_blank = json_variant_is_blank_object(*v); + m_blank = json_variant_is_blank_object(m); + + if (!v_blank && !json_variant_is_object(*v)) + return -EINVAL; + if (!m_blank && !json_variant_is_object(m)) + return -EINVAL; + + if (m_blank) + return 0; /* nothing to do */ + + if (v_blank) { + json_variant_unref(*v); + *v = json_variant_ref(m); + return 1; + } + + v_elements = json_variant_elements(*v); + m_elements = json_variant_elements(m); + if (v_elements > SIZE_MAX - m_elements) /* overflow check */ + return -ENOMEM; + + array = new(JsonVariant*, v_elements + m_elements); + if (!array) + return -ENOMEM; + + k = 0; + for (i = 0; i < v_elements; i += 2) { + JsonVariant *u; + + u = json_variant_by_index(*v, i); + if (!json_variant_is_string(u)) + return -EINVAL; + + if (json_variant_by_key(m, json_variant_string(u))) + continue; /* skip if exists in second variant */ + + array[k++] = u; + array[k++] = json_variant_by_index(*v, i + 1); + } + + for (i = 0; i < m_elements; i++) + array[k++] = json_variant_by_index(m, i); + + r = json_variant_new_object(&w, array, k); + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(w); + + return 1; +} + int json_variant_strv(JsonVariant *v, char ***ret) { char **l = NULL; size_t n, i; diff --git a/src/shared/json.h b/src/shared/json.h index 4a213944b47a5..617bd955fca00 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -175,6 +175,8 @@ int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *valu int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value); int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t value); +int json_variant_merge(JsonVariant **v, JsonVariant *m); + int json_variant_strv(JsonVariant *v, char ***ret); typedef enum JsonParseFlags { From 90f7a5feaf6d42e16555a6d45a4cff4f8494837b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 15 May 2019 16:55:04 +0200 Subject: [PATCH 033/109] json: add concept of normalization Let's add a concept of normalization: as preparation for signing json records let's add a mechanism to bring JSON records into a well-defined order so that we can safely validate JSON records. This adds two booleans to each JsonVariant object: "sorted" and "normalized". The latter indicates whether a variant is fully sorted (i.e. all keys of objects listed in alphabetical order) recursively down the tree. The former is a weaker property: it only checks whether the keys of the object itself are sorted. All variants which are "normalized" are also "sorted", but not vice versa. The knowledge of the "sorted" property is then used to optimize searching for keys in the variant by using bisection. Both properties are determined at the moment the variants are allocated. Since our objects are immutable this is safe. --- src/shared/json.c | 237 +++++++++++++++++++++++++++++++++++++++---- src/shared/json.h | 5 + src/test/test-json.c | 92 ++++++++++++++++- 3 files changed, 314 insertions(+), 20 deletions(-) diff --git a/src/shared/json.c b/src/shared/json.c index 46ed1e50aef76..c353eb92d123b 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -72,9 +72,15 @@ struct JsonVariant { /* While comparing two arrays, we use this for marking what we already have seen */ bool is_marked:1; - /* Ersase from memory when freeing */ + /* Erase from memory when freeing */ bool sensitive:1; + /* If this is an object the fields are strictly ordered by name */ + bool sorted:1; + + /* If in addition to this object all objects referenced by it are also ordered strictly by name */ + bool normalized:1; + /* The current 'depth' of the JsonVariant, i.e. how many levels of member variants this has */ uint16_t depth; @@ -216,10 +222,10 @@ static uint16_t json_variant_depth(JsonVariant *v) { return v->depth; } -static JsonVariant *json_variant_normalize(JsonVariant *v) { +static JsonVariant *json_variant_formalize(JsonVariant *v) { - /* Converts json variants to their normalized form, i.e. fully dereferenced and wherever possible converted to - * the "magic" version if there is one */ + /* Converts json variant pointers to their normalized form, i.e. fully dereferenced and wherever + * possible converted to the "magic" version if there is one */ if (!v) return NULL; @@ -260,9 +266,9 @@ static JsonVariant *json_variant_normalize(JsonVariant *v) { } } -static JsonVariant *json_variant_conservative_normalize(JsonVariant *v) { +static JsonVariant *json_variant_conservative_formalize(JsonVariant *v) { - /* Much like json_variant_normalize(), but won't simplify if the variant has a source/line location attached to + /* Much like json_variant_formalize(), but won't simplify if the variant has a source/line location attached to * it, in order not to lose context */ if (!v) @@ -274,7 +280,7 @@ static JsonVariant *json_variant_conservative_normalize(JsonVariant *v) { if (v->source || v->line > 0 || v->column > 0) return v; - return json_variant_normalize(v); + return json_variant_formalize(v); } static int json_variant_new(JsonVariant **ret, JsonVariantType type, size_t space) { @@ -452,7 +458,7 @@ static void json_variant_set(JsonVariant *a, JsonVariant *b) { case JSON_VARIANT_ARRAY: case JSON_VARIANT_OBJECT: a->is_reference = true; - a->reference = json_variant_ref(json_variant_conservative_normalize(b)); + a->reference = json_variant_ref(json_variant_conservative_formalize(b)); break; case JSON_VARIANT_NULL: @@ -477,6 +483,7 @@ static void json_variant_copy_source(JsonVariant *v, JsonVariant *from) { int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + bool normalized = true; assert_return(ret, -EINVAL); if (n == 0) { @@ -512,8 +519,13 @@ int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) { json_variant_set(w, c); json_variant_copy_source(w, c); + + if (!json_variant_is_normalized(c)) + normalized = false; } + v->normalized = normalized; + *ret = TAKE_PTR(v); return 0; } @@ -551,6 +563,8 @@ int json_variant_new_array_bytes(JsonVariant **ret, const void *p, size_t n) { }; } + v->normalized = true; + *ret = v; return 0; } @@ -602,12 +616,16 @@ int json_variant_new_array_strv(JsonVariant **ret, char **l) { memcpy(w->string, l[v->n_elements], k+1); } + v->normalized = true; + *ret = TAKE_PTR(v); return 0; } int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + const char *prev = NULL; + bool sorted = true, normalized = true; assert_return(ret, -EINVAL); if (n == 0) { @@ -631,9 +649,20 @@ int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) { *c = array[v->n_elements]; uint16_t d; - if ((v->n_elements & 1) == 0 && - !json_variant_is_string(c)) - return -EINVAL; /* Every second one needs to be a string, as it is the key name */ + if ((v->n_elements & 1) == 0) { + const char *k; + + if (!json_variant_is_string(c)) + return -EINVAL; /* Every second one needs to be a string, as it is the key name */ + + assert_se(k = json_variant_string(c)); + + if (prev && strcmp(k, prev) <= 0) + sorted = normalized = false; + + prev = k; + } else if (!json_variant_is_normalized(c)) + normalized = false; d = json_variant_depth(c); if (d >= DEPTH_MAX) /* Refuse too deep nesting */ @@ -650,6 +679,9 @@ int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) { json_variant_copy_source(w, c); } + v->normalized = normalized; + v->sorted = sorted; + *ret = TAKE_PTR(v); return 0; } @@ -1126,7 +1158,7 @@ JsonVariant *json_variant_by_index(JsonVariant *v, size_t idx) { if (idx >= v->n_elements) return NULL; - return json_variant_conservative_normalize(v + 1 + idx); + return json_variant_conservative_formalize(v + 1 + idx); mismatch: log_debug("Element in non-array/non-object JSON variant requested by index, returning NULL."); @@ -1149,6 +1181,37 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria if (v->is_reference) return json_variant_by_key(v->reference, key); + if (v->sorted) { + size_t a = 0, b = v->n_elements/2; + + /* If the variant is sorted we can use bisection to find the entry we need in O(log(n)) time */ + + while (b > a) { + JsonVariant *p; + const char *f; + int c; + + i = (a + b) / 2; + p = json_variant_dereference(v + 1 + i*2); + + assert_se(f = json_variant_string(p)); + + c = strcmp(key, f); + if (c == 0) { + if (ret_key) + *ret_key = json_variant_conservative_formalize(v + 1 + i*2); + + return json_variant_conservative_formalize(v + 1 + i*2 + 1); + } else if (c < 0) + b = i; + else + a = i + 1; + } + + goto not_found; + } + + /* The variant is not sorted, hence search for the field linearly */ for (i = 0; i < v->n_elements; i += 2) { JsonVariant *p; @@ -1160,9 +1223,9 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria if (streq(json_variant_string(p), key)) { if (ret_key) - *ret_key = json_variant_conservative_normalize(v + 1 + i); + *ret_key = json_variant_conservative_formalize(v + 1 + i); - return json_variant_conservative_normalize(v + 1 + i + 1); + return json_variant_conservative_formalize(v + 1 + i + 1); } } @@ -1187,8 +1250,8 @@ JsonVariant *json_variant_by_key(JsonVariant *v, const char *key) { bool json_variant_equal(JsonVariant *a, JsonVariant *b) { JsonVariantType t; - a = json_variant_normalize(a); - b = json_variant_normalize(b); + a = json_variant_formalize(a); + b = json_variant_formalize(b); if (a == b) return true; @@ -1300,7 +1363,7 @@ void json_variant_sensitive(JsonVariant *v) { * flag to all contained variants. And if those are then destroyed this is propagated further down, * and so on. */ - v = json_variant_normalize(v); + v = json_variant_formalize(v); if (!json_variant_is_regular(v)) return; @@ -2017,7 +2080,7 @@ static int json_variant_copy(JsonVariant **nv, JsonVariant *v) { c->n_ref = 1; c->type = t; c->is_reference = true; - c->reference = json_variant_ref(json_variant_normalize(v)); + c->reference = json_variant_ref(json_variant_formalize(v)); *nv = c; return 0; @@ -3819,6 +3882,144 @@ int json_dispatch_variant(const char *name, JsonVariant *variant, JsonDispatchFl return 0; } +static int json_cmp_strings(const void *x, const void *y) { + JsonVariant *const *a = x, *const *b = y; + + if (!json_variant_is_string(*a) || !json_variant_is_string(*b)) + return CMP(*a, *b); + + return strcmp(json_variant_string(*a), json_variant_string(*b)); +} + +int json_variant_sort(JsonVariant **v) { + _cleanup_free_ JsonVariant **a = NULL; + JsonVariant *n = NULL; + size_t i, m; + int r; + + assert(v); + + if (json_variant_is_sorted(*v)) + return 0; + + if (!json_variant_is_object(*v)) + return -EMEDIUMTYPE; + + /* Sorts they key/value pairs in an object variant */ + + m = json_variant_elements(*v); + a = new(JsonVariant*, m); + if (!a) + return -ENOMEM; + + for (i = 0; i < m; i++) + a[i] = json_variant_by_index(*v, i); + + qsort(a, m/2, sizeof(JsonVariant*)*2, json_cmp_strings); + + r = json_variant_new_object(&n, a, m); + if (r < 0) + return r; + if (!n->sorted) /* Check if this worked. This will fail if there are multiple identical keys used. */ + return -ENOTUNIQ; + + json_variant_unref(*v); + *v = n; + + return 1; +} + +int json_variant_normalize(JsonVariant **v) { + _cleanup_free_ JsonVariant **a = NULL; + JsonVariant *n = NULL; + size_t i, j, m; + int r; + + assert(v); + + if (json_variant_is_normalized(*v)) + return 0; + + if (!json_variant_is_object(*v) && !json_variant_is_array(*v)) + return -EMEDIUMTYPE; + + /* Sorts the key/value pairs in an object variant anywhere down the tree in the specified variant */ + + m = json_variant_elements(*v); + a = new(JsonVariant*, m); + if (!a) + return -ENOMEM; + + for (i = 0; i < m; i++) { + a[i] = json_variant_ref(json_variant_by_index(*v, i)); + + r = json_variant_normalize(a + i); + if (r < 0) + goto finish; + } + + qsort(a, m/2, sizeof(JsonVariant*)*2, json_cmp_strings); + + if (json_variant_is_object(*v)) + r = json_variant_new_object(&n, a, m); + else { + assert(json_variant_is_array(*v)); + r = json_variant_new_array(&n, a, m); + } + if (r < 0) + goto finish; + if (!n->normalized) { /* Let's see if normalization worked. It will fail if there are multiple + * identical keys used in the same object anywhere, or if there are floating + * point numbers used (see below) */ + r = -ENOTUNIQ; + goto finish; + } + + json_variant_unref(*v); + *v = n; + + r = 1; + +finish: + for (j = 0; j < i; j++) + json_variant_unref(a[j]); + + return r; +} + +bool json_variant_is_normalized(JsonVariant *v) { + + /* For now, let's consider anything containing numbers not expressible as integers as + * non-normalized. That's because we cannot sensibly compare them due to accuracy issues, nor even + * store them if they are too large. */ + if (json_variant_is_real(v) && !json_variant_is_integer(v) && !json_variant_is_unsigned(v)) + return false; + + /* The concept only applies to variants that include other variants, i.e. objects and arrays. All + * others are normalized anyway. */ + if (!json_variant_is_object(v) && !json_variant_is_array(v)) + return true; + + /* Empty objects/arrays don't include any other variant, hence are always normalized too */ + if (json_variant_elements(v) == 0) + return true; + + return v->normalized; /* For everything else there's an explicit boolean we maintain */ +} + +bool json_variant_is_sorted(JsonVariant *v) { + + /* Returns true if all key/value pairs of an object are properly sorted. Note that this only applies + * to objects, not arrays. */ + + if (!json_variant_is_object(v)) + return true; + if (json_variant_elements(v) <= 1) + return true; + + return v->sorted; +} + static const char* const json_variant_type_table[_JSON_VARIANT_TYPE_MAX] = { [JSON_VARIANT_STRING] = "string", [JSON_VARIANT_INTEGER] = "integer", diff --git a/src/shared/json.h b/src/shared/json.h index 617bd955fca00..294af4d8a9465 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -122,6 +122,8 @@ static inline bool json_variant_is_null(JsonVariant *v) { bool json_variant_is_negative(JsonVariant *v); bool json_variant_is_blank_object(JsonVariant *v); +bool json_variant_is_normalized(JsonVariant *v); +bool json_variant_is_sorted(JsonVariant *v); size_t json_variant_elements(JsonVariant *v); JsonVariant *json_variant_by_index(JsonVariant *v, size_t index); @@ -179,6 +181,9 @@ int json_variant_merge(JsonVariant **v, JsonVariant *m); int json_variant_strv(JsonVariant *v, char ***ret); +int json_variant_sort(JsonVariant **v); +int json_variant_normalize(JsonVariant **v); + typedef enum JsonParseFlags { JSON_PARSE_SENSITIVE = 1 << 0, /* mark variant as "sensitive", i.e. something containing secret key material or such */ } JsonParseFlags; diff --git a/src/test/test-json.c b/src/test/test-json.c index 90b91f6310c80..c72cd7427432c 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -415,6 +415,93 @@ static void test_depth(void) { fputs("\n", stdout); } +static void test_normalize(void) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL; + _cleanup_free_ char *t = NULL; + + assert_se(json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("b", JSON_BUILD_STRING("x")), + JSON_BUILD_PAIR("c", JSON_BUILD_STRING("y")), + JSON_BUILD_PAIR("a", JSON_BUILD_STRING("z")))) >= 0); + + assert_se(!json_variant_is_sorted(v)); + assert_se(!json_variant_is_normalized(v)); + + assert_se(json_variant_format(v, 0, &t) >= 0); + assert_se(streq(t, "{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}")); + t = mfree(t); + + assert_se(json_build(&w, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("bar", JSON_BUILD_STRING("zzz")), + JSON_BUILD_PAIR("foo", JSON_BUILD_VARIANT(v)))) >= 0); + + assert_se(json_variant_is_sorted(w)); + assert_se(!json_variant_is_normalized(w)); + + assert_se(json_variant_format(w, 0, &t) >= 0); + assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}}")); + t = mfree(t); + + assert_se(json_variant_sort(&v) >= 0); + assert_se(json_variant_is_sorted(v)); + assert_se(json_variant_is_normalized(v)); + + assert_se(json_variant_format(v, 0, &t) >= 0); + assert_se(streq(t, "{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}")); + t = mfree(t); + + assert_se(json_variant_normalize(&w) >= 0); + assert_se(json_variant_is_sorted(w)); + assert_se(json_variant_is_normalized(w)); + + assert_se(json_variant_format(w, 0, &t) >= 0); + assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}}")); + t = mfree(t); +} + +static void test_bisect(void) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + char c; + + /* Tests the bisection logic in json_variant_by_key() */ + + for (c = 'z'; c >= 'a'; c--) { + + if ((c % 3) == 0) + continue; + + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + assert_se(json_variant_new_stringn(&w, (char[4]) { '<', c, c, '>' }, 4) >= 0); + assert_se(json_variant_set_field(&v, (char[2]) { c, 0 }, w) >= 0); + } + + json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL); + + assert_se(!json_variant_is_sorted(v)); + assert_se(!json_variant_is_normalized(v)); + assert_se(json_variant_normalize(&v) >= 0); + assert_se(json_variant_is_sorted(v)); + assert_se(json_variant_is_normalized(v)); + + json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL); + + for (c = 'a'; c <= 'z'; c++) { + JsonVariant *k; + const char *z; + + k = json_variant_by_key(v, (char[2]) { c, 0 }); + assert_se(!k == ((c % 3) == 0)); + + if (!k) + continue; + + assert_se(json_variant_is_string(k)); + + z = (char[5]){ '<', c, c, '>', 0}; + assert_se(streq(json_variant_string(k), z)); + } +} + int main(int argc, char *argv[]) { test_setup_logging(LOG_DEBUG); @@ -462,10 +549,11 @@ int main(int argc, char *argv[]) { test_variant("[ 0, -0, 0.0, -0.0, 0.000, -0.000, 0e0, -0e0, 0e+0, -0e-0, 0e-0, -0e000, 0e+000 ]", test_zeroes); test_build(); - test_source(); - test_depth(); + test_normalize(); + test_bisect(); + return 0; } From db42e52a880aae9b0c1c3594319e6e02c7bf2c4e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 17:56:00 +0200 Subject: [PATCH 034/109] json: add new helper json_variant_new_base64() --- src/shared/json.c | 14 ++++++++++++++ src/shared/json.h | 1 + 2 files changed, 15 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index c353eb92d123b..1ee85040ccbd4 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -412,6 +412,20 @@ int json_variant_new_stringn(JsonVariant **ret, const char *s, size_t n) { return 0; } +int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n) { + _cleanup_free_ char *s = NULL; + ssize_t k; + + assert_return(ret, -EINVAL); + assert_return(n == 0 || p, -EINVAL); + + k = base64mem(p, n, &s); + if (k < 0) + return k; + + return json_variant_new_stringn(ret, s, k); +} + static void json_variant_set(JsonVariant *a, JsonVariant *b) { assert(a); diff --git a/src/shared/json.h b/src/shared/json.h index 294af4d8a9465..eed0fa0d61754 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -55,6 +55,7 @@ typedef enum JsonVariantType { } JsonVariantType; int json_variant_new_stringn(JsonVariant **ret, const char *s, size_t n); +int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n); int json_variant_new_integer(JsonVariant **ret, intmax_t i); int json_variant_new_unsigned(JsonVariant **ret, uintmax_t u); int json_variant_new_real(JsonVariant **ret, long double d); From 3f44959d4612151a7df2397adaaba019656f278e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 17:56:16 +0200 Subject: [PATCH 035/109] json: add new helper json_variant_append_array() --- src/shared/json.c | 43 +++++++++++++++++++++++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 45 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 1ee85040ccbd4..29239e46e0900 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1971,6 +1971,49 @@ int json_variant_merge(JsonVariant **v, JsonVariant *m) { return 1; } +int json_variant_append_array(JsonVariant **v, JsonVariant *element) { + _cleanup_(json_variant_unrefp) JsonVariant *nv = NULL; + bool blank; + int r; + + assert(v); + assert(element); + + + if (!*v || json_variant_is_null(*v)) + blank = true; + else if (!json_variant_is_array(*v)) + return -EINVAL; + else + blank = json_variant_elements(*v) == 0; + + if (blank) + r = json_variant_new_array(&nv, (JsonVariant*[]) { element }, 1); + else { + _cleanup_free_ JsonVariant **array = NULL; + size_t i; + + array = new(JsonVariant*, json_variant_elements(*v)); + if (array) + return -ENOMEM; + + for (i = 0; i < json_variant_elements(*v); i++) + array[i] = json_variant_by_index(*v, i); + + array[i] = element; + + r = json_variant_new_array(&nv, array, i + 1); + } + + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(nv); + + return 0; +} + int json_variant_strv(JsonVariant *v, char ***ret) { char **l = NULL; size_t n, i; diff --git a/src/shared/json.h b/src/shared/json.h index eed0fa0d61754..43a18256bb639 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -178,6 +178,8 @@ int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *valu int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value); int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t value); +int json_variant_append_array(JsonVariant **v, JsonVariant *element); + int json_variant_merge(JsonVariant **v, JsonVariant *m); int json_variant_strv(JsonVariant *v, char ***ret); From d8b214e02bd84daedd40e9758f10062676a4183e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 17:56:42 +0200 Subject: [PATCH 036/109] json: allow putting together base64 fields with json_build() --- src/shared/json.c | 30 ++++++++++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 32 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 29239e46e0900..fc4c8d634ae18 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -3457,6 +3457,36 @@ int json_buildv(JsonVariant **ret, va_list ap) { break; } + case _JSON_BUILD_BASE64: { + const void *p; + size_t n; + + if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { + r = -EINVAL; + goto finish; + } + + p = va_arg(ap, const void *); + n = va_arg(ap, size_t); + + if (current->n_suppress == 0) { + r = json_variant_new_base64(&add, p, n); + if (r < 0) + goto finish; + } + + n_subtract = 1; + + if (current->expect == EXPECT_TOPLEVEL) + current->expect = EXPECT_END; + else if (current->expect == EXPECT_OBJECT_VALUE) + current->expect = EXPECT_OBJECT_KEY; + else + assert(current->expect == EXPECT_ARRAY_ELEMENT); + + break; + } + case _JSON_BUILD_OBJECT_BEGIN: if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { diff --git a/src/shared/json.h b/src/shared/json.h index 43a18256bb639..be926522e6220 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -215,6 +215,7 @@ enum { _JSON_BUILD_VARIANT, _JSON_BUILD_LITERAL, _JSON_BUILD_STRV, + _JSON_BUILD_BASE64, _JSON_BUILD_MAX, }; @@ -231,6 +232,7 @@ enum { #define JSON_BUILD_VARIANT(v) _JSON_BUILD_VARIANT, ({ JsonVariant *_x = v; _x; }) #define JSON_BUILD_LITERAL(l) _JSON_BUILD_LITERAL, ({ const char *_x = l; _x; }) #define JSON_BUILD_STRV(l) _JSON_BUILD_STRV, ({ char **_x = l; _x; }) +#define JSON_BUILD_BASE64(p, n) _JSON_BUILD_BASE64, ({ const void *_x = p; _x; }), ({ size_t _y = n; _y; }) int json_build(JsonVariant **ret, ...); int json_buildv(JsonVariant **ret, va_list ap); From a9ba9ca9a736d4d789f2c2bea7c6bb33db004fd6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 May 2019 20:59:04 +0200 Subject: [PATCH 037/109] json: add ability to generate empty arrays/objects in json builder --- src/shared/json.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/json.h b/src/shared/json.h index be926522e6220..a748722df785e 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -225,7 +225,9 @@ enum { #define JSON_BUILD_REAL(d) _JSON_BUILD_REAL, ({ long double _x = d; _x; }) #define JSON_BUILD_BOOLEAN(b) _JSON_BUILD_BOOLEAN, ({ bool _x = b; _x; }) #define JSON_BUILD_ARRAY(...) _JSON_BUILD_ARRAY_BEGIN, __VA_ARGS__, _JSON_BUILD_ARRAY_END +#define JSON_BUILD_EMPTY_ARRAY _JSON_BUILD_ARRAY_BEGIN, _JSON_BUILD_ARRAY_END #define JSON_BUILD_OBJECT(...) _JSON_BUILD_OBJECT_BEGIN, __VA_ARGS__, _JSON_BUILD_OBJECT_END +#define JSON_BUILD_EMPTY_OBJECT _JSON_BUILD_OBJECT_BEGIN, _JSON_BUILD_OBJECT_END #define JSON_BUILD_PAIR(n, ...) _JSON_BUILD_PAIR, ({ const char *_x = n; _x; }), __VA_ARGS__ #define JSON_BUILD_PAIR_CONDITION(c, n, ...) _JSON_BUILD_PAIR_CONDITION, ({ bool _x = c; _x; }), ({ const char *_x = n; _x; }), __VA_ARGS__ #define JSON_BUILD_NULL _JSON_BUILD_NULL From ae561bb95fcb44cd0e1b414f905245f79b9106c6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 24 May 2019 10:44:56 +0200 Subject: [PATCH 038/109] json: permit 'null' as a way to reset tri-states to default --- src/shared/json.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index fc4c8d634ae18..08f0577d3ca3e 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -3821,6 +3821,11 @@ int json_dispatch_tristate(const char *name, JsonVariant *variant, JsonDispatchF assert(variant); assert(b); + if (json_variant_is_null(variant)) { + *b = -1; + return 0; + } + if (!json_variant_is_boolean(variant)) return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a boolean.", strna(name)); From bce7c7a6c1060f017bf7f13b5aff660899194326 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 28 May 2019 15:05:53 +0200 Subject: [PATCH 039/109] json: add explicit log call for ENOMEM --- src/shared/json.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/json.h b/src/shared/json.h index a748722df785e..bafa2ee4215f7 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -310,6 +310,9 @@ int json_log_internal(JsonVariant *variant, int level, int error, const char *fi : -ERRNO_VALUE(_e); \ }) +#define json_log_oom(variant, flags) \ + json_log(variant, flags, SYNTHETIC_ERRNO(ENOMEM), "Out of memory.") + #define JSON_VARIANT_STRING_CONST(x) _JSON_VARIANT_STRING_CONST(UNIQ, (x)) #define _JSON_VARIANT_STRING_CONST(xq, x) \ From f8a6c47f07362ed0aba32d3d085535b0e9353f0c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 29 May 2019 12:24:40 +0200 Subject: [PATCH 040/109] json: add new flag for forcing a flush after dumping json data to file This is particularly useful when no trailing \n is generated, i.e. stdio doesn't flush the output on its own. --- src/shared/json.c | 3 +++ src/shared/json.h | 1 + 2 files changed, 4 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 08f0577d3ca3e..0c2bbc6edb568 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1762,6 +1762,9 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha fputc('\n', f); if (flags & JSON_FORMAT_SSE) fputc('\n', f); /* In case of SSE add a second newline */ + + if (flags & JSON_FORMAT_FLUSH) + fflush(f); } int json_variant_filter(JsonVariant **v, char **to_remove) { diff --git a/src/shared/json.h b/src/shared/json.h index bafa2ee4215f7..e3a114d7a5e7d 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -167,6 +167,7 @@ typedef enum JsonFormatFlags { JSON_FORMAT_SOURCE = 1 << 4, /* prefix with source filename/line/column */ JSON_FORMAT_SSE = 1 << 5, /* prefix/suffix with W3C server-sent events */ JSON_FORMAT_SEQ = 1 << 6, /* prefix/suffix with RFC 7464 application/json-seq */ + JSON_FORMAT_FLUSH = 1 << 7, /* call fflush() after dumping JSON */ } JsonFormatFlags; int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret); From 243dc1003c59c0e22706ab5475313b4eae85195b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 28 Jun 2019 20:05:21 +0200 Subject: [PATCH 041/109] json: add json_variant_unbase64() helper --- src/shared/json.c | 8 ++++++++ src/shared/json.h | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 0c2bbc6edb568..499c34f675eb2 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -4115,6 +4115,14 @@ bool json_variant_is_sorted(JsonVariant *v) { return v->sorted; } +int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size) { + + if (!json_variant_is_string(v)) + return -EINVAL; + + return unbase64mem(json_variant_string(v), (size_t) -1, ret, ret_size); +} + static const char* const json_variant_type_table[_JSON_VARIANT_TYPE_MAX] = { [JSON_VARIANT_STRING] = "string", [JSON_VARIANT_INTEGER] = "integer", diff --git a/src/shared/json.h b/src/shared/json.h index e3a114d7a5e7d..ab8f5d1e087c0 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -323,5 +323,7 @@ int json_log_internal(JsonVariant *variant, int level, int error, const char *fi (JsonVariant*) ((uintptr_t) UNIQ_T(json_string_const, xq) + 1); \ }) +int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size); + const char *json_variant_type_to_string(JsonVariantType t); JsonVariantType json_variant_type_from_string(const char *s); From 31efe31459207dea64ed8f7e22c31e10bc018f01 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:27:02 +0200 Subject: [PATCH 042/109] json: add json_variant_set_field_integer() and json_variant_set_field_boolean() helpers --- src/shared/json.c | 22 ++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 24 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 499c34f675eb2..e1c9675a820ac 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1900,6 +1900,17 @@ int json_variant_set_field_string(JsonVariant **v, const char *field, const char return json_variant_set_field(v, field, m); } +int json_variant_set_field_integer(JsonVariant **v, const char *field, intmax_t i) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_integer(&m, i); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t u) { _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; int r; @@ -1911,6 +1922,17 @@ int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_ return json_variant_set_field(v, field, m); } +int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_boolean(&m, b); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + int json_variant_merge(JsonVariant **v, JsonVariant *m) { _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; _cleanup_free_ JsonVariant **array = NULL; diff --git a/src/shared/json.h b/src/shared/json.h index ab8f5d1e087c0..512463baa64be 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -177,7 +177,9 @@ int json_variant_filter(JsonVariant **v, char **to_remove); int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value); int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value); +int json_variant_set_field_integer(JsonVariant **v, const char *field, intmax_t value); int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t value); +int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b); int json_variant_append_array(JsonVariant **v, JsonVariant *element); From fbcef7e7510c11d89d0fd8c28bbb8a9b86554ae2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:27:12 +0200 Subject: [PATCH 043/109] json: add more dispatch helpers --- src/shared/json.c | 76 +++++++++++++++++++++++++++++++++++++++++++++++ src/shared/json.h | 4 +++ 2 files changed, 80 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index e1c9675a820ac..92d0c5385eb4a 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -24,6 +24,7 @@ #include "string-util.h" #include "strv.h" #include "terminal-util.h" +#include "user-util.h" #include "utf8.h" /* Refuse putting together variants with a larger depth than 4K by default (as a protection against overflowing stacks @@ -3999,6 +4000,81 @@ int json_dispatch_variant(const char *name, JsonVariant *variant, JsonDispatchFl return 0; } +int json_dispatch_uid_gid(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uid_t *uid = userdata; + uintmax_t k; + + assert_cc(sizeof(uid_t) == sizeof(uint32_t)); + assert_cc(sizeof(gid_t) == sizeof(uint32_t)); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" + assert_cc(((uid_t) -1 < (uid_t) 0) == ((gid_t) -1 < (gid_t) 0)); +#pragma GCC diagnostic pop + + if (json_variant_is_null(variant)) { + *uid = UID_INVALID; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name)); + + k = json_variant_unsigned(variant); + if (k > UINT32_MAX || !uid_is_valid(k)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid UID/GID.", strna(name)); + + *uid = k; + return 0; +} + +int json_dispatch_user_group_name(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!valid_user_group_name(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid user/group name.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +int json_dispatch_id128(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + sd_id128_t *uuid = userdata; + int r; + + if (json_variant_is_null(variant)) { + *uuid = SD_ID128_NULL; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = sd_id128_from_string(json_variant_string(variant), uuid); + if (r < 0) + return json_log(variant, flags, r, "JSON field '%s' is not a valid UID.", strna(name)); + + return 0; +} + +int json_dispatch_unsupported(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not allowed in this object.", strna(name)); +} + static int json_cmp_strings(const void *x, const void *y) { JsonVariant *const *a = x, *const *b = y; diff --git a/src/shared/json.h b/src/shared/json.h index 512463baa64be..0612d4c50bdbd 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -278,6 +278,10 @@ int json_dispatch_integer(const char *name, JsonVariant *variant, JsonDispatchFl int json_dispatch_unsigned(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_uint32(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_int32(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_uid_gid(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_user_group_name(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_id128(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_unsupported(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); assert_cc(sizeof(uintmax_t) == sizeof(uint64_t)); #define json_dispatch_uint64 json_dispatch_unsigned From abcc2df227ff39e08a109abca08bcbcedb56afe8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 29 Jul 2019 09:12:28 +0200 Subject: [PATCH 044/109] json: teach json_build() to build arrays from C arrays of JsonVariant --- src/shared/json.c | 30 ++++++++++++++++++++++++++++++ src/shared/json.h | 2 ++ 2 files changed, 32 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 92d0c5385eb4a..5af60e2ac68d3 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -3372,6 +3372,36 @@ int json_buildv(JsonVariant **ret, va_list ap) { break; + case _JSON_BUILD_VARIANT_ARRAY: { + JsonVariant **array; + size_t n; + + if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { + r = -EINVAL; + goto finish; + } + + array = va_arg(ap, JsonVariant**); + n = va_arg(ap, size_t); + + if (current->n_suppress == 0) { + r = json_variant_new_array(&add, array, n); + if (r < 0) + goto finish; + } + + n_subtract = 1; + + if (current->expect == EXPECT_TOPLEVEL) + current->expect = EXPECT_END; + else if (current->expect == EXPECT_OBJECT_VALUE) + current->expect = EXPECT_OBJECT_KEY; + else + assert(current->expect == EXPECT_ARRAY_ELEMENT); + + break; + } + case _JSON_BUILD_LITERAL: { const char *l; diff --git a/src/shared/json.h b/src/shared/json.h index 0612d4c50bdbd..888289822d8b5 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -216,6 +216,7 @@ enum { _JSON_BUILD_PAIR_CONDITION, _JSON_BUILD_NULL, _JSON_BUILD_VARIANT, + _JSON_BUILD_VARIANT_ARRAY, _JSON_BUILD_LITERAL, _JSON_BUILD_STRV, _JSON_BUILD_BASE64, @@ -235,6 +236,7 @@ enum { #define JSON_BUILD_PAIR_CONDITION(c, n, ...) _JSON_BUILD_PAIR_CONDITION, ({ bool _x = c; _x; }), ({ const char *_x = n; _x; }), __VA_ARGS__ #define JSON_BUILD_NULL _JSON_BUILD_NULL #define JSON_BUILD_VARIANT(v) _JSON_BUILD_VARIANT, ({ JsonVariant *_x = v; _x; }) +#define JSON_BUILD_VARIANT_ARRAY(v, n) _JSON_BUILD_VARIANT_ARRAY, ({ JsonVariant **_x = v; _x; }), ({ size_t _y = n; _y; }) #define JSON_BUILD_LITERAL(l) _JSON_BUILD_LITERAL, ({ const char *_x = l; _x; }) #define JSON_BUILD_STRV(l) _JSON_BUILD_STRV, ({ char **_x = l; _x; }) #define JSON_BUILD_BASE64(p, n) _JSON_BUILD_BASE64, ({ const void *_x = p; _x; }), ({ size_t _y = n; _y; }) From 7323d0e690cbc20b1302a1fa2fe50600b462ed3c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:45:50 +0200 Subject: [PATCH 045/109] json: add const string dispatcher This adds json_dispatch_const_string() which is similar to json_dispatch_string() but doesn't store a strdup()'ed copy of the string, but a pointer directly into the JSON record. This should simplify cases where the json variant sticks around long enough anyway. --- src/shared/json.c | 21 +++++++++++++++++++++ src/shared/json.h | 1 + 2 files changed, 22 insertions(+) diff --git a/src/shared/json.c b/src/shared/json.c index 5af60e2ac68d3..a4a6a57251e45 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -3972,6 +3972,27 @@ int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFla return 0; } +int json_dispatch_const_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + const char **s = userdata; + + assert(variant); + assert(s); + + if (json_variant_is_null(variant)) { + *s = NULL; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant))) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + + *s = json_variant_string(variant); + return 0; +} + int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { _cleanup_strv_free_ char **l = NULL; char ***s = userdata; diff --git a/src/shared/json.h b/src/shared/json.h index 888289822d8b5..1a1b273a0446b 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -272,6 +272,7 @@ typedef struct JsonDispatch { int json_dispatch(JsonVariant *v, const JsonDispatch table[], JsonDispatchCallback bad, JsonDispatchFlags flags, void *userdata); int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_const_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_boolean(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_tristate(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); From dedc45d506b2c1c1d61ef889bec07370e2cdc631 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 15 Aug 2019 09:36:04 +0200 Subject: [PATCH 046/109] json: add new output flag JSON_PRETTY_AUTO This takes inspiration from JSON_COLOR_AUTO: it will automatically map to JSON_PRETTY if connected to a TTY and JSON_NEWLINE otherwise. --- src/shared/json.c | 3 +++ src/shared/json.h | 17 +++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/shared/json.c b/src/shared/json.c index a4a6a57251e45..6222c188a1843 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -1752,6 +1752,9 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha if (((flags & (JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_COLOR)) == JSON_FORMAT_COLOR_AUTO) && colors_enabled()) flags |= JSON_FORMAT_COLOR; + if (((flags & (JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_PRETTY)) == JSON_FORMAT_PRETTY_AUTO)) + flags |= on_tty() ? JSON_FORMAT_PRETTY : JSON_FORMAT_NEWLINE; + if (flags & JSON_FORMAT_SSE) fputs("data: ", f); if (flags & JSON_FORMAT_SEQ) diff --git a/src/shared/json.h b/src/shared/json.h index 1a1b273a0446b..71f7e606b2086 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -160,14 +160,15 @@ struct json_variant_foreach_state { int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *ret_line, unsigned *ret_column); typedef enum JsonFormatFlags { - JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */ - JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */ - JSON_FORMAT_COLOR = 1 << 2, /* insert ANSI color sequences */ - JSON_FORMAT_COLOR_AUTO = 1 << 3, /* insert ANSI color sequences if colors_enabled() says so */ - JSON_FORMAT_SOURCE = 1 << 4, /* prefix with source filename/line/column */ - JSON_FORMAT_SSE = 1 << 5, /* prefix/suffix with W3C server-sent events */ - JSON_FORMAT_SEQ = 1 << 6, /* prefix/suffix with RFC 7464 application/json-seq */ - JSON_FORMAT_FLUSH = 1 << 7, /* call fflush() after dumping JSON */ + JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */ + JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */ + JSON_FORMAT_PRETTY_AUTO = 1 << 2, /* same, but only if connected to a tty (and JSON_FORMAT_NEWLINE otherwise) */ + JSON_FORMAT_COLOR = 1 << 3, /* insert ANSI color sequences */ + JSON_FORMAT_COLOR_AUTO = 1 << 4, /* insert ANSI color sequences if colors_enabled() says so */ + JSON_FORMAT_SOURCE = 1 << 5, /* prefix with source filename/line/column */ + JSON_FORMAT_SSE = 1 << 6, /* prefix/suffix with W3C server-sent events */ + JSON_FORMAT_SEQ = 1 << 7, /* prefix/suffix with RFC 7464 application/json-seq */ + JSON_FORMAT_FLUSH = 1 << 8, /* call fflush() after dumping JSON */ } JsonFormatFlags; int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret); From c3ca000cd577ac59b48632fb2ebf6854e6f6b370 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 19 Aug 2019 20:28:34 +0200 Subject: [PATCH 047/109] sd-bus: add new call sd_bus_message_sensitive() and SD_BUS_VTABLE_SENSITIVE This allows marking messages that contain "sensitive" data with a flag. If it's set then the messages are erased from memory when the message is freed. Similar, a flag may be set on vtable entries: incoming/outgoing message matching the entry will then automatically be flagged this way. This is supposed to be an easy method to mark messages containing potentially sensitive data (such as passwords) for proper destruction. (Note that this of course is only is as safe as the broker in between is doing something similar. But let's at least not be the ones at fault here.) --- src/libsystemd/libsystemd.sym | 5 ++++ src/libsystemd/sd-bus/bus-message.c | 37 ++++++++++++++++++++++++----- src/libsystemd/sd-bus/bus-message.h | 1 + src/libsystemd/sd-bus/bus-objects.c | 24 +++++++++++++++++++ src/systemd/sd-bus-vtable.h | 1 + src/systemd/sd-bus.h | 1 + 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/libsystemd/libsystemd.sym b/src/libsystemd/libsystemd.sym index 5ec42e0f1f826..13cca2db6c46e 100644 --- a/src/libsystemd/libsystemd.sym +++ b/src/libsystemd/libsystemd.sym @@ -682,3 +682,8 @@ global: sd_bus_object_vtable_format; sd_event_source_disable_unref; } LIBSYSTEMD_241; + +LIBSYSTEMD_244 { +global: + sd_bus_message_sensitive; +} LIBSYSTEMD_243; diff --git a/src/libsystemd/sd-bus/bus-message.c b/src/libsystemd/sd-bus/bus-message.c index eb029e4453268..0515064236a5d 100644 --- a/src/libsystemd/sd-bus/bus-message.c +++ b/src/libsystemd/sd-bus/bus-message.c @@ -45,12 +45,24 @@ static void message_free_part(sd_bus_message *m, struct bus_body_part *part) { assert(m); assert(part); - if (part->memfd >= 0) + if (part->memfd >= 0) { + /* erase this requested, but ony if the memfd is not sealed yet, i.e. is writable */ + if (m->sensitive && !m->sealed) + explicit_bzero_safe(part->data, part->size); + close_and_munmap(part->memfd, part->mmap_begin, part->mapped); - else if (part->munmap_this) + } else if (part->munmap_this) + /* We don't erase sensitive data here, since the data is memory mapped from someone else, and + * we just don't know if it's OK to write to it */ munmap(part->mmap_begin, part->mapped); - else if (part->free_this) - free(part->data); + else { + /* Erase this if that is requested. Since this is regular memory we know we can write it. */ + if (m->sensitive) + explicit_bzero_safe(part->data, part->size); + + if (part->free_this) + free(part->data); + } if (part != &m->body) free(part); @@ -113,11 +125,11 @@ static void message_reset_containers(sd_bus_message *m) { static sd_bus_message* message_free(sd_bus_message *m) { assert(m); + message_reset_parts(m); + if (m->free_header) free(m->header); - message_reset_parts(m); - /* Note that we don't unref m->bus here. That's already done by sd_bus_message_unref() as each user * reference to the bus message also is considered a reference to the bus connection itself. */ @@ -727,6 +739,12 @@ static int message_new_reply( t->dont_send = !!(call->header->flags & BUS_MESSAGE_NO_REPLY_EXPECTED); t->enforced_reply_signature = call->enforced_reply_signature; + /* let's copy the sensitive flag over. Let's do that as a safety precaution to keep a transaction + * wholly sensitive if already the incoming message was sensitive. This is particularly useful when a + * vtable record sets the SD_BUS_VTABLE_SENSITIVE flag on a method call, since this means it applies + * to both the message call and the reply. */ + t->sensitive = call->sensitive; + *m = TAKE_PTR(t); return 0; } @@ -5919,3 +5937,10 @@ _public_ int sd_bus_message_set_priority(sd_bus_message *m, int64_t priority) { m->priority = priority; return 0; } + +_public_ int sd_bus_message_sensitive(sd_bus_message *m) { + assert_return(m, -EINVAL); + + m->sensitive = true; + return 0; +} diff --git a/src/libsystemd/sd-bus/bus-message.h b/src/libsystemd/sd-bus/bus-message.h index ced0bb3d34a91..a88a531e15b82 100644 --- a/src/libsystemd/sd-bus/bus-message.h +++ b/src/libsystemd/sd-bus/bus-message.h @@ -85,6 +85,7 @@ struct sd_bus_message { bool free_header:1; bool free_fds:1; bool poisoned:1; + bool sensitive:1; /* The first and last bytes of the message */ struct bus_header *header; diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c index ae643cacc740f..26f93d21dab64 100644 --- a/src/libsystemd/sd-bus/bus-objects.c +++ b/src/libsystemd/sd-bus/bus-objects.c @@ -353,6 +353,12 @@ static int method_callbacks_run( if (require_fallback && !c->parent->is_fallback) return 0; + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(m); + if (r < 0) + return r; + } + r = check_access(bus, m, c, &error); if (r < 0) return bus_maybe_reply_error(m, r, &error); @@ -577,6 +583,12 @@ static int property_get_set_callbacks_run( if (require_fallback && !c->parent->is_fallback) return 0; + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(m); + if (r < 0) + return r; + } + r = vtable_property_get_userdata(bus, m->path, c, &u, &error); if (r <= 0) return bus_maybe_reply_error(m, r, &error); @@ -591,6 +603,12 @@ static int property_get_set_callbacks_run( if (r < 0) return r; + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(reply); + if (r < 0) + return r; + } + if (is_get) { /* Note that we do not protect against reexecution * here (using the last_iteration check, see below), @@ -692,6 +710,12 @@ static int vtable_append_one_property( assert(c); assert(v); + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(reply); + if (r < 0) + return r; + } + r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r; diff --git a/src/systemd/sd-bus-vtable.h b/src/systemd/sd-bus-vtable.h index 0f43554d82519..95b5914236a47 100644 --- a/src/systemd/sd-bus-vtable.h +++ b/src/systemd/sd-bus-vtable.h @@ -43,6 +43,7 @@ enum { SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE = 1ULL << 5, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION = 1ULL << 6, SD_BUS_VTABLE_PROPERTY_EXPLICIT = 1ULL << 7, + SD_BUS_VTABLE_SENSITIVE = 1ULL << 8, /* covers both directions: method call + reply */ _SD_BUS_VTABLE_CAPABILITY_MASK = 0xFFFFULL << 40 }; diff --git a/src/systemd/sd-bus.h b/src/systemd/sd-bus.h index 84ceb62dc79c7..3c9792e4975e2 100644 --- a/src/systemd/sd-bus.h +++ b/src/systemd/sd-bus.h @@ -328,6 +328,7 @@ int sd_bus_message_peek_type(sd_bus_message *m, char *type, const char **content int sd_bus_message_verify_type(sd_bus_message *m, char type, const char *contents); int sd_bus_message_at_end(sd_bus_message *m, int complete); int sd_bus_message_rewind(sd_bus_message *m, int complete); +int sd_bus_message_sensitive(sd_bus_message *m); /* Bus management */ From a65e6daeed1e2df2e7a336abf224a9d4742ec37b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 20 Aug 2019 15:35:53 +0200 Subject: [PATCH 048/109] sd-bus: don't include properties maked as "emit-invalidation" in InterfacesAdded signals Properties marked this way really shouldn't be sent around willy-nilly, that's what the flag is about, hence exclude it from InterfacesAdded signals (and in fact anything that is a signal). --- src/libsystemd/sd-bus/bus-objects.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c index 26f93d21dab64..474f8a6a1695c 100644 --- a/src/libsystemd/sd-bus/bus-objects.c +++ b/src/libsystemd/sd-bus/bus-objects.c @@ -774,9 +774,18 @@ static int vtable_append_all_properties( if (v->flags & SD_BUS_VTABLE_HIDDEN) continue; + /* Let's not include properties marked as "explicit" in any message that contians a generic + * dump of properties, but only in those genrated as a response to an explicit request. */ if (v->flags & SD_BUS_VTABLE_PROPERTY_EXPLICIT) continue; + /* Let's not include properties marked only for invalidation on change (i.e. in contrast to + * those whose new values are included in PropertiesChanges message) in any signals. This is + * useful to ensure they aren't included in InterfacesAdded messages. */ + if (reply->header->type != SD_BUS_MESSAGE_METHOD_RETURN && + FLAGS_SET(v->flags, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION)) + continue; + r = vtable_append_one_property(bus, reply, path, c, v, userdata, error); if (r < 0) return r; From 70522236c1a708f0ae79cc45b590f9a9f498331d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:33:33 +0200 Subject: [PATCH 049/109] nss-util: add macros for generating getpwent()/getgrent() prototypes We have similar macros already for getpwuid()/getpwnam(), let's add more of this. --- src/basic/nss-util.h | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/basic/nss-util.h b/src/basic/nss-util.h index 2045175d1cb3c..29cf22676ae6f 100644 --- a/src/basic/nss-util.h +++ b/src/basic/nss-util.h @@ -139,6 +139,38 @@ enum nss_status _nss_##module##_getgrgid_r( \ char *buffer, size_t buflen, \ int *errnop) _public_ +#define NSS_PWENT_PROTOTYPES(module) \ +enum nss_status _nss_##module##_endpwent( \ + void) _public_; \ +enum nss_status _nss_##module##_setpwent( \ + int stayopen) _public_; \ +enum nss_status _nss_##module##_getpwent_r( \ + struct passwd *result, \ + char *buffer, \ + size_t buflen, \ + int *errnop) _public_; + +#define NSS_GRENT_PROTOTYPES(module) \ +enum nss_status _nss_##module##_endgrent( \ + void) _public_; \ +enum nss_status _nss_##module##_setgrent( \ + int stayopen) _public_; \ +enum nss_status _nss_##module##_getgrent_r( \ + struct group *result, \ + char *buffer, \ + size_t buflen, \ + int *errnop) _public_; + +#define NSS_INITGROUPS_PROTOTYPE(module) \ +enum nss_status _nss_##module##_initgroups_dyn( \ + const char *user, \ + gid_t group, \ + long int *start, \ + long int *size, \ + gid_t **groupsp, \ + long int limit, \ + int *errnop) _public_; + typedef enum nss_status (*_nss_gethostbyname4_r_t)( const char *name, struct gaih_addrtuple **pat, From bb42c64a0f6756ca0442216b3014f0361e12e98d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:34:29 +0200 Subject: [PATCH 050/109] user-util: export is_nologin_shell() so that we can use it elsewhere --- src/basic/user-util.c | 2 +- src/basic/user-util.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 3b253bc264dd3..aca98329e2e2f 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -89,7 +89,7 @@ char *getusername_malloc(void) { return uid_to_name(getuid()); } -static bool is_nologin_shell(const char *shell) { +bool is_nologin_shell(const char *shell) { return PATH_IN_SET(shell, /* 'nologin' is the friendliest way to disable logins for a user account. It prints a nice diff --git a/src/basic/user-util.h b/src/basic/user-util.h index cfa515f5e8a26..d507c135ebd85 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -127,3 +127,5 @@ int putsgent_sane(const struct sgrp *sg, FILE *stream); #endif int make_salt(char **ret); + +bool is_nologin_shell(const char *shell); From 2c6bb5d56c34ffcaec1c76fd1cce5079e682ed47 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:34:46 +0200 Subject: [PATCH 051/109] user-util: add uid_is_container() for checking whether UID is in container range We have similar calls for the dynamic user and system range, let's add this too here. --- src/basic/user-util.h | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/basic/user-util.h b/src/basic/user-util.h index d507c135ebd85..7488071086ef1 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -57,6 +57,14 @@ int take_etc_passwd_lock(const char *root); #define ETC_PASSWD_LOCK_PATH "/etc/.pwd.lock" +static inline bool uid_is_system(uid_t uid) { + return uid <= SYSTEM_UID_MAX; +} + +static inline bool gid_is_system(gid_t gid) { + return gid <= SYSTEM_GID_MAX; +} + static inline bool uid_is_dynamic(uid_t uid) { return DYNAMIC_UID_MIN <= uid && uid <= DYNAMIC_UID_MAX; } @@ -65,12 +73,12 @@ static inline bool gid_is_dynamic(gid_t gid) { return uid_is_dynamic((uid_t) gid); } -static inline bool uid_is_system(uid_t uid) { - return uid <= SYSTEM_UID_MAX; +static inline bool uid_is_container(uid_t uid) { + return CONTAINER_UID_BASE_MIN <= uid && uid <= CONTAINER_UID_BASE_MAX; } -static inline bool gid_is_system(gid_t gid) { - return gid <= SYSTEM_GID_MAX; +static inline bool gid_is_container(gid_t gid) { + return uid_is_container((uid_t) gid); } /* The following macros add 1 when converting things, since UID 0 is a valid UID, while the pointer From b7c9c9bba1ea779750e99b449300a076423c5b6c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Aug 2019 09:50:56 +0200 Subject: [PATCH 052/109] user-util: add helper to check whether UNIX hashed password is valid --- src/basic/user-util.c | 12 ++++++++++++ src/basic/user-util.h | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/basic/user-util.c b/src/basic/user-util.c index aca98329e2e2f..70355554afb62 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -925,3 +925,15 @@ int make_salt(char **ret) { *ret = salt; return 0; } + +bool hashed_password_valid(const char *s) { + + /* Returns true if the specified string is a 'valid' hashed UNIX password, i.e. if starts with '$' or + * with '!$' (the latter being a valid, yet locked password). */ + + if (!s) + return false; + + return s[0] == '$' || + (s[0] == '!' && s[1] == '$'); +} diff --git a/src/basic/user-util.h b/src/basic/user-util.h index 7488071086ef1..87972bdca4a1e 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -137,3 +137,5 @@ int putsgent_sane(const struct sgrp *sg, FILE *stream); int make_salt(char **ret); bool is_nologin_shell(const char *shell); + +bool hashed_password_valid(const char *s); From c22c28ebdb3a981dc2177ed64eed67e180643a24 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 23 Apr 2019 15:23:48 +0200 Subject: [PATCH 053/109] tmpfile-util: if no path is passed to fopen_temporary() make one up Let's beef up functionality a bit, and modernize the whole function. --- src/basic/tmpfile-util.c | 46 ++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/basic/tmpfile-util.c b/src/basic/tmpfile-util.c index afcf58aeac8bb..14c974d65d22e 100644 --- a/src/basic/tmpfile-util.c +++ b/src/basic/tmpfile-util.c @@ -20,38 +20,48 @@ #include "tmpfile-util.h" #include "umask-util.h" -int fopen_temporary(const char *path, FILE **_f, char **_temp_path) { - FILE *f; - char *t; - int r, fd; +int fopen_temporary(const char *path, FILE **ret_f, char **ret_temp_path) { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *t = NULL; + _cleanup_close_ int fd = -1; + int r; - assert(path); - assert(_f); - assert(_temp_path); + if (path) { + r = tempfn_xxxxxx(path, NULL, &t); + if (r < 0) + return r; + } else { + const char *d; - r = tempfn_xxxxxx(path, NULL, &t); - if (r < 0) - return r; + r = tmp_dir(&d); + if (r < 0) + return r; + + t = path_join(d, "XXXXXX"); + if (!t) + return -ENOMEM; + } fd = mkostemp_safe(t); - if (fd < 0) { - free(t); + if (fd < 0) return -errno; - } /* This assumes that returned FILE object is short-lived and used within the same single-threaded * context and never shared externally, hence locking is not necessary. */ r = fdopen_unlocked(fd, "w", &f); if (r < 0) { - unlink(t); - free(t); - safe_close(fd); + (void) unlink(t); return r; } - *_f = f; - *_temp_path = t; + TAKE_FD(fd); + + if (ret_f) + *ret_f = TAKE_PTR(f); + + if (ret_temp_path) + *ret_temp_path = TAKE_PTR(t); return 0; } From 4ab7baef019767a0f73dddb1aab82cad89072b5d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 16:48:32 +0200 Subject: [PATCH 054/109] tmpfile-util: modernize mkostemp_safe() a bit --- src/basic/tmpfile-util.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/basic/tmpfile-util.c b/src/basic/tmpfile-util.c index 14c974d65d22e..2b9ce5ef7a8af 100644 --- a/src/basic/tmpfile-util.c +++ b/src/basic/tmpfile-util.c @@ -68,12 +68,12 @@ int fopen_temporary(const char *path, FILE **ret_f, char **ret_temp_path) { /* This is much like mkostemp() but is subject to umask(). */ int mkostemp_safe(char *pattern) { - _unused_ _cleanup_umask_ mode_t u = umask(0077); int fd; assert(pattern); - fd = mkostemp(pattern, O_CLOEXEC); + RUN_WITH_UMASK(0077) + fd = mkostemp(pattern, O_CLOEXEC); if (fd < 0) return -errno; From 1cfd6b4e2923df8454743e18c697bdcd3d166f96 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 6 May 2019 22:38:43 +0200 Subject: [PATCH 055/109] process-util: add new safe_fork() flag for connecting stdout to stderr This adds a new safe_fork() flag. If set the child process' fd 1 becomes fd 2 of the caller. This is useful for invoking tools (such as various mkfs/fsck implementations) that output status messages to stdout, but which we invoke and don't want to pollute stdout with their output. --- src/basic/process-util.c | 7 +++++++ src/basic/process-util.h | 21 +++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 5452edd7a4bc0..f8822a8309952 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -1319,6 +1319,13 @@ int safe_fork_full( log_full_errno(prio, r, "Failed to connect stdin/stdout to /dev/null: %m"); _exit(EXIT_FAILURE); } + + } else if (flags & FORK_STDOUT_TO_STDERR) { + + if (dup2(STDERR_FILENO, STDOUT_FILENO) < 0) { + log_full_errno(prio, r, "Failed to connect stdout to stderr: %m"); + _exit(EXIT_FAILURE); + } } if (flags & FORK_RLIMIT_NOFILE_SAFE) { diff --git a/src/basic/process-util.h b/src/basic/process-util.h index 41d4759c97136..dbe216cd9f100 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -148,16 +148,17 @@ void reset_cached_pid(void); int must_be_root(void); typedef enum ForkFlags { - FORK_RESET_SIGNALS = 1 << 0, /* Reset all signal handlers and signal mask */ - FORK_CLOSE_ALL_FDS = 1 << 1, /* Close all open file descriptors in the child, except for 0,1,2 */ - FORK_DEATHSIG = 1 << 2, /* Set PR_DEATHSIG in the child */ - FORK_NULL_STDIO = 1 << 3, /* Connect 0,1,2 to /dev/null */ - FORK_REOPEN_LOG = 1 << 4, /* Reopen log connection */ - FORK_LOG = 1 << 5, /* Log above LOG_DEBUG log level about failures */ - FORK_WAIT = 1 << 6, /* Wait until child exited */ - FORK_NEW_MOUNTNS = 1 << 7, /* Run child in its own mount namespace */ - FORK_MOUNTNS_SLAVE = 1 << 8, /* Make child's mount namespace MS_SLAVE */ - FORK_RLIMIT_NOFILE_SAFE = 1 << 9, /* Set RLIMIT_NOFILE soft limit to 1K for select() compat */ + FORK_RESET_SIGNALS = 1 << 0, /* Reset all signal handlers and signal mask */ + FORK_CLOSE_ALL_FDS = 1 << 1, /* Close all open file descriptors in the child, except for 0,1,2 */ + FORK_DEATHSIG = 1 << 2, /* Set PR_DEATHSIG in the child */ + FORK_NULL_STDIO = 1 << 3, /* Connect 0,1,2 to /dev/null */ + FORK_REOPEN_LOG = 1 << 4, /* Reopen log connection */ + FORK_LOG = 1 << 5, /* Log above LOG_DEBUG log level about failures */ + FORK_WAIT = 1 << 6, /* Wait until child exited */ + FORK_NEW_MOUNTNS = 1 << 7, /* Run child in its own mount namespace */ + FORK_MOUNTNS_SLAVE = 1 << 8, /* Make child's mount namespace MS_SLAVE */ + FORK_RLIMIT_NOFILE_SAFE = 1 << 9, /* Set RLIMIT_NOFILE soft limit to 1K for select() compat */ + FORK_STDOUT_TO_STDERR = 1 << 10, /* Make stdout a copy of stderr */ } ForkFlags; int safe_fork_full(const char *name, const int except_fds[], size_t n_except_fds, ForkFlags flags, pid_t *ret_pid); From 375068e35a080da8b229f489ac6038c4576a5a8a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 17 May 2019 10:17:06 +0200 Subject: [PATCH 056/109] main-func: send main exit code to parent via sd_notify() on exit So far we silently convert negative return values from run() as EXIT_FAILURE, which is how UNIX expects it. In many cases it would be very useful for the caller to retrieve the actual error number we exit with. Let's generically return that via sd_notify()'s ERRNO= attribute. This means callers can set $NOTIFY_SOCKET and get the actual error number delivered at their doorstep just like that. --- src/shared/main-func.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/main-func.h b/src/shared/main-func.h index 6c26cb9fb5673..cf23ad450c5ce 100644 --- a/src/shared/main-func.h +++ b/src/shared/main-func.h @@ -3,6 +3,8 @@ #include +#include "sd-daemon.h" + #include "pager.h" #include "selinux-util.h" #include "spawn-ask-password-agent.h" @@ -16,6 +18,8 @@ save_argc_argv(argc, argv); \ intro; \ r = impl; \ + if (r < 0) \ + (void) sd_notifyf(0, "ERRNO=%i", -r); \ ask_password_agent_close(); \ polkit_agent_close(); \ pager_close(); \ From a4ad8ef9eaf49d7401e79e332de0545275100c8a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:44:13 +0200 Subject: [PATCH 057/109] varlink: add correct support for more/continues method calls --- src/shared/varlink.c | 97 ++++++++++++++++++++++++++++++++++++-------- src/shared/varlink.h | 4 ++ 2 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index e987911d45f6f..ae68e1b3ffa7f 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -29,6 +29,7 @@ typedef enum VarlinkState { /* Client side states */ VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY, + VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_CALLED, VARLINK_PROCESSING_REPLY, @@ -39,7 +40,6 @@ typedef enum VarlinkState { VARLINK_PROCESSING_METHOD_MORE, VARLINK_PROCESSING_METHOD_ONEWAY, VARLINK_PROCESSED_METHOD, - VARLINK_PROCESSED_METHOD_MORE, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE, @@ -63,6 +63,7 @@ typedef enum VarlinkState { IN_SET(state, \ VARLINK_IDLE_CLIENT, \ VARLINK_AWAITING_REPLY, \ + VARLINK_AWAITING_REPLY_MORE, \ VARLINK_CALLING, \ VARLINK_CALLED, \ VARLINK_PROCESSING_REPLY, \ @@ -71,7 +72,6 @@ typedef enum VarlinkState { VARLINK_PROCESSING_METHOD_MORE, \ VARLINK_PROCESSING_METHOD_ONEWAY, \ VARLINK_PROCESSED_METHOD, \ - VARLINK_PROCESSED_METHOD_MORE, \ VARLINK_PENDING_METHOD, \ VARLINK_PENDING_METHOD_MORE) @@ -185,6 +185,7 @@ struct VarlinkServer { static const char* const varlink_state_table[_VARLINK_STATE_MAX] = { [VARLINK_IDLE_CLIENT] = "idle-client", [VARLINK_AWAITING_REPLY] = "awaiting-reply", + [VARLINK_AWAITING_REPLY_MORE] = "awaiting-reply-more", [VARLINK_CALLING] = "calling", [VARLINK_CALLED] = "called", [VARLINK_PROCESSING_REPLY] = "processing-reply", @@ -193,7 +194,6 @@ static const char* const varlink_state_table[_VARLINK_STATE_MAX] = { [VARLINK_PROCESSING_METHOD_MORE] = "processing-method-more", [VARLINK_PROCESSING_METHOD_ONEWAY] = "processing-method-oneway", [VARLINK_PROCESSED_METHOD] = "processed-method", - [VARLINK_PROCESSED_METHOD_MORE] = "processed-method-more", [VARLINK_PENDING_METHOD] = "pending-method", [VARLINK_PENDING_METHOD_MORE] = "pending-method-more", [VARLINK_PENDING_DISCONNECT] = "pending-disconnect", @@ -405,7 +405,7 @@ static int varlink_test_disconnect(Varlink *v) { goto disconnect; /* If we are waiting for incoming data but the read side is shut down, disconnect. */ - if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER) && v->read_disconnected) + if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER) && v->read_disconnected) goto disconnect; /* Similar, if are a client that hasn't written anything yet but the write side is dead, also @@ -478,7 +478,7 @@ static int varlink_read(Varlink *v) { assert(v); - if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER)) + if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER)) return 0; if (v->connecting) /* read() on a socket while we are in connect() will fail with EINVAL, hence exit early here */ return 0; @@ -596,7 +596,7 @@ static int varlink_parse_message(Varlink *v) { static int varlink_test_timeout(Varlink *v) { assert(v); - if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING)) + if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING)) return 0; if (v->timeout == USEC_INFINITY) return 0; @@ -673,7 +673,7 @@ static int varlink_dispatch_reply(Varlink *v) { assert(v); - if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING)) + if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING)) return 0; if (!v->current) return 0; @@ -715,6 +715,11 @@ static int varlink_dispatch_reply(Varlink *v) { goto invalid; } + /* Replies with 'continue' set are only OK we set 'more' when we initiated the method */ + if (v->state != VARLINK_AWAITING_REPLY_MORE && FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + goto invalid; + + /* An error is final */ if (error && FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) goto invalid; @@ -722,7 +727,7 @@ static int varlink_dispatch_reply(Varlink *v) { if (r < 0) goto invalid; - if (v->state == VARLINK_AWAITING_REPLY) { + if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE)) { varlink_set_state(v, VARLINK_PROCESSING_REPLY); if (v->reply_callback) { @@ -734,17 +739,18 @@ static int varlink_dispatch_reply(Varlink *v) { v->current = json_variant_unref(v->current); if (v->state == VARLINK_PROCESSING_REPLY) { + assert(v->n_pending > 0); - v->n_pending--; - varlink_set_state(v, v->n_pending == 0 ? VARLINK_IDLE_CLIENT : VARLINK_AWAITING_REPLY); + if (!FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + v->n_pending--; + + varlink_set_state(v, + FLAGS_SET(flags, VARLINK_REPLY_CONTINUES) ? VARLINK_AWAITING_REPLY_MORE : + v->n_pending == 0 ? VARLINK_IDLE_CLIENT : VARLINK_AWAITING_REPLY); } } else { assert(v->state == VARLINK_CALLING); - - if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) - goto invalid; - varlink_set_state(v, VARLINK_CALLED); } @@ -878,7 +884,6 @@ static int varlink_dispatch_method(Varlink *v) { varlink_set_state(v, VARLINK_PENDING_METHOD); break; - case VARLINK_PROCESSED_METHOD_MORE: /* One reply for a "more" message was sent, more to come */ case VARLINK_PROCESSING_METHOD_MORE: /* No reply for a "more" message was sent, more to come */ varlink_set_state(v, VARLINK_PENDING_METHOD_MORE); break; @@ -1073,7 +1078,7 @@ int varlink_get_events(Varlink *v) { return EPOLLOUT; if (!v->read_disconnected && - IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER) && + IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER) && !v->current && v->input_buffer_unscanned <= 0) ret |= EPOLLIN; @@ -1091,7 +1096,7 @@ int varlink_get_timeout(Varlink *v, usec_t *ret) { if (v->state == VARLINK_DISCONNECTED) return -ENOTCONN; - if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING) && + if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING) && v->timeout != USEC_INFINITY) { if (ret) *ret = usec_add(v->timestamp, v->timeout); @@ -1259,6 +1264,8 @@ int varlink_send(Varlink *v, const char *method, JsonVariant *parameters) { if (v->state == VARLINK_DISCONNECTED) return -ENOTCONN; + + /* We allow enqueuing multiple method calls at once! */ if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY)) return -EBUSY; @@ -1308,6 +1315,8 @@ int varlink_invoke(Varlink *v, const char *method, JsonVariant *parameters) { if (v->state == VARLINK_DISCONNECTED) return -ENOTCONN; + + /* We allow enqueing multiple method calls at once! */ if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY)) return -EBUSY; @@ -1349,6 +1358,60 @@ int varlink_invokeb(Varlink *v, const char *method, ...) { return varlink_invoke(v, method, parameters); } +int varlink_observe(Varlink *v, const char *method, JsonVariant *parameters) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + assert_return(v, -EINVAL); + assert_return(method, -EINVAL); + + if (v->state == VARLINK_DISCONNECTED) + return -ENOTCONN; + /* Note that we don't allow enqueuing multiple method calls when we are in more/continues mode! We + * thus insist on an idle client here. */ + if (v->state != VARLINK_IDLE_CLIENT) + return -EBUSY; + + r = varlink_sanitize_parameters(¶meters); + if (r < 0) + return r; + + r = json_build(&m, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("method", JSON_BUILD_STRING(method)), + JSON_BUILD_PAIR("parameters", JSON_BUILD_VARIANT(parameters)), + JSON_BUILD_PAIR("more", JSON_BUILD_BOOLEAN(true)))); + if (r < 0) + return r; + + r = varlink_enqueue_json(v, m); + if (r < 0) + return r; + + + varlink_set_state(v, VARLINK_AWAITING_REPLY_MORE); + v->n_pending++; + v->timestamp = now(CLOCK_MONOTONIC); + + return 0; +} + +int varlink_observeb(Varlink *v, const char *method, ...) { + _cleanup_(json_variant_unrefp) JsonVariant *parameters = NULL; + va_list ap; + int r; + + assert_return(v, -EINVAL); + + va_start(ap, method); + r = json_buildv(¶meters, ap); + va_end(ap); + + if (r < 0) + return r; + + return varlink_observe(v, method, parameters); +} + int varlink_call( Varlink *v, const char *method, diff --git a/src/shared/varlink.h b/src/shared/varlink.h index d96fa93619a7e..0208802010b1c 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -86,6 +86,10 @@ int varlink_callb(Varlink *v, const char *method, JsonVariant **ret_parameters, int varlink_invoke(Varlink *v, const char *method, JsonVariant *parameters); int varlink_invokeb(Varlink *v, const char *method, ...); +/* Enqueue method call, expect a reply now, and possibly more later, which are all delivered to the reply callback */ +int varlink_observe(Varlink *v, const char *method, JsonVariant *parameters); +int varlink_observeb(Varlink *v, const char *method, ...); + /* Enqueue a final reply */ int varlink_reply(Varlink *v, JsonVariant *parameters); int varlink_replyb(Varlink *v, ...); From e34088c381347f2108e0887cac33828afb56d61e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 28 May 2019 14:18:49 +0200 Subject: [PATCH 058/109] varlink: add varlink_close_unref() helper --- src/shared/varlink.c | 10 +++++++++- src/shared/varlink.h | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index ae68e1b3ffa7f..c64af290259aa 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -1194,6 +1194,15 @@ int varlink_close(Varlink *v) { return 1; } +Varlink* varlink_close_unref(Varlink *v) { + + if (!v) + return NULL; + + (void) varlink_close(v); + return varlink_unref(v); +} + Varlink* varlink_flush_close_unref(Varlink *v) { if (!v) @@ -1201,7 +1210,6 @@ Varlink* varlink_flush_close_unref(Varlink *v) { (void) varlink_flush(v); (void) varlink_close(v); - return varlink_unref(v); } diff --git a/src/shared/varlink.h b/src/shared/varlink.h index 0208802010b1c..0d9617d40352f 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -73,6 +73,7 @@ int varlink_flush(Varlink *v); int varlink_close(Varlink *v); Varlink* varlink_flush_close_unref(Varlink *v); +Varlink* varlink_close_unref(Varlink *v); /* Enqueue method call, not expecting a reply */ int varlink_send(Varlink *v, const char *method, JsonVariant *parameters); @@ -152,6 +153,7 @@ int varlink_server_set_connections_max(VarlinkServer *s, unsigned m); int varlink_server_set_description(VarlinkServer *s, const char *description); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_unref); +DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_close_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_flush_close_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServer *, varlink_server_unref); From 4b6af2d4f0a3626e5fb3eea4eb8e559e9c2a9f08 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 15 Aug 2019 09:34:05 +0200 Subject: [PATCH 059/109] varlink: move connection fds > fd2 We want to use this code in NSS modules, and we never know the execution environment we are run in there, hence let's move our fds up to ensure we won't step into dangerous fd territory. This is similar to how we already do it in sd-bus for client connection fds. --- src/shared/varlink.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index c64af290259aa..f5b4ee7db2015 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -287,6 +287,8 @@ int varlink_connect_address(Varlink **ret, const char *address) { if (v->fd < 0) return -errno; + v->fd = fd_move_above_stdio(v->fd); + if (connect(v->fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) { if (!IN_SET(errno, EAGAIN, EINPROGRESS)) return -errno; @@ -2228,6 +2230,8 @@ int varlink_server_listen_address(VarlinkServer *s, const char *address, mode_t if (fd < 0) return -errno; + fd = fd_move_above_stdio(fd); + (void) sockaddr_un_unlink(&sockaddr.un); RUN_WITH_UMASK(~m & 0777) From 4090e1604770ed4caaa7688c565dd48f113d6034 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 20 Aug 2019 14:07:09 +0200 Subject: [PATCH 060/109] varlink: port varlink code over to use getdtablesize() for sizing number of concurrent connections Use the official glibc API for determining this parameter. In most other cases in our tree it's better to go directly for RLIMIT_NOFILE since it's semantically what we want, but for this case it appears more appropriate to use the friendlier, shorter, explicit API. --- src/shared/varlink.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/shared/varlink.c b/src/shared/varlink.c index f5b4ee7db2015..7f59fde4d8c2b 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -2414,17 +2414,18 @@ int varlink_server_bind_connect(VarlinkServer *s, VarlinkConnect callback) { } unsigned varlink_server_connections_max(VarlinkServer *s) { - struct rlimit rl; + int dts; /* If a server is specified, return the setting for that server, otherwise the default value */ if (s) return s->connections_max; - assert_se(getrlimit(RLIMIT_NOFILE, &rl) >= 0); + dts = getdtablesize(); + assert_se(dts > 0); /* Make sure we never use up more than ¾th of RLIMIT_NOFILE for IPC */ - if (VARLINK_DEFAULT_CONNECTIONS_MAX > rl.rlim_cur / 4 * 3) - return rl.rlim_cur / 4 * 3; + if (VARLINK_DEFAULT_CONNECTIONS_MAX > (unsigned) dts / 4 * 3) + return dts / 4 * 3; return VARLINK_DEFAULT_CONNECTIONS_MAX; } From 60a652ba38873a0d7cbfd983cb6dfec4a3323114 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:50:42 +0200 Subject: [PATCH 061/109] format-table: add UID/GID output support to format-table.h --- src/shared/format-table.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/shared/format-table.h b/src/shared/format-table.h index aacf978978847..80f319d054a30 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -53,6 +53,12 @@ typedef enum TableDataType { #define TABLE_PID TABLE_INT32 assert_cc(sizeof(pid_t) == sizeof(int32_t)); +/* UIDs/GIDs are just 32bit unsigned integers on Linux */ +#define TABLE_UID TABLE_UINT32 +#define TABLE_GID TABLE_UINT32 +assert_cc(sizeof(uid_t) == sizeof(uint32_t)); +assert_cc(sizeof(gid_t) == sizeof(uint32_t)); + typedef struct Table Table; typedef struct TableCell TableCell; From 167ef06f222333b9f505786c1a15111ae7a77e9d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 14:50:01 +0200 Subject: [PATCH 062/109] login: port tables over to use TABLE_UID/TABLE_GID --- src/login/inhibit.c | 4 ++-- src/login/loginctl.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/login/inhibit.c b/src/login/inhibit.c index 25ff4d0be6428..b3b37fb595906 100644 --- a/src/login/inhibit.c +++ b/src/login/inhibit.c @@ -113,9 +113,9 @@ static int print_inhibitors(sd_bus *bus) { r = table_add_many(table, TABLE_STRING, who, - TABLE_UINT32, uid, + TABLE_UID, (uid_t) uid, TABLE_STRING, strna(u), - TABLE_UINT32, pid, + TABLE_PID, (pid_t) pid, TABLE_STRING, strna(comm), TABLE_STRING, what, TABLE_STRING, why, diff --git a/src/login/loginctl.c b/src/login/loginctl.c index 2ad9887066f6c..6e7d6600132f4 100644 --- a/src/login/loginctl.c +++ b/src/login/loginctl.c @@ -185,7 +185,7 @@ static int list_sessions(int argc, char *argv[], void *userdata) { r = table_add_many(table, TABLE_STRING, id, - TABLE_UINT32, uid, + TABLE_UID, (uid_t) uid, TABLE_STRING, user, TABLE_STRING, seat, TABLE_STRING, strna(tty)); @@ -244,7 +244,7 @@ static int list_users(int argc, char *argv[], void *userdata) { break; r = table_add_many(table, - TABLE_UINT32, uid, + TABLE_UID, (uid_t) uid, TABLE_STRING, user); if (r < 0) return log_error_errno(r, "Failed to add row to table: %m"); From 663c065356fd663322421b047bbe5ff99db9ad4d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 3 Jul 2019 14:46:42 +0200 Subject: [PATCH 063/109] mkosi: add fdisk-devel, openssl-devel, libpwquality-devel and efsck to build This is preparation for subsequent additions which link against these libraries. --- .mkosi/mkosi.fedora | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.mkosi/mkosi.fedora b/.mkosi/mkosi.fedora index b0673f707a399..2de0976bc38de 100644 --- a/.mkosi/mkosi.fedora +++ b/.mkosi/mkosi.fedora @@ -38,10 +38,12 @@ BuildPackages= libblkid-devel libcap-devel libcurl-devel + libfdisk-devel libgcrypt-devel libidn2-devel libmicrohttpd-devel libmount-devel + libpwquality-devel libseccomp-devel libselinux-devel libtool @@ -51,6 +53,7 @@ BuildPackages= lz4-devel m4 meson + openssl-devel pam-devel pcre2-devel pkgconfig @@ -61,6 +64,7 @@ BuildPackages= xz-devel Packages= + e2fsprogs libidn2 BuildDirectory=mkosi.builddir From fd97a2cfc9597d2fd089676b8e582346f4654702 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 15:25:36 +0200 Subject: [PATCH 064/109] shared: add generic user/group record structures and JSON parsers --- src/shared/group-record.c | 347 ++++++++ src/shared/group-record.h | 44 + src/shared/meson.build | 7 +- src/shared/user-record.c | 1680 +++++++++++++++++++++++++++++++++++++ src/shared/user-record.h | 356 ++++++++ 5 files changed, 2433 insertions(+), 1 deletion(-) create mode 100644 src/shared/group-record.c create mode 100644 src/shared/group-record.h create mode 100644 src/shared/user-record.c create mode 100644 src/shared/user-record.h diff --git a/src/shared/group-record.c b/src/shared/group-record.c new file mode 100644 index 0000000000000..5c4534242f5c7 --- /dev/null +++ b/src/shared/group-record.c @@ -0,0 +1,347 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "group-record.h" +#include "strv.h" +#include "user-util.h" + +GroupRecord* group_record_new(void) { + GroupRecord *h; + + h = new(GroupRecord, 1); + if (!h) + return NULL; + + *h = (GroupRecord) { + .n_ref = 1, + .disposition = _USER_DISPOSITION_INVALID, + .last_change_usec = UINT64_MAX, + .gid = GID_INVALID, + }; + + return h; +} + +static GroupRecord *group_record_free(GroupRecord *g) { + if (!g) + return NULL; + + free(g->group_name); + free(g->realm); + free(g->group_name_and_realm_auto); + + strv_free(g->members); + free(g->service); + strv_free(g->administrators); + strv_free_erase(g->hashed_password); + + json_variant_unref(g->json); + + return mfree(g); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(GroupRecord, group_record, group_record_free); + +static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch privileged_dispatch_table[] = { + { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(GroupRecord, hashed_password), JSON_SAFE }, + {}, + }; + + return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch binding_dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, binding_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch per_machine_dispatch_table[] = { + { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 }, + { "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 }, + { "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 }, + {}, + }; + + JsonVariant *e; + int r; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + bool matching = false; + JsonVariant *m; + + if (!json_variant_is_object(e)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name)); + + m = json_variant_by_key(e, "matchMachineId"); + if (m) { + r = per_machine_id_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + + if (!matching) { + m = json_variant_by_key(e, "matchHostname"); + if (m) { + r = per_machine_hostname_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + } + + if (!matching) + continue; + + r = json_dispatch(e, per_machine_dispatch_table, NULL, flags, userdata); + if (r < 0) + return r; + } + + return 0; +} + +static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch status_dispatch_table[] = { + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, status_dispatch_table, NULL, flags, userdata); +} + +static int group_record_augment(GroupRecord *h, JsonDispatchFlags json_flags) { + assert(h); + + if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR)) + return 0; + + assert(h->group_name); + + if (!h->group_name_and_realm_auto && h->realm) { + h->group_name_and_realm_auto = strjoin(h->group_name, "@", h->realm); + if (!h->group_name_and_realm_auto) + return json_log_oom(h->json, json_flags); + } + + return 0; +} + +int group_record_load( + GroupRecord *h, + JsonVariant *v, + UserRecordLoadFlags load_flags) { + + static const JsonDispatch group_dispatch_table[] = { + { "groupName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(GroupRecord, group_name), 0 }, + { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(GroupRecord, realm), 0 }, + { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(GroupRecord, disposition), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE }, + { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(GroupRecord, last_change_usec), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 }, + { "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 }, + { "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 }, + + { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 }, + + /* Not defined for now, for groups, but let's at least generate sensible errors about it */ + { "secret", JSON_VARIANT_OBJECT, json_dispatch_unsupported, 0, 0 }, + + /* Ignore the perMachine, binding and status stuff here, and process it later, so that it overrides whatever is set above */ + { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + + /* Ignore 'signature', we check it with explicit accessors instead */ + { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + {}, + }; + + JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags); + JsonVariant *per_machine, *binding, *status; + int r; + + assert(h); + assert(!h->json); + + /* Note that this call will leave a half-initialized record around on failure! */ + + if ((USER_RECORD_REQUIRE_MASK(load_flags) & (USER_RECORD_SECRET|USER_RECORD_PRIVILEGED)) != 0) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Secret and privileged section currently not available for groups, refusing."); + + r = user_group_record_mangle(v, load_flags, &h->json, &h->mask); + if (r < 0) + return r; + + r = json_dispatch(h->json, group_dispatch_table, NULL, json_flags, h); + if (r < 0) + return r; + + /* During the parsing operation above we ignored the 'perMachine' and 'binding' fields, since we want + * them to override the global options. Let's process them now. */ + + per_machine = json_variant_by_key(h->json, "perMachine"); + if (per_machine) { + r = dispatch_per_machine("perMachine", per_machine, json_flags, h); + if (r < 0) + return r; + } + + binding = json_variant_by_key(h->json, "binding"); + if (binding) { + r = dispatch_binding("binding", binding, json_flags, h); + if (r < 0) + return r; + } + + status = json_variant_by_key(h->json, "status"); + if (status) { + r = dispatch_status("binding", status, json_flags, h); + if (r < 0) + return r; + } + + if (FLAGS_SET(h->mask, USER_RECORD_REGULAR) && !h->group_name) + return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "Group name field missing, refusing."); + + r = group_record_augment(h, json_flags); + if (r < 0) + return r; + + return 0; +} + +int group_record_build(GroupRecord **ret, ...) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + va_list ap; + int r; + + assert(ret); + + va_start(ap, ret); + r = json_buildv(&v, ap); + va_end(ap); + + if (r < 0) + return r; + + g = group_record_new(); + if (!g) + return -ENOMEM; + + r = group_record_load(g, v, USER_RECORD_LOAD_FULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(g); + return 0; +} + +const char *group_record_group_name_and_realm(GroupRecord *h) { + assert(h); + + /* Return the pre-initialized joined string if it is defined */ + if (h->group_name_and_realm_auto) + return h->group_name_and_realm_auto; + + /* If it's not defined then we cannot have a realm */ + assert(!h->realm); + return h->group_name; +} + +UserDisposition group_record_disposition(GroupRecord *h) { + assert(h); + + if (h->disposition >= 0) + return h->disposition; + + /* If not declared, derive from GID */ + + if (!gid_is_valid(h->gid)) + return _USER_DISPOSITION_INVALID; + + if (h->gid == 0 || h->gid == GID_NOBODY) + return USER_INTRINSIC; + + if (gid_is_system(h->gid)) + return USER_SYSTEM; + + if (gid_is_dynamic(h->gid)) + return USER_DYNAMIC; + + if (gid_is_container(h->gid)) + return USER_CONTAINER; + + if (h->gid > INT32_MAX) + return USER_RESERVED; + + return USER_REGULAR; +} + +int group_record_clone(GroupRecord *h, UserRecordLoadFlags flags, GroupRecord **ret) { + _cleanup_(group_record_unrefp) GroupRecord *c = NULL; + int r; + + assert(h); + assert(ret); + + c = group_record_new(); + if (!c) + return -ENOMEM; + + r = group_record_load(c, h->json, flags); + if (r < 0) + return r; + + *ret = TAKE_PTR(c); + return 0; +} diff --git a/src/shared/group-record.h b/src/shared/group-record.h new file mode 100644 index 0000000000000..b72a43e50d8d6 --- /dev/null +++ b/src/shared/group-record.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "json.h" +#include "user-record.h" + +typedef struct GroupRecord { + unsigned n_ref; + UserRecordMask mask; + bool incomplete; + + char *group_name; + char *realm; + char *group_name_and_realm_auto; + + UserDisposition disposition; + uint64_t last_change_usec; + + gid_t gid; + + char **members; + + char *service; + + /* The following exist mostly so that we can cover the full /etc/gshadow set of fields, we currently + * do not actually make use of these */ + char **administrators; /* maps to 'struct sgrp' .sg_adm field */ + char **hashed_password; /* maps to 'struct sgrp' .sg_passwd field */ + + JsonVariant *json; +} GroupRecord; + +GroupRecord* group_record_new(void); +GroupRecord* group_record_ref(GroupRecord *g); +GroupRecord* group_record_unref(GroupRecord *g); + +DEFINE_TRIVIAL_CLEANUP_FUNC(GroupRecord*, group_record_unref); + +int group_record_load(GroupRecord *h, JsonVariant *v, UserRecordLoadFlags flags); +int group_record_build(GroupRecord **ret, ...); +int group_record_clone(GroupRecord *g, UserRecordLoadFlags flags, GroupRecord **ret); + +const char *group_record_group_name_and_realm(GroupRecord *h); +UserDisposition group_record_disposition(GroupRecord *h); diff --git a/src/shared/meson.build b/src/shared/meson.build index 031f018663aa3..81990e848956f 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -84,6 +84,8 @@ shared_sources = files(''' generator.c generator.h gpt.h + group-record.c + group-record.h id128-print.c id128-print.h ima-util.c @@ -178,6 +180,8 @@ shared_sources = files(''' uid-range.h unit-file.c unit-file.h + user-record.c + user-record.h utmp-wtmp.h varlink.c varlink.h @@ -278,7 +282,8 @@ libshared_deps = [threads, libidn, libxz, liblz4, - libblkid] + libblkid, + libcrypt] libshared_sym_path = '@0@/libshared.sym'.format(meson.current_source_dir()) diff --git a/src/shared/user-record.c b/src/shared/user-record.c new file mode 100644 index 0000000000000..f5f9a9375409d --- /dev/null +++ b/src/shared/user-record.c @@ -0,0 +1,1680 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "cgroup-util.h" +#include "dns-domain.h" +#include "env-util.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "hostname-util.h" +#include "memory-util.h" +#include "path-util.h" +#include "rlimit-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "strv.h" +#include "user-record.h" +#include "user-util.h" + +#define DEFAULT_RATELIMIT_BURST 10 +#define DEFAULT_RATELIMIT_INTERVAL_USEC (1*USEC_PER_MINUTE) + +UserRecord* user_record_new(void) { + UserRecord *h; + + h = new(UserRecord, 1); + if (!h) + return NULL; + + *h = (UserRecord) { + .n_ref = 1, + .disposition = _USER_DISPOSITION_INVALID, + .last_change_usec = UINT64_MAX, + .last_password_change_usec = UINT64_MAX, + .umask = MODE_INVALID, + .nice_level = INT_MAX, + .not_before_usec = UINT64_MAX, + .not_after_usec = UINT64_MAX, + .locked = -1, + .storage = _USER_STORAGE_INVALID, + .access_mode = MODE_INVALID, + .disk_size = UINT64_MAX, + .disk_size_relative = UINT64_MAX, + .tasks_max = UINT64_MAX, + .memory_high = UINT64_MAX, + .memory_max = UINT64_MAX, + .cpu_weight = UINT64_MAX, + .io_weight = UINT64_MAX, + .uid = UID_INVALID, + .gid = GID_INVALID, + .nodev = true, + .nosuid = true, + .luks_discard = -1, + .luks_volume_key_size = UINT64_MAX, + .luks_pbkdf_time_cost_usec = UINT64_MAX, + .luks_pbkdf_memory_cost = UINT64_MAX, + .luks_pbkdf_parallel_threads = UINT64_MAX, + .disk_usage = UINT64_MAX, + .disk_free = UINT64_MAX, + .disk_ceiling = UINT64_MAX, + .disk_floor = UINT64_MAX, + .signed_locally = -1, + .good_authentication_counter = UINT64_MAX, + .bad_authentication_counter = UINT64_MAX, + .last_good_authentication_usec = UINT64_MAX, + .last_bad_authentication_usec = UINT64_MAX, + .ratelimit_begin_usec = UINT64_MAX, + .ratelimit_count = UINT64_MAX, + .ratelimit_interval_usec = UINT64_MAX, + .ratelimit_burst = UINT64_MAX, + .removable = -1, + .enforce_password_policy = -1, + .auto_login = -1, + .stop_delay_usec = UINT64_MAX, + .kill_processes = -1, + .password_change_min_usec = UINT64_MAX, + .password_change_max_usec = UINT64_MAX, + .password_change_warn_usec = UINT64_MAX, + .password_change_inactive_usec = UINT64_MAX, + .password_change_now = -1, + }; + + return h; +} + +static UserRecord* user_record_free(UserRecord *h) { + if (!h) + return NULL; + + free(h->user_name); + free(h->realm); + free(h->user_name_and_realm_auto); + free(h->real_name); + free(h->email_address); + erase_and_free(h->password_hint); + free(h->location); + free(h->icon_name); + + free(h->shell); + + strv_free(h->environment); + free(h->time_zone); + free(h->preferred_language); + rlimit_free_all(h->rlimits); + + free(h->skeleton_directory); + + strv_free_erase(h->hashed_password); + strv_free_erase(h->ssh_authorized_keys); + strv_free_erase(h->password); + + free(h->fscrypt_salt); + + free(h->cifs_service); + free(h->cifs_user_name); + free(h->cifs_domain); + + free(h->image_path); + free(h->image_path_auto); + free(h->home_directory); + free(h->home_directory_auto); + + strv_free(h->member_of); + + free(h->file_system_type); + free(h->luks_cipher); + free(h->luks_cipher_mode); + free(h->luks_pbkdf_hash_algorithm); + free(h->luks_pbkdf_type); + + free(h->state); + free(h->service); + + json_variant_unref(h->json); + + return mfree(h); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(UserRecord, user_record, user_record_free); + +int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + r = dns_name_is_valid(n); + if (r < 0) + return json_log(variant, flags, r, "Failed to check if JSON field '%s' is a valid DNS domain.", strna(name)); + if (r == 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid DNS domain.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_gecos(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + _cleanup_free_ char *k = NULL; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!valid_gecos(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid GECOS compatible real name.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_nice(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + int *nl = userdata; + intmax_t m; + + if (json_variant_is_null(variant)) { + *nl = INT_MAX; + return 0; + } + + if (!json_variant_is_integer(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + m = json_variant_integer(variant); + if (m < PRIO_MIN || m >= PRIO_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not a valid nice level.", strna(name)); + + *nl = m; + return 0; +} + +static int json_dispatch_rlimit_value(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + rlim_t *ret = userdata; + + if (json_variant_is_null(variant)) + *ret = RLIM_INFINITY; + else if (json_variant_is_unsigned(variant)) { + uintmax_t w; + + w = json_variant_unsigned(variant); + if (w == RLIM_INFINITY || (uintmax_t) w != json_variant_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "Resource limit value '%s' is out of range.", name); + + *ret = (rlim_t) w; + } else + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit value '%s' is not an unsigned integer.", name); + + return 0; +} + +static int json_dispatch_rlimits(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + struct rlimit** limits = userdata; + JsonVariant *value; + const char *key; + int r; + + assert_se(limits); + + if (json_variant_is_null(variant)) { + rlimit_free_all(limits); + return 0; + } + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + JSON_VARIANT_OBJECT_FOREACH(key, value, variant) { + JsonVariant *jcur, *jmax; + struct rlimit rl; + const char *p; + int l; + + p = startswith(key, "RLIMIT_"); + if (!p) + l = -1; + else + l = rlimit_from_string(p); + if (l < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' not known.", key); + + if (!json_variant_is_object(value)) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' has invalid value.", key); + + if (json_variant_elements(value) != 4) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' value is does not have two fields as expected.", key); + + jcur = json_variant_by_key(value, "cur"); + if (!jcur) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'cur' field.", key); + r = json_dispatch_rlimit_value("cur", jcur, flags, &rl.rlim_cur); + if (r < 0) + return r; + + jmax = json_variant_by_key(value, "max"); + if (!jmax) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'max' field.", key); + r = json_dispatch_rlimit_value("max", jmax, flags, &rl.rlim_max); + if (r < 0) + return r; + + if (limits[l]) + *(limits[l]) = rl; + else { + limits[l] = newdup(struct rlimit, &rl, 1); + if (!limits[l]) + return log_oom(); + } + } + + return 0; +} + +static int json_dispatch_filename_or_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + _cleanup_free_ char *k = NULL; + const char *n; + int r; + + assert(s); + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!filename_is_valid(n) && !path_is_normalized(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid file name or normalized path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + _cleanup_free_ char *k = NULL; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!path_is_normalized(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a normalized file system path.", strna(name)); + if (!path_is_absolute(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an absolute file system path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_home_directory(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + _cleanup_free_ char *k = NULL; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!valid_home(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid home directory path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_image_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + _cleanup_free_ char *k = NULL; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (empty_or_root(n) || !path_is_valid(n) || !path_is_absolute(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid image path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_umask(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + mode_t *m = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *m = (mode_t) -1; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name)); + + k = json_variant_unsigned(variant); + if (k > 0777) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…0777.", strna(name)); + + *m = (mode_t) k; + return 0; +} + +static int json_dispatch_access_mode(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + mode_t *m = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *m = (mode_t) -1; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name)); + + k = json_variant_unsigned(variant); + if (k > 07777) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…07777.", strna(name)); + + *m = (mode_t) k; + return 0; +} + +static int json_dispatch_environment(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_strv_free_ char **n = NULL; + char ***l = userdata; + size_t i; + int r; + + if (json_variant_is_null(variant)) { + *l = strv_free(*l); + return 0; + } + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + for (i = 0; i < json_variant_elements(variant); i++) { + _cleanup_free_ char *c = NULL; + JsonVariant *e; + const char *a; + + e = json_variant_by_index(variant, i); + if (!json_variant_is_string(e)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name)); + + assert_se(a = json_variant_string(e)); + + if (!env_assignment_is_valid(a)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of environment variables.", strna(name)); + + c = strdup(a); + if (!c) + return json_log_oom(variant, flags); + + r = strv_env_replace(&n, c); + if (r < 0) + return json_log_oom(variant, flags); + + c = NULL; + } + + strv_free_and_replace(*l, n); + return 0; +} + +int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserDisposition *disposition = userdata, k; + + if (json_variant_is_null(variant)) { + *disposition = _USER_DISPOSITION_INVALID; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + k = user_disposition_from_string(json_variant_string(variant)); + if (k < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Disposition type '%s' not known.", json_variant_string(variant)); + + *disposition = k; + return 0; +} + +static int json_dispatch_storage(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserStorage *storage = userdata, k; + + if (json_variant_is_null(variant)) { + *storage = _USER_STORAGE_INVALID; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + k = user_storage_from_string(json_variant_string(variant)); + if (k < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Storage type '%s' not known.", json_variant_string(variant)); + + *storage = k; + return 0; +} + +static int json_dispatch_disk_size(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uint64_t *size = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *size = UINT64_MAX; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an integer.", strna(name)); + + k = json_variant_unsigned(variant); + if (k < USER_DISK_SIZE_MIN || k > USER_DISK_SIZE_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX); + + *size = k; + return 0; +} + +static int json_dispatch_tasks_or_memory_max(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uint64_t *limit = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *limit = UINT64_MAX; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name)); + + k = json_variant_unsigned(variant); + if (k <= 0 || k >= UINT64_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) 1, UINT64_MAX-1); + + *limit = k; + return 0; +} + +static int json_dispatch_weight(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uint64_t *weight = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *weight = UINT64_MAX; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name)); + + k = json_variant_unsigned(variant); + if (k <= CGROUP_WEIGHT_MIN || k >= CGROUP_WEIGHT_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) CGROUP_WEIGHT_MIN, (uint64_t) CGROUP_WEIGHT_MAX); + + *weight = k; + return 0; +} + +int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_strv_free_ char **l = NULL; + char ***list = userdata; + JsonVariant *e; + int r; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + + if (!json_variant_is_string(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string."); + + if (!valid_user_group_name(json_variant_string(e))) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a valid user/group name: %s", json_variant_string(e)); + + r = strv_extend(&l, json_variant_string(e)); + if (r < 0) + return json_log(e, flags, r, "Failed to append array element: %m"); + } + + r = strv_extend_strv(list, l, true); + if (r < 0) + return json_log(variant, flags, r, "Failed to merge user/group arrays: %m"); + + return 0; +} + +static int dispatch_secret(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch secret_dispatch_table[] = { + { "password", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, password), 0 }, + {}, + }; + + return json_dispatch(variant, secret_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch privileged_dispatch_table[] = { + { "passwordHint", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, password_hint), 0 }, + { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, hashed_password), JSON_SAFE }, + { "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 }, + {}, + }; + + return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_fscrypt_salt(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserRecord *h = userdata; + size_t l; + void *b; + int r; + + assert_se(h); + + if (json_variant_is_null(variant)) { + h->fscrypt_salt = mfree(h->fscrypt_salt); + h->fscrypt_salt_size = 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = unbase64mem(json_variant_string(variant), (size_t) -1, &b, &l); + if (r < 0) + return json_log(variant, flags, r, "Failed to decode fscrypt salt: %m"); + + free(h->fscrypt_salt); + h->fscrypt_salt = b; + h->fscrypt_salt_size = l; + + return 0; +} + +static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch binding_dispatch_table[] = { + { "imagePath", JSON_VARIANT_STRING, json_dispatch_image_path, offsetof(UserRecord, image_path), 0 }, + { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 }, + { "partitionUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 0 }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, binding_dispatch_table, NULL, flags, userdata); +} + +int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags) { + sd_id128_t mid; + int r; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(ids, flags, r, "Failed to acquire machine ID: %m"); + + if (json_variant_is_string(ids)) { + sd_id128_t k; + + r = sd_id128_from_string(json_variant_string(ids), &k); + if (r < 0) { + json_log(ids, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(ids)); + return 0; + } + + return sd_id128_equal(mid, k); + } + + if (json_variant_is_array(ids)) { + JsonVariant *e; + + JSON_VARIANT_ARRAY_FOREACH(e, ids) { + sd_id128_t k; + + if (!json_variant_is_string(e)) { + json_log(e, flags, 0, "Machine ID is not a string, ignoring: %m"); + continue; + } + + r = sd_id128_from_string(json_variant_string(e), &k); + if (r < 0) { + json_log(e, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(e)); + continue; + } + + if (sd_id128_equal(mid, k)) + return true; + } + + return false; + } + + json_log(ids, flags, 0, "Machine ID is not a string or array of strings, ignoring: %m"); + return false; +} + +int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags) { + _cleanup_free_ char *hn = NULL; + int r; + + r = gethostname_strict(&hn); + if (r == -ENXIO) { + json_log(hns, flags, r, "No hostname set, not matching perMachine hostname record: %m"); + return false; + } + if (r < 0) + return json_log(hns, flags, r, "Failed to acquire hostname: %m"); + + if (json_variant_is_string(hns)) + return streq(json_variant_string(hns), hn); + + if (json_variant_is_array(hns)) { + JsonVariant *e; + + JSON_VARIANT_ARRAY_FOREACH(e, hns) { + + if (!json_variant_is_string(e)) { + json_log(e, flags, 0, "Hostname is not a string, ignoring: %m"); + continue; + } + + if (streq(json_variant_string(hns), hn)) + return true; + } + + return false; + } + + json_log(hns, flags, 0, "Hostname is not a string or array of strings, ignoring: %m"); + return false; +} + +static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch per_machine_dispatch_table[] = { + { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, + { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, + { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, + { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, + { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, + { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, + { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, + { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, + { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, + { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, + { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, + { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, + { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, + { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, + { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, + { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, + { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, + { "mountNoSUID", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, + { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, + { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 0 }, + { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, + { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, + { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, + { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "partitionUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0, }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, + { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, + { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, + { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, + { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, + { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, + { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, + { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, + { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, + { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, + { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, + { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, + { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, + { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, + { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, + { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, + {}, + }; + + JsonVariant *e; + int r; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + bool matching = false; + JsonVariant *m; + + if (!json_variant_is_object(e)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name)); + + m = json_variant_by_key(e, "matchMachineId"); + if (m) { + r = per_machine_id_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + + if (!matching) { + m = json_variant_by_key(e, "matchHostname"); + if (m) { + r = per_machine_hostname_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + } + + if (!matching) + continue; + + r = json_dispatch(e, per_machine_dispatch_table, NULL, flags, userdata); + if (r < 0) + return r; + } + + return 0; +} + +static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch status_dispatch_table[] = { + { "diskUsage", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 }, + { "diskFree", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 }, + { "diskCeiling", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 }, + { "diskFloor", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 }, + { "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE }, + { "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 }, + { "goodAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 }, + { "badAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 }, + { "lastGoodAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 }, + { "lastBadAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 }, + { "rateLimitBeginUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 }, + { "rateLimitCount", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 }, + { "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 0 }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, status_dispatch_table, NULL, flags, userdata); +} + +static int user_record_augment(UserRecord *h, JsonDispatchFlags json_flags) { + assert(h); + + if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR)) + return 0; + + assert(h->user_name); + + if (!h->user_name_and_realm_auto && h->realm) { + h->user_name_and_realm_auto = strjoin(h->user_name, "@", h->realm); + if (!h->user_name_and_realm_auto) + return json_log_oom(h->json, json_flags); + } + + /* Let's add in the following automatisms only for regular users, they dont make sense for any others */ + if (user_record_disposition(h) != USER_REGULAR) + return 0; + + if (!h->home_directory && !h->home_directory_auto) { + h->home_directory_auto = path_join("/home/", h->user_name); + if (!h->home_directory_auto) + return json_log_oom(h->json, json_flags); + } + + if (!h->image_path && !h->image_path_auto) { + const char *suffix; + UserStorage storage; + + storage = user_record_storage(h); + if (storage == USER_LUKS) + suffix = ".home"; + else if (IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) + suffix = ".homedir"; + else + suffix = NULL; + + if (suffix) { + h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), suffix); + if (!h->image_path_auto) + return json_log_oom(h->json, json_flags); + } + } + + return 0; +} + +int user_group_record_mangle( + JsonVariant *v, + UserRecordLoadFlags load_flags, + JsonVariant **ret_variant, + UserRecordMask *ret_mask) { + + static const struct { + UserRecordMask mask; + const char *name; + } mask_field[] = { + { USER_RECORD_PRIVILEGED, "privileged" }, + { USER_RECORD_SECRET, "secret" }, + { USER_RECORD_BINDING, "binding" }, + { USER_RECORD_PER_MACHINE, "perMachine" }, + { USER_RECORD_STATUS, "status" }, + { USER_RECORD_SIGNATURE, "signature" }, + }; + + JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags); + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + JsonVariant *array[ELEMENTSOF(mask_field) * 2]; + size_t n_retain = 0, i; + UserRecordMask m = 0; + int r; + + assert((load_flags & _USER_RECORD_MASK_MAX) == 0); /* detect mistakes when accidentally passing + * UserRecordMask bit masks as UserRecordLoadFlags + * value */ + + assert(v); + assert(ret_variant); + assert(ret_mask); + + /* Note that this function is shared with the group record parser, hence we try to be generic in our + * log message wording here, to cover both cases. */ + + if (!json_variant_is_object(v)) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is not a JSON object, refusing."); + + if (USER_RECORD_ALLOW_MASK(load_flags) == 0) /* allow nothing? */ + return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Nothing allowed in record, refusing."); + + if (USER_RECORD_STRIP_MASK(load_flags) == _USER_RECORD_MASK_MAX) /* strip everything? */ + return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Stripping everything from record, refusing."); + + /* Check if we have the special sections and if they match our flags set */ + for (i = 0; i < ELEMENTSOF(mask_field); i++) { + JsonVariant *e, *k; + + if (FLAGS_SET(USER_RECORD_STRIP_MASK(load_flags), mask_field[i].mask)) { + if (!w) + w = json_variant_ref(v); + + r = json_variant_filter(&w, STRV_MAKE(mask_field[i].name)); + if (r < 0) + return json_log(w, json_flags, r, "Failed to remove field from variant: %m"); + + v = w; + continue; + } + + e = json_variant_by_key_full(v, mask_field[i].name, &k); + if (e) { + if (!FLAGS_SET(USER_RECORD_ALLOW_MASK(load_flags), mask_field[i].mask)) + return json_log(e, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", mask_field[i].name); + + if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) { + array[n_retain++] = k; + array[n_retain++] = e; + } + + m |= mask_field[i].mask; + } else { + if (FLAGS_SET(USER_RECORD_REQUIRE_MASK(load_flags), mask_field[i].mask)) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks '%s' field, which is required.", mask_field[i].name); + } + } + + if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) { + _cleanup_(json_variant_unrefp) JsonVariant *k = NULL; + + /* If we are supposed to strip regular items, then let's instead just allocate a new object + * with just the stuff we need. */ + + r = json_variant_new_object(&k, array, n_retain); + if (r < 0) + return json_log(v, json_flags, r, "Failed to allocate new object: %m"); + + json_variant_unref(w); + w = TAKE_PTR(k); + + v = w; + } else { + /* And now check if there's anything else in the record */ + for (i = 0; i < json_variant_elements(v); i += 2) { + const char *f; + bool special = false; + size_t j; + + assert_se(f = json_variant_string(json_variant_by_index(v, i))); + + for (j = 0; j < ELEMENTSOF(mask_field); j++) + if (streq(f, mask_field[j].name)) { /* already covered in the loop above */ + special = true; + continue; + } + + if (!special) { + if ((load_flags & (USER_RECORD_ALLOW_REGULAR|USER_RECORD_REQUIRE_REGULAR)) == 0) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", f); + + m |= USER_RECORD_REGULAR; + break; + } + } + } + + if (FLAGS_SET(load_flags, USER_RECORD_REQUIRE_REGULAR) && !FLAGS_SET(m, USER_RECORD_REGULAR)) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks basic identity fields, which are required."); + + if (m == 0) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is empty."); + + if (w) + *ret_variant = TAKE_PTR(w); + else + *ret_variant = json_variant_ref(v); + + *ret_mask = m; + return 0; +} + +int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_flags) { + + static const JsonDispatch user_dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), 0 }, + { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 }, + { "realName", JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 }, + { "emailAddress", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, email_address), JSON_SAFE }, + { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, + { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, + { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(UserRecord, disposition), 0 }, + { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 }, + { "lastPasswordChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 }, + { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, + { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, + { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, + { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, + { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, + { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, + { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, + { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, + { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, + { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, + { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, + { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, + { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, + { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, + { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, + { "mountNoSUID", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, + { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, + { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 0 }, + { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, + { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, + { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, + { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, + { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "partitionUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUUID", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0 }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, + { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, + { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, + { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, + { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE }, + { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, + { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, + { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, + { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, + { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, + { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, + { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, + { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, + { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, + { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, + { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, + + { "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 }, + { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 }, + + /* Ignore the perMachine, binding, status stuff here, and process it later, so that it overrides whatever is set above */ + { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + + /* Ignore 'signature', we check it with explicit accessors instead */ + { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + {}, + }; + + JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags); + JsonVariant *per_machine, *binding, *status; + int r; + + assert(h); + assert(!h->json); + + /* Note that this call will leave a half-initialized record around on failure! */ + + r = user_group_record_mangle(v, load_flags, &h->json, &h->mask); + if (r < 0) + return r; + + r = json_dispatch(h->json, user_dispatch_table, NULL, json_flags, h); + if (r < 0) + return r; + + /* During the parsing operation above we ignored the 'perMachine', 'binding' and 'status' fields, + * since we want them to override the global options. Let's process them now. */ + + per_machine = json_variant_by_key(h->json, "perMachine"); + if (per_machine) { + r = dispatch_per_machine("perMachine", per_machine, json_flags, h); + if (r < 0) + return r; + } + + binding = json_variant_by_key(h->json, "binding"); + if (binding) { + r = dispatch_binding("binding", binding, json_flags, h); + if (r < 0) + return r; + } + + status = json_variant_by_key(h->json, "status"); + if (status) { + r = dispatch_status("binding", status, json_flags, h); + if (r < 0) + return r; + } + + if (FLAGS_SET(h->mask, USER_RECORD_REGULAR) && !h->user_name) + return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "User name field missing, refusing."); + + r = user_record_augment(h, json_flags); + if (r < 0) + return r; + + return 0; +} + +int user_record_build(UserRecord **ret, ...) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *u = NULL; + va_list ap; + int r; + + assert(ret); + + va_start(ap, ret); + r = json_buildv(&v, ap); + va_end(ap); + + if (r < 0) + return r; + + u = user_record_new(); + if (!u) + return -ENOMEM; + + r = user_record_load(u, v, USER_RECORD_LOAD_FULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(u); + return 0; +} + +const char *user_record_user_name_and_realm(UserRecord *h) { + assert(h); + + /* Return the pre-initialized joined string if it is defined */ + if (h->user_name_and_realm_auto) + return h->user_name_and_realm_auto; + + /* If it's not defined then we cannot have a realm */ + assert(!h->realm); + return h->user_name; +} + +UserStorage user_record_storage(UserRecord *h) { + assert(h); + + if (h->storage >= 0) + return h->storage; + + return USER_CLASSIC; +} + +const char *user_record_file_system_type(UserRecord *h) { + assert(h); + + return h->file_system_type ?: "ext4"; +} + +const char *user_record_skeleton_directory(UserRecord *h) { + assert(h); + + return h->skeleton_directory ?: "/etc/skel"; +} + +mode_t user_record_access_mode(UserRecord *h) { + assert(h); + + return h->access_mode != (mode_t) -1 ? h->access_mode : 0700; +} + +const char* user_record_home_directory(UserRecord *h) { + assert(h); + + if (h->home_directory) + return h->home_directory; + if (h->home_directory_auto) + return h->home_directory_auto; + + /* The root user is special, hence be special about it */ + if (streq_ptr(h->user_name, "root")) + return "/root"; + + return "/"; +} + +const char *user_record_image_path(UserRecord *h) { + assert(h); + + if (h->image_path) + return h->image_path; + if (h->image_path_auto) + return h->image_path_auto; + + return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ? user_record_home_directory(h) : NULL; +} + +const char *user_record_cifs_user_name(UserRecord *h) { + assert(h); + + return h->cifs_user_name ?: h->user_name; +} + +unsigned long user_record_mount_flags(UserRecord *h) { + assert(h); + + return (h->nosuid ? MS_NOSUID : 0) | + (h->noexec ? MS_NOEXEC : 0) | + (h->nodev ? MS_NODEV : 0); +} + +const char *user_record_shell(UserRecord *h) { + assert(h); + + if (h->shell) + return h->shell; + + if (streq_ptr(h->user_name, "root") || + user_record_disposition(h) == USER_REGULAR) + return "/bin/bash"; + + return NOLOGIN; +} + +const char *user_record_real_name(UserRecord *h) { + assert(h); + + return h->real_name ?: h->user_name; +} + +bool user_record_luks_discard(UserRecord *h) { + const char *ip; + + assert(h); + + if (h->luks_discard >= 0) + return h->luks_discard; + + ip = user_record_image_path(h); + if (!ip) + return false; + + /* Use discard by default if we are referring to a real block device, but not when operating on a + * loopback device. We want to optimize for SSD and flash storage after all, but we should be careful + * when storing stuff on top of regular file systems in loopback files as doing discard then would + * mean thin provisioning and we should not do that willy-nilly since it means we'll risk EIO later + * on should the disk space to back our file systems not be available. */ + + return path_startswith(ip, "/dev/"); +} + +const char *user_record_luks_cipher(UserRecord *h) { + assert(h); + + return h->luks_cipher ?: "aes"; +} + +const char *user_record_luks_cipher_mode(UserRecord *h) { + assert(h); + + return h->luks_cipher_mode ?: "xts-plain64"; +} + +uint64_t user_record_luks_volume_key_size(UserRecord *h) { + assert(h); + + /* We return a value here that can be cast without loss into size_t which is what libcrypsetup expects */ + + if (h->luks_volume_key_size == UINT64_MAX) + return 256 / 8; + + return MIN(h->luks_volume_key_size, SIZE_MAX); +} + +const char* user_record_luks_pbkdf_type(UserRecord *h) { + assert(h); + + return h->luks_pbkdf_type ?: "argon2i"; +} + +uint64_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h) { + assert(h); + + /* Returns a value with ms granularity, since that's what libcryptsetup expects */ + + if (h->luks_pbkdf_time_cost_usec == UINT64_MAX) + return 500 * USEC_PER_MSEC; /* We default to 500ms, in contrast to libcryptsetup's 2s, which is just awfully slow on every login */ + + return MIN(DIV_ROUND_UP(h->luks_pbkdf_time_cost_usec, USEC_PER_MSEC), UINT32_MAX) * USEC_PER_MSEC; +} + +uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h) { + assert(h); + + /* Returns a value with kb granularity, since that's what libcryptsetup expects */ + + if (h->luks_pbkdf_memory_cost == UINT64_MAX) + return 64*1024*1024; /* We default to 64M, since this should work on smaller systems too */ + + return MIN(DIV_ROUND_UP(h->luks_pbkdf_memory_cost, 1024), UINT32_MAX) * 1024; +} + +uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h) { + assert(h); + + if (h->luks_pbkdf_memory_cost == UINT64_MAX) + return 1; /* We default to 1, since this should work on smaller systems too */ + + return MIN(h->luks_pbkdf_parallel_threads, UINT32_MAX); +} + +const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h) { + assert(h); + + return h->luks_pbkdf_hash_algorithm ?: "sha512"; +} + +gid_t user_record_gid(UserRecord *h) { + assert(h); + + if (gid_is_valid(h->gid)) + return h->gid; + + return (gid_t) h->uid; +} + +UserDisposition user_record_disposition(UserRecord *h) { + assert(h); + + if (h->disposition >= 0) + return h->disposition; + + /* If not declared, derive from UID */ + + if (!uid_is_valid(h->uid)) + return _USER_DISPOSITION_INVALID; + + if (h->uid == 0 || h->uid == UID_NOBODY) + return USER_INTRINSIC; + + if (uid_is_system(h->uid)) + return USER_SYSTEM; + + if (uid_is_dynamic(h->uid)) + return USER_DYNAMIC; + + if (uid_is_container(h->uid)) + return USER_CONTAINER; + + if (h->uid > INT32_MAX) + return USER_RESERVED; + + return USER_REGULAR; +} + +int user_record_removable(UserRecord *h) { + UserStorage storage; + assert(h); + + if (h->removable >= 0) + return h->removable; + + /* Refuse to decide for classic records */ + storage = user_record_storage(h); + if (h->storage < 0 || h->storage == USER_CLASSIC) + return -1; + + /* For now consider only LUKS home directories with a reference by path as removable */ + return storage == USER_LUKS && path_startswith(user_record_image_path(h), "/dev/"); +} + +uint64_t user_record_ratelimit_interval_usec(UserRecord *h) { + assert(h); + + if (h->ratelimit_interval_usec == UINT64_MAX) + return DEFAULT_RATELIMIT_INTERVAL_USEC; + + return h->ratelimit_interval_usec; +} + +uint64_t user_record_ratelimit_burst(UserRecord *h) { + assert(h); + + if (h->ratelimit_burst == UINT64_MAX) + return DEFAULT_RATELIMIT_BURST; + + return h->ratelimit_burst; +} + +uint64_t user_record_ratelimit_next_try(UserRecord *h) { + assert(h); + + /* Calculates when the it's possible to login next. Returns: + * + * UINT64_MAX → Nothing known + * 0 → Right away + * Any other → Next time in CLOCK_REALTIME in usec (which could be in the past) + */ + + if (h->ratelimit_begin_usec == UINT64_MAX || + h->ratelimit_count == UINT64_MAX) + return UINT64_MAX; + + if (h->ratelimit_count < user_record_ratelimit_burst(h)) + return 0; + + return usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)); +} + +bool user_record_equal(UserRecord *a, UserRecord *b) { + assert(a); + assert(b); + + /* We assume that when a record is modified its JSON data is updated at the same time, hence it's + * sufficient to compare the JSON data. */ + + return json_variant_equal(a->json, b->json); +} + +bool user_record_compatible(UserRecord *a, UserRecord *b) { + assert(a); + assert(b); + + /* If either lacks a the regular section, we can't really decide, let's hence say they are + * incompatible. */ + if (!(a->mask & b->mask & USER_RECORD_REGULAR)) + return false; + + return streq_ptr(a->user_name, b->user_name) && + streq_ptr(a->realm, b->realm); +} + +int user_record_compare_last_change(UserRecord *a, UserRecord *b) { + assert(a); + assert(b); + + if (a->last_change_usec == b->last_change_usec) + return 0; + + /* Always consider a record with a timestamp newer than one without */ + if (a->last_change_usec == UINT64_MAX) + return -1; + if (b->last_change_usec == UINT64_MAX) + return 1; + + return CMP(a->last_change_usec, b->last_change_usec); +} + +int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret) { + _cleanup_(user_record_unrefp) UserRecord *c = NULL; + int r; + + assert(h); + assert(ret); + + c = user_record_new(); + if (!c) + return -ENOMEM; + + r = user_record_load(c, h->json, flags); + if (r < 0) + return r; + + *ret = TAKE_PTR(c); + return 0; +} + +int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask) { + _cleanup_(user_record_unrefp) UserRecord *x = NULL, *y = NULL; + int r; + + assert(a); + assert(b); + + /* Compares the two records, but ignores anything not listed in the specified mask */ + + if ((a->mask & ~mask) != 0) { + r = user_record_clone(a, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &x); + if (r < 0) + return r; + + a = x; + } + + if ((b->mask & ~mask) != 0) { + r = user_record_clone(b, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &y); + if (r < 0) + return r; + + b = y; + } + + return user_record_equal(a, b); +} + +int user_record_test_blocked(UserRecord *h) { + usec_t n; + + /* Checks whether access to the specified user shall be allowed at the moment. Returns: + * + * -ESTALE: Record is from the future + * -ENOLCK: Record is blocked + * -EL2HLT: Record is not valid yet + * -EL3HLT: Record is not valid anymore + * + */ + + assert(h); + + n = now(CLOCK_REALTIME); + if (h->last_change_usec != UINT64_MAX && + h->last_change_usec > n) /* Don't allow log ins when the record is from the future */ + return -ESTALE; + + if (h->locked > 0) + return -ENOLCK; + + if (h->not_before_usec != UINT64_MAX && n < h->not_before_usec) + return -EL2HLT; + if (h->not_after_usec != UINT64_MAX && n > h->not_after_usec) + return -EL3HLT; + + return 0; +} + +static const char* const user_storage_table[_USER_STORAGE_MAX] = { + [USER_CLASSIC] = "classic", + [USER_LUKS] = "luks", + [USER_DIRECTORY] = "directory", + [USER_SUBVOLUME] = "subvolume", + [USER_FSCRYPT] = "fscrypt", + [USER_CIFS] = "cifs", +}; + +DEFINE_STRING_TABLE_LOOKUP(user_storage, UserStorage); + +static const char* const user_disposition_table[_USER_DISPOSITION_MAX] = { + [USER_INTRINSIC] = "intrinsic", + [USER_SYSTEM] = "system", + [USER_DYNAMIC] = "dynamic", + [USER_REGULAR] = "regular", + [USER_CONTAINER] = "container", + [USER_RESERVED] = "reserved", +}; + +DEFINE_STRING_TABLE_LOOKUP(user_disposition, UserDisposition); diff --git a/src/shared/user-record.h b/src/shared/user-record.h new file mode 100644 index 0000000000000..83ef1b3f33c27 --- /dev/null +++ b/src/shared/user-record.h @@ -0,0 +1,356 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "sd-id128.h" + +#include "json.h" +#include "missing_resource.h" +#include "time-util.h" + +/* But some limits on disk sizes: not less than 5M, not more than 5T */ +#define USER_DISK_SIZE_MIN (UINT64_C(5)*1024*1024) +#define USER_DISK_SIZE_MAX (UINT64_C(5)*1024*1024*1024*1024) + +/* The default disk size to use when nothing else is specified, relative to free disk space */ +#define USER_DISK_SIZE_DEFAULT_PERCENT 85 + +typedef enum UserDisposition { + USER_INTRINSIC, /* root and nobody */ + USER_SYSTEM, /* statically allocated users for system services */ + USER_DYNAMIC, /* dynamically allocated users for system services */ + USER_REGULAR, /* regular (typically human users) */ + USER_CONTAINER, /* UID ranges allocated for container uses */ + USER_RESERVED, /* Range above 2^31 */ + _USER_DISPOSITION_MAX, + _USER_DISPOSITION_INVALID = -1, +} UserDisposition; + +typedef enum UserHomeStorage { + USER_CLASSIC, + USER_LUKS, + USER_DIRECTORY, /* A directory, and a .identity file in it, which USER_CLASSIC lacks */ + USER_SUBVOLUME, + USER_FSCRYPT, + USER_CIFS, + _USER_STORAGE_MAX, + _USER_STORAGE_INVALID = -1 +} UserStorage; + +typedef enum UserRecordMask { + /* The various sections an identity record may have, as bit mask */ + USER_RECORD_REGULAR = 1U << 0, + USER_RECORD_SECRET = 1U << 1, + USER_RECORD_PRIVILEGED = 1U << 2, + USER_RECORD_PER_MACHINE = 1U << 3, + USER_RECORD_BINDING = 1U << 4, + USER_RECORD_STATUS = 1U << 5, + USER_RECORD_SIGNATURE = 1U << 6, + _USER_RECORD_MASK_MAX = (1U << 7)-1 +} UserRecordMask; + +typedef enum UserRecordLoadFlags { + /* A set of flags used while loading a user record from JSON data. We leave the lower 6 bits free, + * just as a safety precaution so that we can detect borked conversions between UserRecordMask and + * UserRecordLoadFlags. */ + + /* What to require */ + USER_RECORD_REQUIRE_REGULAR = USER_RECORD_REGULAR << 7, + USER_RECORD_REQUIRE_SECRET = USER_RECORD_SECRET << 7, + USER_RECORD_REQUIRE_PRIVILEGED = USER_RECORD_PRIVILEGED << 7, + USER_RECORD_REQUIRE_PER_MACHINE = USER_RECORD_PER_MACHINE << 7, + USER_RECORD_REQUIRE_BINDING = USER_RECORD_BINDING << 7, + USER_RECORD_REQUIRE_STATUS = USER_RECORD_STATUS << 7, + USER_RECORD_REQUIRE_SIGNATURE = USER_RECORD_SIGNATURE << 7, + + /* What to allow */ + USER_RECORD_ALLOW_REGULAR = USER_RECORD_REGULAR << 14, + USER_RECORD_ALLOW_SECRET = USER_RECORD_SECRET << 14, + USER_RECORD_ALLOW_PRIVILEGED = USER_RECORD_PRIVILEGED << 14, + USER_RECORD_ALLOW_PER_MACHINE = USER_RECORD_PER_MACHINE << 14, + USER_RECORD_ALLOW_BINDING = USER_RECORD_BINDING << 14, + USER_RECORD_ALLOW_STATUS = USER_RECORD_STATUS << 14, + USER_RECORD_ALLOW_SIGNATURE = USER_RECORD_SIGNATURE << 14, + + /* What to strip */ + USER_RECORD_STRIP_REGULAR = USER_RECORD_REGULAR << 21, + USER_RECORD_STRIP_SECRET = USER_RECORD_SECRET << 21, + USER_RECORD_STRIP_PRIVILEGED = USER_RECORD_PRIVILEGED << 21, + USER_RECORD_STRIP_PER_MACHINE = USER_RECORD_PER_MACHINE << 21, + USER_RECORD_STRIP_BINDING = USER_RECORD_BINDING << 21, + USER_RECORD_STRIP_STATUS = USER_RECORD_STATUS << 21, + USER_RECORD_STRIP_SIGNATURE = USER_RECORD_SIGNATURE << 21, + + /* Some special combinations that deserve explicit names */ + USER_RECORD_LOAD_FULL = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_SECRET | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_BINDING | + USER_RECORD_ALLOW_STATUS | + USER_RECORD_ALLOW_SIGNATURE, + + USER_RECORD_LOAD_REFUSE_SECRET = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_BINDING | + USER_RECORD_ALLOW_STATUS | + USER_RECORD_ALLOW_SIGNATURE, + + USER_RECORD_LOAD_MASK_SECRET = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_BINDING | + USER_RECORD_ALLOW_STATUS | + USER_RECORD_ALLOW_SIGNATURE | + USER_RECORD_STRIP_SECRET, + + USER_RECORD_EXTRACT_SECRET = USER_RECORD_REQUIRE_SECRET | + USER_RECORD_STRIP_REGULAR | + USER_RECORD_STRIP_PRIVILEGED | + USER_RECORD_STRIP_PER_MACHINE | + USER_RECORD_STRIP_BINDING | + USER_RECORD_STRIP_STATUS | + USER_RECORD_STRIP_SIGNATURE, + + USER_RECORD_LOAD_SIGNABLE = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE, + + USER_RECORD_EXTRACT_SIGNABLE = USER_RECORD_LOAD_SIGNABLE | + USER_RECORD_STRIP_SECRET | + USER_RECORD_STRIP_BINDING | + USER_RECORD_STRIP_STATUS | + USER_RECORD_STRIP_SIGNATURE, + + USER_RECORD_LOAD_EMBEDDED = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_SIGNATURE, + + USER_RECORD_EXTRACT_EMBEDDED = USER_RECORD_LOAD_EMBEDDED | + USER_RECORD_STRIP_SECRET | + USER_RECORD_STRIP_BINDING | + USER_RECORD_STRIP_STATUS, + + /* Whether to log about loader errors beyond LOG_DEBUG */ + USER_RECORD_LOG = 1U << 28, + + /* Whether to ignore errors and load what we can */ + USER_RECORD_PERMISSIVE = 1U << 29, +} UserRecordLoadFlags; + +static inline UserRecordLoadFlags USER_RECORD_REQUIRE(UserRecordMask m) { + assert((m & ~_USER_RECORD_MASK_MAX) == 0); + return m << 7; +} + +static inline UserRecordLoadFlags USER_RECORD_ALLOW(UserRecordMask m) { + assert((m & ~_USER_RECORD_MASK_MAX) == 0); + return m << 14; +} + +static inline UserRecordLoadFlags USER_RECORD_STRIP(UserRecordMask m) { + assert((m & ~_USER_RECORD_MASK_MAX) == 0); + return m << 21; +} + +static inline UserRecordMask USER_RECORD_REQUIRE_MASK(UserRecordLoadFlags f) { + return (f >> 7) & _USER_RECORD_MASK_MAX; +} + +static inline UserRecordMask USER_RECORD_ALLOW_MASK(UserRecordLoadFlags f) { + return ((f >> 14) & _USER_RECORD_MASK_MAX) | USER_RECORD_REQUIRE_MASK(f); +} + +static inline UserRecordMask USER_RECORD_STRIP_MASK(UserRecordLoadFlags f) { + return (f >> 21) & _USER_RECORD_MASK_MAX; +} + +static inline JsonDispatchFlags USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(UserRecordLoadFlags flags) { + return (FLAGS_SET(flags, USER_RECORD_LOG) ? JSON_LOG : 0) | + (FLAGS_SET(flags, USER_RECORD_PERMISSIVE) ? JSON_PERMISSIVE : 0); +} + +typedef struct UserRecord { + /* The following three fields are not part of the JSON record */ + unsigned n_ref; + UserRecordMask mask; + bool incomplete; /* incomplete due to security restrictions. */ + + char *user_name; + char *realm; + char *user_name_and_realm_auto; /* the user_name field concatenated with '@' and the realm, if the latter is defined */ + char *real_name; + char *email_address; + char *password_hint; + char *icon_name; + char *location; + + UserDisposition disposition; + uint64_t last_change_usec; + uint64_t last_password_change_usec; + + char *shell; + mode_t umask; + char **environment; + char *time_zone; + char *preferred_language; + int nice_level; + struct rlimit *rlimits[_RLIMIT_MAX]; + + int locked; /* prohibit activation in general */ + uint64_t not_before_usec; /* prohibit activation before this unix time */ + uint64_t not_after_usec; /* prohibit activation after this unix time */ + + UserStorage storage; + uint64_t disk_size; + uint64_t disk_size_relative; /* Disk size, relative to the free bytes of the medium, normalized to UINT32_MAX = 100% */ + char *skeleton_directory; + mode_t access_mode; + + uint64_t tasks_max; + uint64_t memory_high; + uint64_t memory_max; + uint64_t cpu_weight; + uint64_t io_weight; + + bool nosuid; + bool nodev; + bool noexec; + + char **hashed_password; + char **ssh_authorized_keys; + char **password; + + void *fscrypt_salt; + size_t fscrypt_salt_size; + + char *cifs_domain; + char *cifs_user_name; + char *cifs_service; + + char *image_path; + char *image_path_auto; /* when none is configured explicitly, this is where we place the implicit image */ + char *home_directory; + char *home_directory_auto; /* when none is set explicitly, this is where we place the implicit home directory */ + + uid_t uid; + gid_t gid; + + char **member_of; + + char *file_system_type; + sd_id128_t partition_uuid; + sd_id128_t luks_uuid; + sd_id128_t file_system_uuid; + + int luks_discard; + char *luks_cipher; + char *luks_cipher_mode; + uint64_t luks_volume_key_size; + char *luks_pbkdf_hash_algorithm; + char *luks_pbkdf_type; + uint64_t luks_pbkdf_time_cost_usec; + uint64_t luks_pbkdf_memory_cost; + uint64_t luks_pbkdf_parallel_threads; + + uint64_t disk_usage; + uint64_t disk_free; + uint64_t disk_ceiling; + uint64_t disk_floor; + + char *state; + char *service; + int signed_locally; + + uint64_t good_authentication_counter; + uint64_t bad_authentication_counter; + uint64_t last_good_authentication_usec; + uint64_t last_bad_authentication_usec; + + uint64_t ratelimit_begin_usec; + uint64_t ratelimit_count; + uint64_t ratelimit_interval_usec; + uint64_t ratelimit_burst; + + int removable; + int enforce_password_policy; + int auto_login; + + uint64_t stop_delay_usec; /* How long to leave systemd --user around on log-out */ + int kill_processes; /* Whether to kill user processes forcibly on log-out */ + + /* The following exist mostly so that we can cover the full /etc/shadow set of fields, we currently + * do not actually make use of these in the systemd codebase */ + uint64_t password_change_min_usec; /* maps to .sp_min */ + uint64_t password_change_max_usec; /* maps to .sp_max */ + uint64_t password_change_warn_usec; /* maps to .sp_warn */ + uint64_t password_change_inactive_usec; /* maps to .sp_inact */ + int password_change_now; /* Require a password change immediately on next login (.sp_lstchg = 0) */ + + JsonVariant *json; +} UserRecord; + +UserRecord* user_record_new(void); +UserRecord* user_record_ref(UserRecord *h); +UserRecord* user_record_unref(UserRecord *h); + +DEFINE_TRIVIAL_CLEANUP_FUNC(UserRecord*, user_record_unref); + +int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags flags); +int user_record_build(UserRecord **ret, ...); + +const char *user_record_user_name_and_realm(UserRecord *h); +UserStorage user_record_storage(UserRecord *h); +const char *user_record_file_system_type(UserRecord *h); +const char *user_record_skeleton_directory(UserRecord *h); +mode_t user_record_access_mode(UserRecord *h); +const char *user_record_home_directory(UserRecord *h); +const char *user_record_image_path(UserRecord *h); +unsigned long user_record_mount_flags(UserRecord *h); +const char *user_record_cifs_user_name(UserRecord *h); +const char *user_record_shell(UserRecord *h); +const char *user_record_real_name(UserRecord *h); +bool user_record_luks_discard(UserRecord *h); +const char *user_record_luks_cipher(UserRecord *h); +const char *user_record_luks_cipher_mode(UserRecord *h); +uint64_t user_record_luks_volume_key_size(UserRecord *h); +const char* user_record_luks_pbkdf_type(UserRecord *h); +usec_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h); +uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h); +uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h); +const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h); +gid_t user_record_gid(UserRecord *h); +UserDisposition user_record_disposition(UserRecord *h); +int user_record_removable(UserRecord *h); +usec_t user_record_ratelimit_interval_usec(UserRecord *h); +uint64_t user_record_ratelimit_burst(UserRecord *h); + +bool user_record_equal(UserRecord *a, UserRecord *b); +bool user_record_compatible(UserRecord *a, UserRecord *b); +int user_record_compare_last_change(UserRecord *a, UserRecord *b); + +usec_t user_record_ratelimit_next_try(UserRecord *h); + +int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret); +int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask); + +int user_record_test_blocked(UserRecord *h); + +/* The following six are user by group-record.c, that's why we export them here */ +int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); + +int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags); +int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags); +int user_group_record_mangle(JsonVariant *v, UserRecordLoadFlags load_flags, JsonVariant **ret_variant, UserRecordMask *ret_mask); + +const char* user_storage_to_string(UserStorage t) _const_; +UserStorage user_storage_from_string(const char *s) _pure_; + +const char* user_disposition_to_string(UserDisposition t) _const_; +UserDisposition user_disposition_from_string(const char *s) _pure_; From 4c95d0e18694d12c5558ee80bf4403686d24945a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 5 Aug 2019 18:21:30 +0200 Subject: [PATCH 065/109] shared: add helpers for converting NSS passwd/group structures to new JSON objects These new calls may be used to convert classic UNIX/glibc NSS struct passwd and struct group records into new-style JSON-based user/group objects. --- src/shared/group-record-nss.c | 211 +++++++++++++++++++++++++ src/shared/group-record-nss.h | 15 ++ src/shared/meson.build | 4 + src/shared/user-record-nss.c | 288 ++++++++++++++++++++++++++++++++++ src/shared/user-record-nss.h | 15 ++ 5 files changed, 533 insertions(+) create mode 100644 src/shared/group-record-nss.c create mode 100644 src/shared/group-record-nss.h create mode 100644 src/shared/user-record-nss.c create mode 100644 src/shared/user-record-nss.h diff --git a/src/shared/group-record-nss.c b/src/shared/group-record-nss.c new file mode 100644 index 0000000000000..e31f2ce697ac5 --- /dev/null +++ b/src/shared/group-record-nss.c @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "errno-util.h" +#include "group-record-nss.h" +#include "strv.h" +#include "user-util.h" + +int nss_group_to_group_record( + const struct group *grp, + const struct sgrp *sgrp, + GroupRecord **ret) { + + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + int r; + + assert(grp); + assert(ret); + + if (isempty(grp->gr_name)) + return -EINVAL; + + if (sgrp && !streq_ptr(sgrp->sg_namp, grp->gr_name)) + return -EINVAL; + + g = group_record_new(); + if (!g) + return -ENOMEM; + + r = free_and_strdup(&g->group_name, grp->gr_name); + if (r < 0) + return r; + + strv_free(g->members); + g->members = strv_copy(grp->gr_mem); + if (!g->members) + return -ENOMEM; + + g->gid = grp->gr_gid; + + if (sgrp) { + if (hashed_password_valid(sgrp->sg_passwd)) { + strv_free_erase(g->hashed_password); + g->hashed_password = strv_new(sgrp->sg_passwd); + if (!g->hashed_password) + return -ENOMEM; + } else + g->hashed_password = strv_free_erase(g->hashed_password); + + r = strv_extend_strv(&g->members, sgrp->sg_mem, 1); + if (r < 0) + return r; + + strv_free(g->administrators); + g->administrators = strv_copy(sgrp->sg_adm); + if (!g->administrators) + return -ENOMEM; + } else { + g->hashed_password = strv_free_erase(g->hashed_password); + g->administrators = strv_free(g->administrators); + } + + g->json = json_variant_unref(g->json); + r = json_build(&g->json, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(g->gid)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->members), "members", JSON_BUILD_STRV(g->members)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(g->hashed_password)))), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->administrators), "administrators", JSON_BUILD_STRV(g->administrators)))); + if (r < 0) + return r; + + g->mask = USER_RECORD_REGULAR | + (!strv_isempty(g->hashed_password) ? USER_RECORD_PRIVILEGED : 0); + + *ret = TAKE_PTR(g); + return 0; +} + +int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer) { + size_t buflen = 4096; + int r; + + assert(grp); + assert(ret_sgrp); + assert(ret_buffer); + + for (;;) { + _cleanup_free_ char *buf = NULL; + struct sgrp sgrp, *result; + + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getsgnam_r(grp->gr_name, &sgrp, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + *ret_sgrp = *result; + *ret_buffer = TAKE_PTR(buf); + return 0; + } + if (r < 0) + return -EIO; /* Weird, this should not return negative! */ + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } +} + +int nss_group_record_by_name(const char *name, GroupRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct group grp, *result; + bool incomplete = false; + size_t buflen = 4096; + struct sgrp sgrp; + int r; + + assert(name); + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getgrnam_r(name, &grp, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + break; + } + + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrnam_r() returned a negative value"); + if (r != ERANGE) + return -r; + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_sgrp_for_group(result, &sgrp, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} + +int nss_group_record_by_gid(gid_t gid, GroupRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct group grp, *result; + bool incomplete = false; + size_t buflen = 4096; + struct sgrp sgrp; + int r; + + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getgrgid_r(gid, &grp, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + break; + } + + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrgid_r() returned a negative value"); + if (r != ERANGE) + return -r; + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_sgrp_for_group(result, &sgrp, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} diff --git a/src/shared/group-record-nss.h b/src/shared/group-record-nss.h new file mode 100644 index 0000000000000..38b2995178ff7 --- /dev/null +++ b/src/shared/group-record-nss.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "group-record.h" + +/* Synthesize GroupRecord objects from NSS data */ + +int nss_group_to_group_record(const struct group *grp, const struct sgrp *sgrp, GroupRecord **ret); +int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer); + +int nss_group_record_by_name(const char *name, GroupRecord **ret); +int nss_group_record_by_gid(gid_t gid, GroupRecord **ret); diff --git a/src/shared/meson.build b/src/shared/meson.build index 81990e848956f..d5b1f283b945a 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -84,6 +84,8 @@ shared_sources = files(''' generator.c generator.h gpt.h + group-record-nss.c + group-record-nss.h group-record.c group-record.h id128-print.c @@ -180,6 +182,8 @@ shared_sources = files(''' uid-range.h unit-file.c unit-file.h + user-record-nss.c + user-record-nss.h user-record.c user-record.h utmp-wtmp.h diff --git a/src/shared/user-record-nss.c b/src/shared/user-record-nss.c new file mode 100644 index 0000000000000..2d881fec8a0ee --- /dev/null +++ b/src/shared/user-record-nss.c @@ -0,0 +1,288 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "errno-util.h" +#include "format-util.h" +#include "strv.h" +#include "user-record-nss.h" +#include "user-util.h" + +int nss_passwd_to_user_record( + const struct passwd *pwd, + const struct spwd *spwd, + UserRecord **ret) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(pwd); + assert(ret); + + if (isempty(pwd->pw_name)) + return -EINVAL; + + if (spwd && !streq_ptr(spwd->sp_namp, pwd->pw_name)) + return -EINVAL; + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = free_and_strdup(&hr->user_name, pwd->pw_name); + if (r < 0) + return r; + + if (isempty(pwd->pw_gecos) || streq_ptr(pwd->pw_gecos, hr->user_name)) + hr->real_name = mfree(hr->real_name); + else { + r = free_and_strdup(&hr->real_name, pwd->pw_gecos); + if (r < 0) + return r; + } + + if (isempty(pwd->pw_dir)) + hr->home_directory = mfree(hr->home_directory); + else { + r = free_and_strdup(&hr->home_directory, pwd->pw_dir); + if (r < 0) + return r; + } + + if (isempty(pwd->pw_shell)) + hr->shell = mfree(hr->shell); + else { + r = free_and_strdup(&hr->shell, pwd->pw_shell); + if (r < 0) + return r; + } + + hr->uid = pwd->pw_uid; + hr->gid = pwd->pw_gid; + + if (spwd) { + if (hashed_password_valid(spwd->sp_pwdp)) { + strv_free_erase(hr->hashed_password); + hr->hashed_password = strv_new(spwd->sp_pwdp); + if (!hr->hashed_password) + return -ENOMEM; + } else + hr->hashed_password = strv_free_erase(hr->hashed_password); + + /* shadow-utils suggests using "chage -E 0" (or -E 1, depending on which man page you check) + * for locking a whole account, hence check for that. Note that it also defines a way to lock + * just a password instead of the whole account, but that's mostly pointless in times of + * password-less authorization, hence let's not bother. */ + + if (spwd->sp_expire >= 0) + hr->locked = spwd->sp_expire <= 1; + else + hr->locked = -1; + + if (spwd->sp_expire > 1 && (uint64_t) spwd->sp_expire < (UINT64_MAX-1)/USEC_PER_DAY) + hr->not_after_usec = spwd->sp_expire * USEC_PER_DAY; + else + hr->not_after_usec = UINT64_MAX; + + if (spwd->sp_lstchg >= 0) + hr->password_change_now = spwd->sp_lstchg == 0; + else + hr->password_change_now = -1; + + if (spwd->sp_lstchg > 0 && (uint64_t) spwd->sp_lstchg <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->last_password_change_usec = spwd->sp_lstchg * USEC_PER_DAY; + else + hr->last_password_change_usec = UINT64_MAX; + + if (spwd->sp_min > 0 && (uint64_t) spwd->sp_min <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_min_usec = spwd->sp_min * USEC_PER_DAY; + else + hr->password_change_min_usec = UINT64_MAX; + + if (spwd->sp_max > 0 && (uint64_t) spwd->sp_max <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_max_usec = spwd->sp_max * USEC_PER_DAY; + else + hr->password_change_max_usec = UINT64_MAX; + + if (spwd->sp_warn > 0 && (uint64_t) spwd->sp_warn <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_warn_usec = spwd->sp_warn * USEC_PER_DAY; + else + hr->password_change_warn_usec = UINT64_MAX; + + if (spwd->sp_inact > 0 && (uint64_t) spwd->sp_inact <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_inactive_usec = spwd->sp_inact * USEC_PER_DAY; + else + hr->password_change_inactive_usec = UINT64_MAX; + } else { + hr->hashed_password = strv_free_erase(hr->hashed_password); + hr->locked = -1; + hr->not_after_usec = UINT64_MAX; + hr->password_change_now = -1, + hr->last_password_change_usec = UINT64_MAX; + hr->password_change_min_usec = UINT64_MAX; + hr->password_change_max_usec = UINT64_MAX; + hr->password_change_warn_usec = UINT64_MAX; + hr->password_change_inactive_usec = UINT64_MAX; + } + + hr->json = json_variant_unref(hr->json); + r = json_build(&hr->json, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(hr->user_name)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(hr->uid)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(hr->gid)), + JSON_BUILD_PAIR_CONDITION(hr->real_name, "realName", JSON_BUILD_STRING(hr->real_name)), + JSON_BUILD_PAIR_CONDITION(hr->home_directory, "homeDirectory", JSON_BUILD_STRING(hr->home_directory)), + JSON_BUILD_PAIR_CONDITION(hr->shell, "shell", JSON_BUILD_STRING(hr->shell)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(hr->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(hr->hashed_password)))), + JSON_BUILD_PAIR_CONDITION(hr->locked >= 0, "locked", JSON_BUILD_BOOLEAN(hr->locked)), + JSON_BUILD_PAIR_CONDITION(hr->not_after_usec != UINT64_MAX, "notAfterUSec", JSON_BUILD_UNSIGNED(hr->not_after_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_now >= 0, "passwordChangeNow", JSON_BUILD_BOOLEAN(hr->password_change_now)), + JSON_BUILD_PAIR_CONDITION(hr->last_password_change_usec != UINT64_MAX, "lastPasswordChangeUSec", JSON_BUILD_UNSIGNED(hr->last_password_change_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_min_usec != UINT64_MAX, "passwordChangeMinUSec", JSON_BUILD_UNSIGNED(hr->password_change_min_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_max_usec != UINT64_MAX, "passwordChangeMaxUSec", JSON_BUILD_UNSIGNED(hr->password_change_max_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_warn_usec != UINT64_MAX, "passwordChangeWarnUSec", JSON_BUILD_UNSIGNED(hr->password_change_warn_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_inactive_usec != UINT64_MAX, "passwordChangeInactiveUSec", JSON_BUILD_UNSIGNED(hr->password_change_inactive_usec)))); + + if (r < 0) + return r; + + hr->mask = USER_RECORD_REGULAR | + (!strv_isempty(hr->hashed_password) ? USER_RECORD_PRIVILEGED : 0); + + *ret = TAKE_PTR(hr); + return 0; +} + +int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer) { + size_t buflen = 4096; + int r; + + assert(pwd); + assert(ret_spwd); + assert(ret_buffer); + + for (;;) { + _cleanup_free_ char *buf = NULL; + struct spwd spwd, *result; + + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getspnam_r(pwd->pw_name, &spwd, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + *ret_spwd = *result; + *ret_buffer = TAKE_PTR(buf); + return 0; + } + if (r < 0) + return -EIO; /* Weird, this should not return negative! */ + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } +} + +int nss_user_record_by_name(const char *name, UserRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct passwd pwd, *result; + bool incomplete = false; + size_t buflen = 4096; + struct spwd spwd; + int r; + + assert(name); + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getpwnam_r(name, &pwd, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + break; + } + + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwnam_r() returned a negative value"); + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_spwd_for_passwd(result, &spwd, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for user %s, ignoring: %m", name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} + +int nss_user_record_by_uid(uid_t uid, UserRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct passwd pwd, *result; + bool incomplete = false; + size_t buflen = 4096; + struct spwd spwd; + int r; + + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getpwuid_r(uid, &pwd, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + break; + } + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwuid_r() returned a negative value"); + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_spwd_for_passwd(result, &spwd, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for UID " UID_FMT ", ignoring: %m", uid); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} diff --git a/src/shared/user-record-nss.h b/src/shared/user-record-nss.h new file mode 100644 index 0000000000000..d5fb23ad2a294 --- /dev/null +++ b/src/shared/user-record-nss.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "user-record.h" + +/* Synthesizes a UserRecord object from NSS data */ + +int nss_passwd_to_user_record(const struct passwd *pwd, const struct spwd *spwd, UserRecord **ret); +int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer); + +int nss_user_record_by_name(const char *name, UserRecord **ret); +int nss_user_record_by_uid(uid_t uid, UserRecord **ret); From 4060e8d2fce33ddc5d0dba1370db96a3fc0907c4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 15:26:32 +0200 Subject: [PATCH 066/109] shared: add internal API for querying JSON user records via varlink This new API can be used in place of NSS by our own internal code if more than the classic UNIX records are needed. --- src/shared/meson.build | 2 + src/shared/userdb.c | 1347 ++++++++++++++++++++++++++++++++++++++++ src/shared/userdb.h | 41 ++ 3 files changed, 1390 insertions(+) create mode 100644 src/shared/userdb.c create mode 100644 src/shared/userdb.h diff --git a/src/shared/meson.build b/src/shared/meson.build index d5b1f283b945a..ea1bd92c9da4b 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -186,6 +186,8 @@ shared_sources = files(''' user-record-nss.h user-record.c user-record.h + userdb.c + userdb.h utmp-wtmp.h varlink.c varlink.h diff --git a/src/shared/userdb.c b/src/shared/userdb.c new file mode 100644 index 0000000000000..eef435d5a5eae --- /dev/null +++ b/src/shared/userdb.c @@ -0,0 +1,1347 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "dirent-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "group-record-nss.h" +#include "missing_syscall.h" +#include "parse-util.h" +#include "set.h" +#include "socket-util.h" +#include "strv.h" +#include "user-record-nss.h" +#include "user-util.h" +#include "userdb.h" +#include "varlink.h" + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(link_hash_ops, void, trivial_hash_func, trivial_compare_func, Varlink, varlink_unref); + +typedef enum LookupWhat { + LOOKUP_USER, + LOOKUP_GROUP, + LOOKUP_MEMBERSHIP, + _LOOKUP_WHAT_MAX, +} LookupWhat; + +struct UserDBIterator { + LookupWhat what; + Set *links; + bool nss_covered:1; + bool nss_iterating:1; + bool synthesize_root:1; + bool synthesize_nobody:1; + int error; + int nss_lock; + unsigned n_found; + sd_event *event; + UserRecord *found_user; /* when .what == LOOKUP_USER */ + GroupRecord *found_group; /* when .what == LOOKUP_GROUP */ + + char *found_user_name, *found_group_name; /* when .what == LOOKUP_MEMBERSHIP */ + char **members_of_group; + size_t index_members_of_group; + char *filter_user_name; +}; + +UserDBIterator* userdb_iterator_free(UserDBIterator *iterator) { + if (!iterator) + return NULL; + + set_free(iterator->links); + + switch (iterator->what) { + + case LOOKUP_USER: + user_record_unref(iterator->found_user); + + if (iterator->nss_iterating) + endpwent(); + + break; + + case LOOKUP_GROUP: + group_record_unref(iterator->found_group); + + if (iterator->nss_iterating) + endgrent(); + + break; + + case LOOKUP_MEMBERSHIP: + free(iterator->found_user_name); + free(iterator->found_group_name); + strv_free(iterator->members_of_group); + free(iterator->filter_user_name); + + if (iterator->nss_iterating) + endgrent(); + + break; + + default: + assert_not_reached("Unexpected state?"); + } + + sd_event_unref(iterator->event); + safe_close(iterator->nss_lock); + + return mfree(iterator); +} + +static UserDBIterator* userdb_iterator_new(LookupWhat what) { + UserDBIterator *i; + + assert(what >= 0); + assert(what < _LOOKUP_WHAT_MAX); + + i = new(UserDBIterator, 1); + if (!i) + return NULL; + + *i = (UserDBIterator) { + .what = what, + .nss_lock = -1, + }; + + return i; +} + +static int userdb_on_query_reply( + Varlink *link, + JsonVariant *parameters, + const char *error_id, + VarlinkReplyFlags flags, + void *userdata) { + + UserDBIterator *iterator = userdata; + int r; + + assert(iterator); + + if (error_id) { + log_debug("Got lookup error: %s", error_id); + + if (STR_IN_SET(error_id, + "io.systemd.UserDatabase.NoRecordFound", + "io.systemd.UserDatabase.ConflictingRecordFound")) + r = -ESRCH; + else if (streq(error_id, "io.systemd.UserDatabase.ServiceNotAvailable")) + r = -EHOSTDOWN; + else if (streq(error_id, VARLINK_ERROR_TIMEOUT)) + r = -ETIMEDOUT; + else + r = -EIO; + + goto finish; + } + + switch (iterator->what) { + + case LOOKUP_USER: { + struct user_data { + JsonVariant *record; + bool incomplete; + } user_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct user_data, record), 0 }, + { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct user_data, incomplete), 0 }, + {} + }; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + + assert_se(!iterator->found_user); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &user_data); + if (r < 0) + goto finish; + + if (!user_data.record) { + r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key"); + goto finish; + } + + hr = user_record_new(); + if (!hr) { + r = -ENOMEM; + goto finish; + } + + r = user_record_load(hr, user_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE); + if (r < 0) + goto finish; + + if (!hr->service) { + r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "User record does not carry service information, refusing."); + goto finish; + } + + hr->incomplete = user_data.incomplete; + + /* We match the root user by the name since the name is our primary key. We match the nobody + * use by UID though, since the name might differ on OSes */ + if (streq_ptr(hr->user_name, "root")) + iterator->synthesize_root = false; + if (hr->uid == UID_NOBODY) + iterator->synthesize_nobody = false; + + iterator->found_user = TAKE_PTR(hr); + iterator->n_found++; + + /* More stuff coming? then let's just exit cleanly here */ + if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + return 0; + + /* Otherwise, let's remove this link and exit cleanly then */ + r = 0; + goto finish; + } + + case LOOKUP_GROUP: { + struct group_data { + JsonVariant *record; + bool incomplete; + } group_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct group_data, record), 0 }, + { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct group_data, incomplete), 0 }, + {} + }; + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + + assert_se(!iterator->found_group); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &group_data); + if (r < 0) + goto finish; + + if (!group_data.record) { + r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key"); + goto finish; + } + + g = group_record_new(); + if (!g) { + r = -ENOMEM; + goto finish; + } + + r = group_record_load(g, group_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE); + if (r < 0) + goto finish; + + if (!g->service) { + r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Group record does not carry service information, refusing."); + goto finish; + } + + g->incomplete = group_data.incomplete; + + if (streq_ptr(g->group_name, "root")) + iterator->synthesize_root = false; + if (g->gid == GID_NOBODY) + iterator->synthesize_nobody = false; + + iterator->found_group = TAKE_PTR(g); + iterator->n_found++; + + if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + return 0; + + r = 0; + goto finish; + } + + case LOOKUP_MEMBERSHIP: { + struct membership_data { + const char *user_name; + const char *group_name; + } membership_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, user_name), JSON_SAFE }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, group_name), JSON_SAFE }, + {} + }; + + assert(!iterator->found_user_name); + assert(!iterator->found_group_name); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &membership_data); + if (r < 0) + goto finish; + + iterator->found_user_name = mfree(iterator->found_user_name); + iterator->found_group_name = mfree(iterator->found_group_name); + + iterator->found_user_name = strdup(membership_data.user_name); + if (!iterator->found_user_name) { + r = -ENOMEM; + goto finish; + } + + iterator->found_group_name = strdup(membership_data.group_name); + if (!iterator->found_group_name) { + r = -ENOMEM; + goto finish; + } + + iterator->n_found++; + + if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + return 0; + + r = 0; + goto finish; + } + + default: + assert_not_reached("unexpected lookup"); + } + +finish: + /* If we got one ESRCH, let that win. This way when we do a wild dump we won't be tripped up by bad + * errors if at least one connection ended cleanly */ + if (r == -ESRCH || iterator->error == 0) + iterator->error = -r; + + assert_se(set_remove(iterator->links, link) == link); + link = varlink_unref(link); + return 0; +} + +static int userdb_connect( + UserDBIterator *iterator, + const char *path, + const char *method, + bool more, + JsonVariant *query) { + + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + int r; + + assert(iterator); + assert(path); + assert(method); + + r = varlink_connect_address(&vl, path); + if (r < 0) + return log_debug_errno(r, "Unable to connect to %s: %m", path); + + varlink_set_userdata(vl, iterator); + + if (!iterator->event) { + r = sd_event_new(&iterator->event); + if (r < 0) + return log_debug_errno(r, "Unable to allocate event loop: %m"); + } + + r = varlink_attach_event(vl, iterator->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_debug_errno(r, "Failed to attach varlink connection to event loop: %m"); + + (void) varlink_set_description(vl, path); + + r = varlink_bind_reply(vl, userdb_on_query_reply); + if (r < 0) + return log_debug_errno(r, "Failed to bind reply callback: %m"); + + if (more) + r = varlink_observe(vl, method, query); + else + r = varlink_invoke(vl, method, query); + if (r < 0) + return log_debug_errno(r, "Failed to invoke varlink method: %m"); + + r = set_ensure_allocated(&iterator->links, &link_hash_ops); + if (r < 0) + return log_debug_errno(r, "Failed to allocate set: %m"); + + r = set_put(iterator->links, vl); + if (r < 0) + return log_debug_errno(r, "Failed to add varlink connection to set: %m"); + + TAKE_PTR(vl); + return r; +} + +static int userdb_start_query( + UserDBIterator *iterator, + const char *method, + bool more, + JsonVariant *query, + UserDBFlags flags) { + + _cleanup_(strv_freep) char **except = NULL, **only = NULL; + _cleanup_(closedirp) DIR *d = NULL; + struct dirent *de; + const char *e; + int r, ret = 0; + + assert(iterator); + assert(method); + + e = getenv("SYSTEMD_BYPASS_USERDB"); + if (e) { + r = parse_boolean(e); + if (r > 0) + return -ENOLINK; + if (r < 0) { + except = strv_split(e, ":"); + if (!except) + return -ENOMEM; + } + } + + e = getenv("SYSTEMD_ONLY_USERDB"); + if (e) { + only = strv_split(e, ":"); + if (!only) + return -ENOMEM; + } + + /* First, let's talk to the multiplexer, if we can */ + if ((flags & (USERDB_AVOID_MULTIPLEXER|USERDB_AVOID_DYNAMIC_USER|USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE)) == 0 && + !strv_contains(except, "io.systemd.Multiplexer") && + (!only || strv_contains(only, "io.systemd.Multiplexer"))) { + _cleanup_(json_variant_unrefp) JsonVariant *patched_query = json_variant_ref(query); + + r = json_variant_set_field_string(&patched_query, "service", "io.systemd.Multiplexer"); + if (r < 0) + return log_debug_errno(r, "Unable to set service JSON field: %m"); + + r = userdb_connect(iterator, "/run/systemd/userdb/io.systemd.Multiplexer", method, more, patched_query); + if (r >= 0) { + iterator->nss_covered = true; /* The multiplexer does NSS */ + return 0; + } + } + + d = opendir("/run/systemd/userdb/"); + if (!d) { + if (errno == ENOENT) + return -ESRCH; + + return -errno; + } + + FOREACH_DIRENT(de, d, return -errno) { + _cleanup_(json_variant_unrefp) JsonVariant *patched_query = NULL; + _cleanup_free_ char *p = NULL; + bool is_nss; + + if (streq(de->d_name, "io.systemd.Multiplexer")) /* We already tried this above, don't try this again */ + continue; + + if (FLAGS_SET(flags, USERDB_AVOID_DYNAMIC_USER) && + streq(de->d_name, "io.systemd.DynamicUser")) + continue; + + /* Avoid NSS is this is requested. Note that we also skip NSS when we were asked to skip the + * multiplexer, since in that case it's safer to do NSS in the client side emulation below + * (and when we run as part of systemd-userdbd.service we don't want to talk to ourselves + * anyway). */ + is_nss = streq(de->d_name, "io.systemd.NameServiceSwitch"); + if ((flags & (USERDB_AVOID_NSS|USERDB_AVOID_MULTIPLEXER)) != 0 && is_nss) + continue; + + if (strv_contains(except, de->d_name)) + continue; + + if (only && !strv_contains(only, de->d_name)) + continue; + + p = path_join("/run/systemd/userdb/", de->d_name); + if (!p) + return -ENOMEM; + + patched_query = json_variant_ref(query); + r = json_variant_set_field_string(&patched_query, "service", de->d_name); + if (r < 0) + return log_debug_errno(r, "Unable to set service JSON field: %m"); + + r = userdb_connect(iterator, p, method, more, patched_query); + if (is_nss && r >= 0) /* Turn off fallback NSS if we found the NSS service and could connect + * to it */ + iterator->nss_covered = true; + + if (ret == 0 && r < 0) + ret = r; + } + + if (set_isempty(iterator->links)) + return ret; /* propagate last error we saw if we couldn't connect to anything. */ + + /* We connected to some services, in this case, ignore the ones we failed on */ + return 0; +} + +static int userdb_process( + UserDBIterator *iterator, + UserRecord **ret_user_record, + GroupRecord **ret_group_record, + char **ret_user_name, + char **ret_group_name) { + + int r; + + assert(iterator); + + for (;;) { + if (iterator->what == LOOKUP_USER && iterator->found_user) { + if (ret_user_record) + *ret_user_record = TAKE_PTR(iterator->found_user); + else + iterator->found_user = user_record_unref(iterator->found_user); + + if (ret_group_record) + *ret_group_record = NULL; + if (ret_user_name) + *ret_user_name = NULL; + if (ret_group_name) + *ret_group_name = NULL; + + return 0; + } + + if (iterator->what == LOOKUP_GROUP && iterator->found_group) { + if (ret_group_record) + *ret_group_record = TAKE_PTR(iterator->found_group); + else + iterator->found_group = group_record_unref(iterator->found_group); + + if (ret_user_record) + *ret_user_record = NULL; + if (ret_user_name) + *ret_user_name = NULL; + if (ret_group_name) + *ret_group_name = NULL; + + return 0; + } + + if (iterator->what == LOOKUP_MEMBERSHIP && iterator->found_user_name && iterator->found_group_name) { + if (ret_user_name) + *ret_user_name = TAKE_PTR(iterator->found_user_name); + else + iterator->found_user_name = mfree(iterator->found_user_name); + + if (ret_group_name) + *ret_group_name = TAKE_PTR(iterator->found_group_name); + else + iterator->found_group_name = mfree(iterator->found_group_name); + + if (ret_user_record) + *ret_user_record = NULL; + if (ret_group_record) + *ret_group_record = NULL; + + return 0; + } + + if (set_isempty(iterator->links)) { + if (iterator->error == 0) + return -ESRCH; + + return -abs(iterator->error); + } + + if (!iterator->event) + return -ESRCH; + + r = sd_event_run(iterator->event, UINT64_MAX); + if (r < 0) + return r; + } +} + +static int synthetic_root_user_build(UserRecord **ret) { + return user_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING("root")), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/root")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +static int synthetic_nobody_user_build(UserRecord **ret) { + return user_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(NOBODY_USER_NAME)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(UID_NOBODY)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)), + JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)), + JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_USER); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, ret, NULL, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + /* Make sure the NSS lookup doesn't recurse back to us. (EBUSY is fine here, it just means we + * already took the lock from our thread, which is totally OK.) */ + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + /* Client-side NSS fallback */ + r = nss_user_record_by_name(name, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (streq(name, "root")) + return synthetic_root_user_build(ret); + + if (streq(name, NOBODY_USER_NAME) && synthesize_nobody()) + return synthetic_nobody_user_build(ret); + } + + return r; +} + +int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!uid_is_valid(uid)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_USER); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, ret, NULL, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + /* Client-side NSS fallback */ + r = nss_user_record_by_uid(uid, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (uid == 0) + return synthetic_root_user_build(ret); + + if (uid == UID_NOBODY && synthesize_nobody()) + return synthetic_nobody_user_build(ret); + } + + return r; +} + +int userdb_all(UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + + assert(ret); + + iterator = userdb_iterator_new(LOOKUP_USER); + if (!iterator) + return -ENOMEM; + + iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE); + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", true, NULL, flags); + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) { + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + setpwent(); + iterator->nss_iterating = true; + goto finish; + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) + goto finish; + + return r; + +finish: + *ret = TAKE_PTR(iterator); + return 0; +} + +int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret) { + int r; + + assert(iterator); + assert(iterator->what == LOOKUP_USER); + + if (iterator->nss_iterating) { + struct passwd *pw; + + /* If NSS isn't covered elsewhere, let's iterate through it first, since it probably contains + * the more traditional sources, which are probably good to show first. */ + + pw = getpwent(); + if (pw) { + _cleanup_free_ char *buffer = NULL; + bool incomplete = false; + struct spwd spwd; + + if (streq_ptr(pw->pw_name, "root")) + iterator->synthesize_root = false; + if (pw->pw_uid == UID_NOBODY) + iterator->synthesize_nobody = false; + + r = nss_spwd_for_passwd(pw, &spwd, &buffer); + if (r < 0) { + log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_passwd_to_user_record(pw, r >= 0 ? &spwd : NULL, ret); + if (r < 0) + return r; + + if (ret) + (*ret)->incomplete = incomplete; + return r; + } + + if (errno != 0) + log_debug_errno(errno, "Failure to iterate NSS user database, ignoring: %m"); + + iterator->nss_iterating = false; + endpwent(); + } + + r = userdb_process(iterator, ret, NULL, NULL, NULL); + + if (r < 0) { + if (iterator->synthesize_root) { + iterator->synthesize_root = false; + iterator->n_found++; + return synthetic_root_user_build(ret); + } + + if (iterator->synthesize_nobody) { + iterator->synthesize_nobody = false; + iterator->n_found++; + return synthetic_nobody_user_build(ret); + } + } + + /* if we found at least one entry, then ignore errors and indicate that we reached the end */ + if (r < 0 && iterator->n_found > 0) + return -ESRCH; + + return r; +} + +static int synthetic_root_group_build(GroupRecord **ret) { + return group_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING("root")), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +static int synthetic_nobody_group_build(GroupRecord **ret) { + return group_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(NOBODY_GROUP_NAME)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_GROUP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, NULL, ret, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + r = nss_group_record_by_name(name, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (streq(name, "root")) + return synthetic_root_group_build(ret); + + if (streq(name, NOBODY_GROUP_NAME) && synthesize_nobody()) + return synthetic_nobody_group_build(ret); + } + + return r; +} + +int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!gid_is_valid(gid)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_GROUP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, NULL, ret, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + r = nss_group_record_by_gid(gid, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (gid == 0) + return synthetic_root_group_build(ret); + + if (gid == GID_NOBODY && synthesize_nobody()) + return synthetic_nobody_group_build(ret); + } + + return r; +} + +int groupdb_all(UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + + assert(ret); + + iterator = userdb_iterator_new(LOOKUP_GROUP); + if (!iterator) + return -ENOMEM; + + iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE); + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", true, NULL, flags); + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) { + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + setgrent(); + iterator->nss_iterating = true; + goto finish; + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) + goto finish; + + return r; + +finish: + *ret = TAKE_PTR(iterator); + return 0; +} + +int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret) { + int r; + + assert(iterator); + assert(iterator->what == LOOKUP_GROUP); + + if (iterator->nss_iterating) { + struct group *gr; + + errno = 0; + gr = getgrent(); + if (gr) { + _cleanup_free_ char *buffer = NULL; + bool incomplete = false; + struct sgrp sgrp; + + if (streq_ptr(gr->gr_name, "root")) + iterator->synthesize_root = false; + if (gr->gr_gid == GID_NOBODY) + iterator->synthesize_nobody = false; + + r = nss_sgrp_for_group(gr, &sgrp, &buffer); + if (r < 0) { + log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", gr->gr_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_group_to_group_record(gr, r >= 0 ? &sgrp : NULL, ret); + if (r < 0) + return r; + + if (ret) + (*ret)->incomplete = incomplete; + return r; + } + + if (errno != 0) + log_debug_errno(errno, "Failure to iterate NSS group database, ignoring: %m"); + + iterator->nss_iterating = false; + endgrent(); + } + + r = userdb_process(iterator, NULL, ret, NULL, NULL); + + if (r < 0) { + if (iterator->synthesize_root) { + iterator->synthesize_root = false; + iterator->n_found++; + return synthetic_root_group_build(ret); + } + + if (iterator->synthesize_nobody) { + iterator->synthesize_nobody = false; + iterator->n_found++; + return synthetic_nobody_group_build(ret); + } + } + + /* if we found at least one entry, then ignore errors and indicate that we reached the end */ + if (r < 0 && iterator->n_found > 0) + return -ESRCH; + + return r; +} + +int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + assert(ret); + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags); + if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS)) + goto finish; + + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + iterator->filter_user_name = strdup(name); + if (!iterator->filter_user_name) + return -ENOMEM; + + setgrent(); + iterator->nss_iterating = true; + + r = 0; + +finish: + if (r >= 0) + *ret = TAKE_PTR(iterator); + return r; +} + +int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + int r; + + assert(ret); + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags); + + if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS)) + goto finish; + + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + /* We ignore all errors here, since the group might be defined by a userdb native service, and we queried them already above. */ + (void) nss_group_record_by_name(name, &gr); + if (gr) { + iterator->members_of_group = strv_copy(gr->members); + if (!iterator->members_of_group) + return -ENOMEM; + + iterator->index_members_of_group = 0; + + iterator->found_group_name = strdup(name); + if (!iterator->found_group_name) + return -ENOMEM; + } + + r = 0; + +finish: + if (r >= 0) + *ret = TAKE_PTR(iterator); + + return r; +} + +int membershipdb_all(UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + + assert(ret); + + iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, NULL, flags); + if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS)) + goto finish; + + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + setgrent(); + iterator->nss_iterating = true; + + r = 0; + +finish: + if (r >= 0) + *ret = TAKE_PTR(iterator); + + return r; +} + +int membershipdb_iterator_get( + UserDBIterator *iterator, + char **ret_user, + char **ret_group) { + + int r; + + assert(iterator); + + for (;;) { + /* If we are iteratring through NSS acquire a new group entry if we haven't acquired one yet. */ + if (!iterator->members_of_group) { + struct group *g; + + if (!iterator->nss_iterating) + break; + + assert(!iterator->found_user_name); + do { + errno = 0; + g = getgrent(); + if (!g) { + if (errno != 0) + log_debug_errno(errno, "Failure during NSS group iteration, ignoring: %m"); + break; + } + + } while (iterator->filter_user_name ? !strv_contains(g->gr_mem, iterator->filter_user_name) : + strv_isempty(g->gr_mem)); + + if (g) { + r = free_and_strdup(&iterator->found_group_name, g->gr_name); + if (r < 0) + return r; + + if (iterator->filter_user_name) + iterator->members_of_group = strv_new(iterator->filter_user_name); + else + iterator->members_of_group = strv_copy(g->gr_mem); + if (!iterator->members_of_group) + return -ENOMEM; + + iterator->index_members_of_group = 0; + } else { + iterator->nss_iterating = false; + endgrent(); + break; + } + } + + assert(iterator->found_group_name); + assert(iterator->members_of_group); + assert(!iterator->found_user_name); + + if (iterator->members_of_group[iterator->index_members_of_group]) { + _cleanup_free_ char *cu = NULL, *cg = NULL; + + if (ret_user) { + cu = strdup(iterator->members_of_group[iterator->index_members_of_group]); + if (!cu) + return -ENOMEM; + } + + if (ret_group) { + cg = strdup(iterator->found_group_name); + if (!cg) + return -ENOMEM; + } + + if (ret_user) + *ret_user = TAKE_PTR(cu); + + if (ret_group) + *ret_group = TAKE_PTR(cg); + + iterator->index_members_of_group++; + return 0; + } + + iterator->members_of_group = strv_free(iterator->members_of_group); + iterator->found_group_name = mfree(iterator->found_group_name); + } + + r = userdb_process(iterator, NULL, NULL, ret_user, ret_group); + if (r < 0 && iterator->n_found > 0) + return -ESRCH; + + return r; +} + +int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_strv_free_ char **members = NULL; + int r; + + assert(name); + assert(ret); + + r = membershipdb_by_group(name, flags, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *user_name = NULL; + + r = membershipdb_iterator_get(iterator, &user_name, NULL); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + r = strv_consume(&members, TAKE_PTR(user_name)); + if (r < 0) + return r; + } + + strv_sort(members); + strv_uniq(members); + + *ret = TAKE_PTR(members); + return 0; +} + +static int userdb_thread_sockaddr(struct sockaddr_un *ret_sa, socklen_t *ret_salen) { + static const uint8_t + k1[16] = { 0x35, 0xc1, 0x1f, 0x41, 0x59, 0xc6, 0xa0, 0xf9, 0x33, 0x4b, 0x17, 0x3d, 0xb9, 0xf6, 0x14, 0xd9 }, + k2[16] = { 0x6a, 0x11, 0x4c, 0x37, 0xe5, 0xa3, 0x8c, 0xa6, 0x93, 0x55, 0x64, 0x8c, 0x93, 0xee, 0xa1, 0x7b }; + + struct siphash sh; + uint64_t x, y; + pid_t tid; + void *p; + + assert(ret_sa); + assert(ret_salen); + + /* This calculates an AF_UNIX socket address in the abstract namespace whose existance works as an + * indicator whether to emulate NSS records for complex user records that are also available via the + * varlink protocol. The name of the socket is picked in a way so that: + * + * → it is per-thread (by hashing from the TID) + * + * → is not guessable for foreign processes (by hashing from the — hopefully secret — AT_RANDOM + * value every process gets passed from the kernel + * + * By using a socket the NSS emulation can be nicely turned off for limited amounts of time only, + * simply controlled by the lifetime of the fd itself. By using an AF_UNIX socket in the abstract + * namespace the lock is automatically cleaned up when the process dies abnormally. + * + */ + + p = ULONG_TO_PTR(getauxval(AT_RANDOM)); + if (!p) + return -EIO; + + tid = gettid(); + + siphash24_init(&sh, k1); + siphash24_compress(p, 16, &sh); + siphash24_compress(&tid, sizeof(tid), &sh); + x = siphash24_finalize(&sh); + + siphash24_init(&sh, k2); + siphash24_compress(p, 16, &sh); + siphash24_compress(&tid, sizeof(tid), &sh); + y = siphash24_finalize(&sh); + + *ret_sa = (struct sockaddr_un) { + .sun_family = AF_UNIX, + }; + + sprintf(ret_sa->sun_path + 1, "userdb-%016" PRIx64 "%016" PRIx64, x, y); + *ret_salen = offsetof(struct sockaddr_un, sun_path) + 1 + 7 + 32; + + return 0; +} + +int userdb_nss_compat_is_enabled(void) { + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + socklen_t salen; + int r; + + /* Tests whether the NSS compatibility logic is currently turned on for the invoking thread. Returns + * true if NSS compatibility is turned on, i.e. whether NSS records shall be synthesized from complex + * user records. */ + + r = userdb_thread_sockaddr(&sa.un, &salen); + if (r < 0) + return r; + + fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0); + if (fd < 0) + return -errno; + + /* Try to connect(). This doesn't do anything really, except that it checks whether the socket + * address is bound at all. */ + if (connect(fd, &sa.sa, salen) < 0) { + if (errno == ECONNREFUSED) /* the socket is not bound, hence NSS emulation shall be done */ + return true; + + return -errno; + } + + return false; +} + +int userdb_nss_compat_disable(void) { + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + socklen_t salen; + int r; + + /* Turn off the NSS compatibility logic for the invoking thread. By default NSS records are + * synthesized for all complex user records looked up via NSS. If this call is invoked this is + * disabled for the invoking thread, but only for it. A caller that natively supports the varlink + * user record protocol may use that to turn off the compatibility for NSS lookups. */ + + r = userdb_thread_sockaddr(&sa.un, &salen); + if (r < 0) + return r; + + fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + if (bind(fd, &sa.sa, salen) < 0) { + if (errno == EADDRINUSE) /* lock already taken, convert this into a recognizable error */ + return -EBUSY; + + return -errno; + } + + return TAKE_FD(fd); +} diff --git a/src/shared/userdb.h b/src/shared/userdb.h new file mode 100644 index 0000000000000..4288b0ff95dfc --- /dev/null +++ b/src/shared/userdb.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "group-record.h" +#include "user-record.h" + +/* Inquire local services for user/group records */ + +typedef struct UserDBIterator UserDBIterator; + +UserDBIterator *userdb_iterator_free(UserDBIterator *iterator); +DEFINE_TRIVIAL_CLEANUP_FUNC(UserDBIterator*, userdb_iterator_free); + +typedef enum UserDBFlags { + USERDB_AVOID_NSS = 1 << 0, /* don't do client-side nor server-side NSS */ + USERDB_AVOID_DYNAMIC_USER = 1 << 1, /* exclude looking up in io.systemd.DynamicUser */ + USERDB_AVOID_MULTIPLEXER = 1 << 2, /* exclude looking up via io.systemd.Multiplexer */ + USERDB_DONT_SYNTHESIZE = 1 << 3, /* don't synthesize root/nobody */ +} UserDBFlags; + +int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret); +int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret); +int userdb_all(UserDBFlags flags, UserDBIterator **ret); +int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret); + +int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret); +int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret); +int groupdb_all(UserDBFlags flags, UserDBIterator **ret); +int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret); + +int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret); +int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret); +int membershipdb_all(UserDBFlags flags, UserDBIterator **ret); +int membershipdb_iterator_get(UserDBIterator *iterator, char **user, char **group); +int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret); + +int userdb_nss_compat_is_enabled(void); +int userdb_nss_compat_disable(void); From f9f1fb812036d699e75cbc71784a31614c64e88a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 5 Aug 2019 18:21:49 +0200 Subject: [PATCH 067/109] shared: add helpers for displaying new-style user/group records to users --- src/shared/group-record-show.c | 76 ++++++ src/shared/group-record-show.h | 6 + src/shared/meson.build | 4 + src/shared/user-record-show.c | 414 +++++++++++++++++++++++++++++++++ src/shared/user-record-show.h | 8 + 5 files changed, 508 insertions(+) create mode 100644 src/shared/group-record-show.c create mode 100644 src/shared/group-record-show.h create mode 100644 src/shared/user-record-show.c create mode 100644 src/shared/user-record-show.h diff --git a/src/shared/group-record-show.c b/src/shared/group-record-show.c new file mode 100644 index 0000000000000..d0300e483c743 --- /dev/null +++ b/src/shared/group-record-show.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "format-util.h" +#include "group-record-show.h" +#include "strv.h" +#include "user-util.h" +#include "userdb.h" + +void group_record_show(GroupRecord *gr, bool show_full_user_info) { + int r; + + printf(" Group name: %s\n", + group_record_group_name_and_realm(gr)); + + printf(" Disposition: %s\n", user_disposition_to_string(group_record_disposition(gr))); + + if (gr->last_change_usec != USEC_INFINITY) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), gr->last_change_usec)); + } + + if (gid_is_valid(gr->gid)) + printf(" GID: " GID_FMT "\n", gr->gid); + + if (show_full_user_info) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = membershipdb_by_group(gr->group_name, 0, &iterator); + if (r < 0) { + errno = -r; + printf(" Members: (can't acquire: %m)"); + } else { + const char *prefix = " Members:"; + + for (;;) { + _cleanup_free_ char *user = NULL; + + r = membershipdb_iterator_get(iterator, &user, NULL); + if (r == -ESRCH) + break; + if (r < 0) { + errno = -r; + printf("%s (can't iterate: %m\n", prefix); + break; + } + + printf("%s %s\n", prefix, user); + prefix = " "; + } + } + } else { + const char *prefix = " Members:"; + char **i; + + STRV_FOREACH(i, gr->members) { + printf("%s %s\n", prefix, *i); + prefix = " "; + } + } + + if (!strv_isempty(gr->administrators)) { + const char *prefix = " Admins:"; + char **i; + + STRV_FOREACH(i, gr->administrators) { + printf("%s %s\n", prefix, *i); + prefix = " "; + } + } + + if (!strv_isempty(gr->hashed_password)) + printf(" Passwords: %zu\n", strv_length(gr->hashed_password)); + + if (gr->service) + printf(" Service: %s\n", gr->service); +} diff --git a/src/shared/group-record-show.h b/src/shared/group-record-show.h new file mode 100644 index 0000000000000..12bdbd17243c9 --- /dev/null +++ b/src/shared/group-record-show.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "group-record.h" + +void group_record_show(GroupRecord *gr, bool show_full_user_info); diff --git a/src/shared/meson.build b/src/shared/meson.build index ea1bd92c9da4b..234526d7d01d6 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -86,6 +86,8 @@ shared_sources = files(''' gpt.h group-record-nss.c group-record-nss.h + group-record-show.c + group-record-show.h group-record.c group-record.h id128-print.c @@ -184,6 +186,8 @@ shared_sources = files(''' unit-file.h user-record-nss.c user-record-nss.h + user-record-show.c + user-record-show.h user-record.c user-record.h userdb.c diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c new file mode 100644 index 0000000000000..80f89e0acc3e0 --- /dev/null +++ b/src/shared/user-record-show.c @@ -0,0 +1,414 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "format-util.h" +#include "fs-util.h" +#include "group-record.h" +#include "process-util.h" +#include "rlimit-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "user-record-show.h" +#include "user-util.h" +#include "userdb.h" + +const char *user_record_state_color(const char *state) { + if (STR_IN_SET(state, "unfixated", "absent")) + return ansi_grey(); + else if (streq(state, "active")) + return ansi_highlight_green(); + else if (streq(state, "locked")) + return ansi_highlight_yellow(); + + return NULL; +} + +void user_record_show(UserRecord *hr, bool show_full_group_info) { + const char *hd, *ip, *shell; + UserStorage storage; + usec_t t; + int r, b; + + printf(" User name: %s\n", + user_record_user_name_and_realm(hr)); + + if (hr->state) { + const char *color; + + color = user_record_state_color(hr->state); + + printf(" State: %s%s%s\n", + strempty(color), hr->state, color ? ansi_normal() : ""); + } + + printf(" Disposition: %s\n", user_disposition_to_string(user_record_disposition(hr))); + + if (hr->last_change_usec != USEC_INFINITY) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), hr->last_change_usec)); + } + + if (hr->last_password_change_usec != USEC_INFINITY && + hr->last_password_change_usec != hr->last_change_usec) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Passw.: %s\n", format_timestamp(buf, sizeof(buf), hr->last_password_change_usec)); + } + + r = user_record_test_blocked(hr); + switch (r) { + + case -ESTALE: + printf(" Login OK: %sno%s (last change time is in the future)\n", ansi_highlight_red(), ansi_normal()); + break; + + case -ENOLCK: + printf(" Login OK: %sno%s (record is locked)\n", ansi_highlight_red(), ansi_normal()); + break; + + case -EL2HLT: + printf(" Login OK: %sno%s (record not valid yet))\n", ansi_highlight_red(), ansi_normal()); + break; + + case -EL3HLT: + printf(" Login OK: %sno%s (record not valid anymore))\n", ansi_highlight_red(), ansi_normal()); + break; + + default: { + usec_t y; + + if (r < 0) { + errno = -r; + printf(" Login OK: %sno%s (%m)\n", ansi_highlight_red(), ansi_normal()); + break; + } + + if (is_nologin_shell(user_record_shell(hr))) { + printf(" Login OK: %sno%s (nologin shell)\n", ansi_highlight_red(), ansi_normal()); + break; + } + + y = user_record_ratelimit_next_try(hr); + if (y != USEC_INFINITY && y > now(CLOCK_REALTIME)) { + printf(" Login OK: %sno%s (ratelimit)\n", ansi_highlight_red(), ansi_normal()); + break; + } + + printf(" Login OK: %syes%s\n", ansi_highlight_green(), ansi_normal()); + break; + }} + + if (uid_is_valid(hr->uid)) + printf(" UID: " UID_FMT "\n", hr->uid); + if (gid_is_valid(hr->gid)) { + if (show_full_group_info) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + + r = groupdb_by_gid(hr->gid, 0, &gr); + if (r < 0) { + errno = -r; + printf(" GID: " GID_FMT " (unresolvable: %m)\n", hr->gid); + } else + printf(" GID: " GID_FMT " (%s)\n", hr->gid, gr->group_name); + } else + printf(" GID: " GID_FMT "\n", hr->gid); + } else if (uid_is_valid(hr->uid)) /* Show UID as GID if not separately configured */ + printf(" GID: " GID_FMT "\n", (gid_t) hr->uid); + + if (show_full_group_info) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = membershipdb_by_user(hr->user_name, 0, &iterator); + if (r < 0) { + errno = -r; + printf(" Aux. Groups: (can't acquire: %m)\n"); + } else { + const char *prefix = " Aux. Groups:"; + + for (;;) { + _cleanup_free_ char *group = NULL; + + r = membershipdb_iterator_get(iterator, NULL, &group); + if (r == -ESRCH) + break; + if (r < 0) { + errno = -r; + printf("%s (can't iterate: %m)\n", prefix); + break; + } + + printf("%s %s\n", prefix, group); + prefix = " "; + } + } + } + + if (hr->real_name && !streq(hr->real_name, hr->user_name)) + printf(" Real Name: %s\n", hr->real_name); + + hd = user_record_home_directory(hr); + if (hd) + printf(" Directory: %s\n", hd); + + storage = user_record_storage(hr); + if (storage >= 0) /* Let's be political, and clarify which storage we like, and which we don't. About CIFS we don't complain. */ + printf(" Storage: %s%s\n", user_storage_to_string(storage), + storage == USER_LUKS ? " (strong encryption)" : + storage == USER_FSCRYPT ? " (weak encryption)" : + IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME) ? " (no encryption)" : ""); + + ip = user_record_image_path(hr); + if (ip && !streq_ptr(ip, hd)) + printf(" Image Path: %s\n", ip); + + b = user_record_removable(hr); + if (b >= 0) + printf(" Removable: %s\n", yes_no(b)); + + shell = user_record_shell(hr); + if (shell) + printf(" Shell: %s\n", shell); + + if (hr->email_address) + printf(" Email: %s\n", hr->email_address); + if (hr->location) + printf(" Location: %s\n", hr->location); + if (hr->password_hint) + printf(" Passw. Hint: %s\n", hr->password_hint); + if (hr->icon_name) + printf(" Icon Name: %s\n", hr->icon_name); + + if (hr->time_zone) + printf(" Time Zone: %s\n", hr->time_zone); + + if (hr->preferred_language) + printf(" Language: %s\n", hr->preferred_language); + + if (!strv_isempty(hr->environment)) { + char **i; + + STRV_FOREACH(i, hr->environment) { + printf(i == hr->environment ? + " Environment: %s\n" : + " %s\n", *i); + } + } + + if (hr->locked >= 0) + printf(" Locked: %s\n", yes_no(hr->locked)); + + if (hr->not_before_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Not Before: %s\n", format_timestamp(buf, sizeof(buf), hr->not_before_usec)); + } + + if (hr->not_after_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Not After: %s\n", format_timestamp(buf, sizeof(buf), hr->not_after_usec)); + } + + if (hr->umask != MODE_INVALID) + printf(" UMask: 0%03o\n", hr->umask); + + if (nice_is_valid(hr->nice_level)) + printf(" Nice: %i\n", hr->nice_level); + + for (int j = 0; j < _RLIMIT_MAX; j++) { + if (hr->rlimits[j]) + printf(" Limit: RLIMIT_%s=%" PRIu64 ":%" PRIu64 "\n", + rlimit_to_string(j), (uint64_t) hr->rlimits[j]->rlim_cur, (uint64_t) hr->rlimits[j]->rlim_max); + } + + if (hr->tasks_max != UINT64_MAX) + printf(" Tasks Max: %" PRIu64 "\n", hr->tasks_max); + + if (hr->memory_high != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Memory High: %s\n", format_bytes(buf, sizeof(buf), hr->memory_high)); + } + + if (hr->memory_max != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Memory Max: %s\n", format_bytes(buf, sizeof(buf), hr->memory_max)); + } + + if (hr->cpu_weight != UINT64_MAX) + printf(" CPU Weight: %" PRIu64 "\n", hr->cpu_weight); + + if (hr->io_weight != UINT64_MAX) + printf(" IO Weight: %" PRIu64 "\n", hr->io_weight); + + if (hr->access_mode != MODE_INVALID) + printf(" Access Mode: 0%03oo\n", user_record_access_mode(hr)); + + if (storage == USER_LUKS) { + printf("LUKS Discard: %s\n", yes_no(user_record_luks_discard(hr))); + + if (!sd_id128_is_null(hr->luks_uuid)) + printf(" LUKS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->luks_uuid)); + if (!sd_id128_is_null(hr->partition_uuid)) + printf(" Part UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->partition_uuid)); + if (!sd_id128_is_null(hr->file_system_uuid)) + printf(" FS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->file_system_uuid)); + + if (hr->file_system_type) + printf(" File System: %s\n", user_record_file_system_type(hr)); + + if (hr->luks_cipher) + printf(" LUKS Cipher: %s\n", hr->luks_cipher); + if (hr->luks_cipher_mode) + printf(" Cipher Mode: %s\n", hr->luks_cipher_mode); + if (hr->luks_volume_key_size != UINT64_MAX) + printf(" Volume Key: %zubit\n", hr->luks_volume_key_size * 8); + + if (hr->luks_pbkdf_type) + printf(" PBKDF Type: %s\n", hr->luks_pbkdf_type); + if (hr->luks_pbkdf_hash_algorithm) + printf(" PBKDF Hash: %s\n", hr->luks_pbkdf_hash_algorithm); + if (hr->luks_pbkdf_time_cost_usec != UINT64_MAX) { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" PBKDF Time: %s\n", format_timespan(buf, sizeof(buf), hr->luks_pbkdf_time_cost_usec, 0)); + } + if (hr->luks_pbkdf_memory_cost != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" PBKDF Bytes: %s\n", format_bytes(buf, sizeof(buf), hr->luks_pbkdf_memory_cost)); + } + if (hr->luks_pbkdf_parallel_threads != UINT64_MAX) + printf("PBKDF Thread: %" PRIu64 "\n", hr->luks_pbkdf_parallel_threads); + + } else if (storage == USER_CIFS) { + + if (hr->cifs_service) + printf("CIFS Service: %s\n", hr->cifs_service); + } + + if (hr->cifs_user_name) + printf(" CIFS User: %s\n", user_record_cifs_user_name(hr)); + if (hr->cifs_domain) + printf(" CIFS Domain: %s\n", hr->cifs_domain); + + if (storage != USER_CLASSIC) + printf(" Mount Flags: %s %s %s\n", + hr->nosuid ? "nosuid" : "suid", + hr->nodev ? "nodev" : "dev", + hr->noexec ? "noexec" : "exec"); + + if (hr->skeleton_directory) + printf(" Skel. Dir.: %s\n", user_record_skeleton_directory(hr)); + + if (hr->disk_size != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Size: %s\n", format_bytes(buf, sizeof(buf), hr->disk_size)); + } + + if (hr->disk_usage != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Usage: %s\n", format_bytes(buf, sizeof(buf), hr->disk_usage)); + } + + if (hr->disk_free != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Free: %s\n", format_bytes(buf, sizeof(buf), hr->disk_free)); + } + + if (hr->disk_floor != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Floor: %s\n", format_bytes(buf, sizeof(buf), hr->disk_floor)); + } + + if (hr->disk_ceiling != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf("Disk Ceiling: %s\n", format_bytes(buf, sizeof(buf), hr->disk_ceiling)); + } + + if (hr->good_authentication_counter != UINT64_MAX) + printf(" Good Auth.: %" PRIu64 "\n", hr->good_authentication_counter); + + if (hr->last_good_authentication_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Good: %s\n", format_timestamp(buf, sizeof(buf), hr->last_good_authentication_usec)); + } + + if (hr->bad_authentication_counter != UINT64_MAX) + printf(" Bad Auth.: %" PRIu64 "\n", hr->bad_authentication_counter); + + if (hr->last_bad_authentication_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Bad: %s\n", format_timestamp(buf, sizeof(buf), hr->last_bad_authentication_usec)); + } + + t = user_record_ratelimit_next_try(hr); + if (t != USEC_INFINITY) { + usec_t n = now(CLOCK_REALTIME); + + if (t <= n) + printf(" Next Try: anytime\n"); + else { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Next Try: %sin %s%s\n", + ansi_highlight_red(), + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC), + ansi_normal()); + } + } + + if (storage != USER_CLASSIC) { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Auth. Limit: %" PRIu64 " attempts per %s\n", user_record_ratelimit_burst(hr), + format_timespan(buf, sizeof(buf), user_record_ratelimit_interval_usec(hr), 0)); + } + + if (hr->enforce_password_policy >= 0) + printf(" Passwd Pol.: %s\n", yes_no(hr->enforce_password_policy)); + + if (hr->password_change_min_usec != UINT64_MAX || + hr->password_change_max_usec != UINT64_MAX || + hr->password_change_warn_usec != UINT64_MAX || + hr->password_change_inactive_usec != UINT64_MAX) { + + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Passwd Chg.:"); + + if (hr->password_change_min_usec != UINT64_MAX) { + printf(" min %s", format_timespan(buf, sizeof(buf), hr->password_change_min_usec, 0)); + + if (hr->password_change_max_usec != UINT64_MAX) + printf(" …"); + } + + if (hr->password_change_max_usec != UINT64_MAX) + printf(" max %s", format_timespan(buf, sizeof(buf), hr->password_change_max_usec, 0)); + + if (hr->password_change_warn_usec != UINT64_MAX) + printf("/warn %s", format_timespan(buf, sizeof(buf), hr->password_change_warn_usec, 0)); + + if (hr->password_change_inactive_usec != UINT64_MAX) + printf("/inactive %s", format_timespan(buf, sizeof(buf), hr->password_change_inactive_usec, 0)); + + printf("\n"); + } + + if (hr->password_change_now >= 0) + printf("Pas. Ch. Now: %s\n", yes_no(hr->password_change_now)); + + if (!strv_isempty(hr->ssh_authorized_keys)) + printf("SSH Pub. Key: %zu\n", strv_length(hr->ssh_authorized_keys)); + + if (!strv_isempty(hr->hashed_password)) + printf(" Passwords: %zu\n", strv_length(hr->hashed_password)); + + if (hr->signed_locally >= 0) + printf(" Local Sig.: %s\n", yes_no(hr->signed_locally)); + + if (hr->stop_delay_usec != UINT64_MAX) { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Stop Delay: %s\n", format_timespan(buf, sizeof(buf), hr->stop_delay_usec, 0)); + } + + if (hr->auto_login >= 0) + printf("Autom. Login: %s\n", yes_no(hr->auto_login)); + + if (hr->kill_processes >= 0) + printf(" Kill Proc.: %s\n", yes_no(hr->kill_processes)); + + if (hr->service) + printf(" Service: %s\n", hr->service); +} diff --git a/src/shared/user-record-show.h b/src/shared/user-record-show.h new file mode 100644 index 0000000000000..bd22be2ae04ad --- /dev/null +++ b/src/shared/user-record-show.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "user-record.h" + +const char *user_record_state_color(const char *state); + +void user_record_show(UserRecord *hr, bool show_full_group_info); From 50ac473ae31edf1a267151e491a03952fec2bab4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:33:30 +0200 Subject: [PATCH 068/109] userdbd: add new service that can merge userdb queries from multiple clients --- meson.build | 23 + meson_options.txt | 2 + src/userdb/meson.build | 11 + src/userdb/userdbd-manager.c | 301 ++++++++++++ src/userdb/userdbd-manager.h | 34 ++ src/userdb/userdbd.c | 59 +++ src/userdb/userwork.c | 785 +++++++++++++++++++++++++++++++ units/meson.build | 3 + units/systemd-userdbd.service.in | 35 ++ units/systemd-userdbd.socket | 19 + 10 files changed, 1272 insertions(+) create mode 100644 src/userdb/meson.build create mode 100644 src/userdb/userdbd-manager.c create mode 100644 src/userdb/userdbd-manager.h create mode 100644 src/userdb/userdbd.c create mode 100644 src/userdb/userwork.c create mode 100644 units/systemd-userdbd.service.in create mode 100644 units/systemd-userdbd.socket diff --git a/meson.build b/meson.build index e5ceb1e169db3..58ce59935db7d 100644 --- a/meson.build +++ b/meson.build @@ -242,6 +242,7 @@ conf.set_quoted('SYSTEMD_EXPORT_PATH', join_paths(rootlib conf.set_quoted('VENDOR_KEYRING_PATH', join_paths(rootlibexecdir, 'import-pubring.gpg')) conf.set_quoted('USER_KEYRING_PATH', join_paths(pkgsysconfdir, 'import-pubring.gpg')) conf.set_quoted('DOCUMENT_ROOT', join_paths(pkgdatadir, 'gatewayd')) +conf.set_quoted('SYSTEMD_USERWORK_PATH', join_paths(rootlibexecdir, 'systemd-userwork')) conf.set10('MEMORY_ACCOUNTING_DEFAULT', memory_accounting_default) conf.set_quoted('MEMORY_ACCOUNTING_DEFAULT_YES_NO', memory_accounting_default ? 'yes' : 'no') conf.set('STATUS_UNIT_FORMAT_DEFAULT', 'STATUS_UNIT_FORMAT_' + status_unit_format_default.to_upper()) @@ -1279,6 +1280,7 @@ foreach term : ['utmp', 'localed', 'machined', 'portabled', + 'userdb', 'networkd', 'timedated', 'timesyncd', @@ -1492,6 +1494,7 @@ subdir('src/kernel-install') subdir('src/locale') subdir('src/machine') subdir('src/portable') +subdir('src/userdb') subdir('src/nspawn') subdir('src/resolve') subdir('src/timedate') @@ -1928,6 +1931,26 @@ if conf.get('ENABLE_PORTABLED') == 1 public_programs += exe endif +if conf.get('ENABLE_USERDB') == 1 + executable('systemd-userwork', + systemd_userwork_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) + + executable('systemd-userdbd', + systemd_userdbd_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) +endif + foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] meson.add_install_script(meson_make_symlink, join_paths(rootbindir, 'systemctl'), diff --git a/meson_options.txt b/meson_options.txt index 5dc898eb80480..91a9c14ee64b9 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -90,6 +90,8 @@ option('machined', type : 'boolean', description : 'install the systemd-machined stack') option('portabled', type : 'boolean', description : 'install the systemd-portabled stack') +option('userdb', type : 'boolean', + description : 'install the systemd-userdbd stack') option('networkd', type : 'boolean', description : 'install the systemd-networkd stack') option('timedated', type : 'boolean', diff --git a/src/userdb/meson.build b/src/userdb/meson.build new file mode 100644 index 0000000000000..72d2f7da08c6e --- /dev/null +++ b/src/userdb/meson.build @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +systemd_userwork_sources = files(''' + userwork.c +'''.split()) + +systemd_userdbd_sources = files(''' + userdbd-manager.c + userdbd-manager.h + userdbd.c +'''.split()) diff --git a/src/userdb/userdbd-manager.c b/src/userdb/userdbd-manager.c new file mode 100644 index 0000000000000..a5f6b767238ee --- /dev/null +++ b/src/userdb/userdbd-manager.c @@ -0,0 +1,301 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "sd-daemon.h" + +#include "fd-util.h" +#include "mkdir.h" +#include "process-util.h" +#include "set.h" +#include "signal-util.h" +#include "socket-util.h" +#include "stdio-util.h" +#include "umask-util.h" +#include "userdbd-manager.h" +#include "fs-util.h" + +#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC) + +static int start_workers(Manager *m, bool explicit_request); + +static int on_sigchld(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = userdata; + + assert(s); + assert(m); + + for (;;) { + siginfo_t siginfo = {}; + bool removed = false; + + if (waitid(P_ALL, 0, &siginfo, WNOHANG|WEXITED) < 0) { + if (errno == ECHILD) + break; + + log_warning_errno(errno, "Failed to invoke waitid(): %m"); + break; + } + if (siginfo.si_pid == 0) + break; + + if (set_remove(m->workers_dynamic, PID_TO_PTR(siginfo.si_pid))) + removed = true; + if (set_remove(m->workers_fixed, PID_TO_PTR(siginfo.si_pid))) + removed = true; + + if (!removed) { + log_warning("Weird, got SIGCHLD for unknown child " PID_FMT ", ignoring.", siginfo.si_pid); + continue; + } + + if (siginfo.si_code == CLD_EXITED) { + if (siginfo.si_status == EXIT_SUCCESS) + log_debug("Worker " PID_FMT " exited successfully.", siginfo.si_pid); + else + log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", siginfo.si_pid, siginfo.si_status); + } else if (siginfo.si_code == CLD_KILLED) + log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status)); + else if (siginfo.si_code == CLD_DUMPED) + log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status)); + else + log_warning("Can't handle SIGCHLD of this type"); + } + + (void) start_workers(m, false); /* Fill up workers again if we fell below the low watermark */ + return 0; +} + +static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = userdata; + + assert(s); + assert(m); + + (void) start_workers(m, true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */ + return 0; +} + +int manager_new(Manager **ret) { + Manager *m; + int r; + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .listen_fd = -1, + }; + + r = sd_event_new(&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->workers_fixed = set_new(NULL); + m->workers_dynamic = set_new(NULL); + + if (!m->workers_fixed || !m->workers_dynamic) + return -ENOMEM; + + r = sd_event_add_signal(m->event, &m->sigusr2_event_source, SIGUSR2, on_sigusr2, m); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, &m->sigchld_event_source, SIGCHLD, on_sigchld, m); + if (r < 0) + return r; + + RATELIMIT_INIT(m->worker_ratelimit, 5 * USEC_PER_SEC, 50); + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + if (!m) + return NULL; + + set_free(m->workers_fixed); + set_free(m->workers_dynamic); + + sd_event_source_disable_unref(m->sigusr2_event_source); + sd_event_source_disable_unref(m->sigchld_event_source); + + sd_event_unref(m->event); + + return mfree(m); +} + +static size_t manager_current_workers(Manager *m) { + assert(m); + + return set_size(m->workers_fixed) + set_size(m->workers_dynamic); +} + +static int start_one_worker(Manager *m) { + bool fixed; + pid_t pid; + int r; + + assert(m); + + fixed = set_size(m->workers_fixed) < USERDB_WORKERS_MIN; + + r = safe_fork("(sd-worker)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return log_error_errno(r, "Failed to fork new worker child: %m"); + if (r == 0) { + char pids[DECIMAL_STR_MAX(pid_t)]; + /* Child */ + + log_close(); + + r = close_all_fds(&m->listen_fd, 1); + if (r < 0) { + log_error_errno(r, "Failed to close fds in child: %m"); + _exit(EXIT_FAILURE); + } + + log_open(); + + if (m->listen_fd == 3) { + r = fd_cloexec(3, false); + if (r < 0) { + log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m"); + _exit(EXIT_FAILURE); + } + } else { + if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */ + log_error_errno(errno, "Failed to move listen fd to 3: %m"); + _exit(EXIT_FAILURE); + } + + safe_close(m->listen_fd); + } + + xsprintf(pids, PID_FMT, pid); + if (setenv("LISTEN_PID", pids, 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_PID: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("LISTEN_FDS", "1", 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_FDS: %m"); + _exit(EXIT_FAILURE); + } + + + if (setenv("USERDB_FIXED_WORKER", one_zero(fixed), 1) < 0) { + log_error_errno(errno, "Failed to set $USERDB_FIXED_WORKER: %m"); + _exit(EXIT_FAILURE); + } + + // FIXME drop this line + execl("/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */ + //execl("/usr/bin/valgrind", "valgrind", "/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */ + + execl(SYSTEMD_USERWORK_PATH, "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */ + log_error_errno(errno, "Failed start worker process: %m"); + _exit(EXIT_FAILURE); + } + + if (fixed) + r = set_put(m->workers_fixed, PID_TO_PTR(pid)); + else + r = set_put(m->workers_dynamic, PID_TO_PTR(pid)); + if (r < 0) + return log_error_errno(r, "Failed to add child process to set: %m"); + + return 0; +} + +static int start_workers(Manager *m, bool explicit_request) { + int r; + + assert(m); + + for (;;) { + size_t n; + + n = manager_current_workers(m); + if (n >= USERDB_WORKERS_MIN && (!explicit_request || n >= USERDB_WORKERS_MAX)) + break; + + if (!ratelimit_below(&m->worker_ratelimit)) { + /* If we keep starting workers too often, let's fail the whole daemon, something is wrong */ + sd_event_exit(m->event, EXIT_FAILURE); + + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, something is wrong."); + } + + r = start_one_worker(m); + if (r < 0) + return r; + + explicit_request = false; + } + + return 0; +} + +int manager_startup(Manager *m) { + struct timeval ts; + int n, r; + + assert(m); + assert(m->listen_fd < 0); + + n = sd_listen_fds(false); + if (n < 0) + return log_error_errno(n, "Failed to determine number of passed file descriptors: %m"); + if (n > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one listening fd, got %i.", n); + if (n == 1) + m->listen_fd = SD_LISTEN_FDS_START; + else { + union sockaddr_union sockaddr; + + r = sockaddr_un_set_path(&sockaddr.un, "/run/systemd/userdb/io.systemd.NameServiceSwitch"); + if (r < 0) + return log_error_errno(r, "Cannot assign socket path to socket address: %m"); + + r = mkdir_p("/run/systemd/userdb", 0755); + if (r < 0) + return log_error_errno(r, "Failed to create /run/systemd/userdb: %m"); + + m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (m->listen_fd < 0) + return log_error_errno(errno, "Failed to bind on socket: %m"); + + (void) sockaddr_un_unlink(&sockaddr.un); + + RUN_WITH_UMASK(0000) + if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) + return log_error_errno(errno, "Failed to bind socket: %m"); + + r = symlink_idempotent("io.systemd.NameServiceSwitch", "/run/systemd/userdb/io.systemd.Multiplexer", false); + if (r < 0) + return log_error_errno(r, "Failed to bind io.systemd.Multiplexer: %m"); + + if (listen(m->listen_fd, SOMAXCONN) < 0) + return log_error_errno(errno, "Failed to listen on socket: %m"); + } + + /* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be + * GC'ed on idle */ + if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, timeval_store(&ts, LISTEN_TIMEOUT_USEC), sizeof(ts)) < 0) + return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m"); + + return start_workers(m, false); +} diff --git a/src/userdb/userdbd-manager.h b/src/userdb/userdbd-manager.h new file mode 100644 index 0000000000000..0bf67fe55b443 --- /dev/null +++ b/src/userdb/userdbd-manager.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "varlink.h" +#include "ratelimit.h" + +#define USERDB_WORKERS_MIN 3 +#define USERDB_WORKERS_MAX 4096 + +struct Manager { + sd_event *event; + + Set *workers_fixed; /* Workers 0…USERDB_WORKERS_MIN */ + Set *workers_dynamic; /* Workers USERD_WORKERS_MIN+1…USERDB_WORKERS_MAX */ + + sd_event_source *sigusr2_event_source; + sd_event_source *sigchld_event_source; + + int listen_fd; + + RateLimit worker_ratelimit; +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); diff --git a/src/userdb/userdbd.c b/src/userdb/userdbd.c new file mode 100644 index 0000000000000..c7cf0e9485aeb --- /dev/null +++ b/src/userdb/userdbd.c @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "daemon-util.h" +#include "userdbd-manager.h" +#include "log.h" +#include "main-func.h" +#include "signal-util.h" + +/* This service offers two Varlink services, both implementing io.systemd.UserDatabase: + * + * → io.systemd.NameServiceSwitch: this is a compatibility interface for glibc NSS: it response to + * name lookups by checking the classic NSS interfaces and responding that. + * + * → io.systemd.Multiplexer: this multiplexes lookup requests to all Varlink services that have a + * socket in /run/systemd/userdb/. It's supposed to simplify clients that don't want to implement + * the full iterative logic on their own. + */ + +static int run(int argc, char *argv[]) { + _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup_service(); + + // FIXME + log_set_max_level(LOG_DEBUG); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.NameServiceSwitch:io.systemd.Multiplexer", 1) < 0) + return log_error_errno(errno, "Failed to se $SYSTEMD_BYPASS_USERDB: %m"); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGUSR2, -1) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c new file mode 100644 index 0000000000000..8b9fbb5974640 --- /dev/null +++ b/src/userdb/userwork.c @@ -0,0 +1,785 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "sd-daemon.h" + +#include "env-util.h" +#include "fd-util.h" +#include "group-record-nss.h" +#include "group-record.h" +#include "main-func.h" +#include "process-util.h" +#include "strv.h" +#include "time-util.h" +#include "user-record-nss.h" +#include "user-record.h" +#include "user-util.h" +#include "userdb.h" +#include "varlink.h" + +#define ITERATIONS_MAX 64U +#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) +#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC) +#define CONNECTION_IDLE_USEC (15 * USEC_PER_SEC) +#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC) + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static int add_nss_service(JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *status = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(v); + + /* Patch in service field if it's missing. The assumption here is that this field is unset only for + * NSS records */ + + if (json_variant_by_key(*v, "service")) + return 0; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + status = json_variant_ref(json_variant_by_key(*v, "status")); + z = json_variant_ref(json_variant_by_key(status, sd_id128_to_string(mid, buf))); + + if (json_variant_by_key(z, "service")) + return 0; + + r = json_variant_set_field_string(&z, "service", "io.systemd.NameServiceSwitch"); + if (r < 0) + return r; + + r = json_variant_set_field(&status, buf, z); + if (r < 0) + return r; + + return json_variant_set_field(v, "status", status); +} + +static int build_user_json(Varlink *link, UserRecord *ur, JsonVariant **ret) { + _cleanup_(user_record_unrefp) UserRecord *stripped = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + UserRecordLoadFlags flags; + uid_t peer_uid; + bool trusted; + int r; + + assert(ur); + assert(ret); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) { + log_debug_errno(r, "Unable to query peer UID, ignoring: %m"); + trusted = false; + } else + trusted = peer_uid == 0 || peer_uid == ur->uid; + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = user_record_clone(ur, flags, &stripped); + if (r < 0) + return r; + + stripped->incomplete = + ur->incomplete || + (FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED) && + !FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED)); + + v = json_variant_ref(stripped->json); + r = add_nss_service(&v); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)), + JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete)))); +} + +static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) { + if (uid_is_valid(p.uid)) + r = nss_user_record_by_uid(p.uid, &hr); + else if (p.user_name) + r = nss_user_record_by_name(p.user_name, &hr); + else { + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + setpwent(); + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *z = NULL; + _cleanup_free_ char *sbuf = NULL; + struct passwd *pw; + struct spwd spwd; + + errno = 0; + pw = getpwent(); + if (!pw) { + if (errno != 0) + log_debug_errno(errno, "Failure while iterating through NSS user database, ignoring: %m"); + + break; + } + + r = nss_spwd_for_passwd(pw, &spwd, &sbuf); + if (r < 0) + log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name); + + r = nss_passwd_to_user_record(pw, NULL, &z); + if (r < 0) { + endpwent(); + return r; + } + + if (last) { + r = varlink_notify(link, last); + if (r < 0) { + endpwent(); + return r; + } + + last = json_variant_unref(last); + } + + r = build_user_json(link, z, &last); + if (r < 0) { + endpwent(); + return r; + } + } + + endpwent(); + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + + } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) { + + if (uid_is_valid(p.uid)) + r = userdb_by_uid(p.uid, USERDB_AVOID_MULTIPLEXER, &hr); + else if (p.user_name) + r = userdb_by_name(p.user_name, USERDB_AVOID_MULTIPLEXER, &hr); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + r = userdb_all(USERDB_AVOID_MULTIPLEXER, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *z = NULL; + + r = userdb_iterator_get(iterator, &z); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + if (last) { + r = varlink_notify(link, last); + if (r < 0) + return r; + + last = json_variant_unref(last); + } + + r = build_user_json(link, z, &last); + if (r < 0) + return r; + } + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + } else + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) { + log_debug_errno(r, "User lookup failed abnormally: %m"); + return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL); + } + + if ((uid_is_valid(p.uid) && hr->uid != p.uid) || + (p.user_name && !streq(hr->user_name, p.user_name))) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_user_json(link, hr, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int build_group_json(Varlink *link, GroupRecord *gr, JsonVariant **ret) { + _cleanup_(group_record_unrefp) GroupRecord *stripped = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + UserRecordLoadFlags flags; + uid_t peer_uid; + bool trusted; + int r; + + assert(gr); + assert(ret); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) { + log_debug_errno(r, "Unable to query peer UID, ignoring: %m"); + trusted = false; + } else + trusted = peer_uid == 0; + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = group_record_clone(gr, flags, &stripped); + if (r < 0) + return r; + + stripped->incomplete = + gr->incomplete || + (FLAGS_SET(gr->mask, USER_RECORD_PRIVILEGED) && + !FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED)); + + v = json_variant_ref(gr->json); + r = add_nss_service(&v); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)), + JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete)))); +} + +static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) { + + if (gid_is_valid(p.gid)) + r = nss_group_record_by_gid(p.gid, &g); + else if (p.group_name) + r = nss_group_record_by_name(p.group_name, &g); + else { + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + setgrent(); + + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *z = NULL; + _cleanup_free_ char *sbuf = NULL; + struct group *grp; + struct sgrp sgrp; + + errno = 0; + grp = getgrent(); + if (!grp) { + if (errno != 0) + log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m"); + + break; + } + + r = nss_sgrp_for_group(grp, &sgrp, &sbuf); + if (r < 0) + log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", grp->gr_name); + + r = nss_group_to_group_record(grp, r >= 0 ? &sgrp : NULL, &z); + if (r < 0) { + endgrent(); + return r; + } + + if (last) { + r = varlink_notify(link, last); + if (r < 0) { + endgrent(); + return r; + } + + last = json_variant_unref(last); + } + + r = build_group_json(link, z, &last); + if (r < 0) { + endgrent(); + return r; + } + } + + endgrent(); + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + + } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) { + + if (gid_is_valid(p.gid)) + r = groupdb_by_gid(p.gid, USERDB_AVOID_MULTIPLEXER, &g); + else if (p.group_name) + r = groupdb_by_name(p.group_name, USERDB_AVOID_MULTIPLEXER, &g); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + r = groupdb_all(USERDB_AVOID_MULTIPLEXER, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *z = NULL; + + r = groupdb_iterator_get(iterator, &z); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + if (last) { + r = varlink_notify(link, last); + if (r < 0) + return r; + + last = json_variant_unref(last); + } + + r = build_group_json(link, z, &last); + if (r < 0) + return r; + } + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + } else + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) { + log_debug_errno(r, "Group lookup failed abnormally: %m"); + return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL); + } + + if ((uid_is_valid(p.gid) && g->gid != p.gid) || + (p.group_name && !streq(g->group_name, p.group_name))) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(link, g, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + LookupParameters p = {}; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) { + + if (p.group_name) { + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + const char *last = NULL; + char **i; + + r = nss_group_record_by_name(p.group_name, &g); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) + return r; + + STRV_FOREACH(i, g->members) { + + if (p.user_name && !streq_ptr(p.user_name, *i)) + continue; + + if (last) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)))); + if (r < 0) + return r; + } + + last = *i; + } + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)))); + } else { + _cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL; + + setgrent(); + + for (;;) { + struct group *grp; + const char* two[2], **users, **i; + + errno = 0; + grp = getgrent(); + if (!grp) { + if (errno != 0) + log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m"); + + break; + } + + if (p.user_name) { + if (!strv_contains(grp->gr_mem, p.user_name)) + continue; + + two[0] = p.user_name; + two[1] = NULL; + + users = two; + } else + users = (const char**) grp->gr_mem; + + STRV_FOREACH(i, users) { + + if (last_user_name) { + assert(last_group_name); + + r = varlink_notifyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + if (r < 0) { + endgrent(); + return r; + } + + free(last_user_name); + free(last_group_name); + } + + last_user_name = strdup(*i); + last_group_name = strdup(grp->gr_name); + if (!last_user_name || !last_group_name) { + endgrent(); + return -ENOMEM; + } + } + } + + endgrent(); + + if (!last_user_name) { + assert(!last_group_name); + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + } + + assert(last_group_name); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + } + + } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) { + + _cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL; + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + if (p.group_name) + r = membershipdb_by_group(p.group_name, USERDB_AVOID_MULTIPLEXER, &iterator); + else if (p.user_name) + r = membershipdb_by_user(p.user_name, USERDB_AVOID_MULTIPLEXER, &iterator); + else + r = membershipdb_all(USERDB_AVOID_MULTIPLEXER, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *user_name = NULL, *group_name = NULL; + + r = membershipdb_iterator_get(iterator, &user_name, &group_name); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + /* If both group + user are specified do a-posteriori filtering */ + if (p.group_name && p.user_name && !streq(group_name, p.group_name)) + continue; + + if (last_user_name) { + assert(last_group_name); + + r = varlink_notifyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + if (r < 0) + return r; + + free(last_user_name); + free(last_group_name); + } + + last_user_name = TAKE_PTR(user_name); + last_group_name = TAKE_PTR(group_name); + } + + if (!last_user_name) { + assert(!last_group_name); + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + } + + assert(last_group_name); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + } + + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); +} + +static int process_connection(VarlinkServer *server, int fd) { + _cleanup_(varlink_close_unrefp) Varlink *vl = NULL; + int r; + + r = varlink_server_add_connection(server, fd, &vl); + if (r < 0) { + fd = safe_close(fd); + return log_error_errno(r, "Failed to add connection: %m"); + } + + vl = varlink_ref(vl); + + for (;;) { + r = varlink_process(vl); + if (r == -ENOTCONN) { + log_debug("Connection terminated."); + break; + } + if (r < 0) + return log_error_errno(r, "Failed to process connection: %m"); + if (r > 0) + continue; + + r = varlink_wait(vl, CONNECTION_IDLE_USEC); + if (r < 0) + return log_error_errno(r, "Failed to wait for connection events: %m"); + if (r == 0) + break; + } + + return 0; +} + +static int run(int argc, char *argv[]) { + usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY; + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_close_ int lock = -1; + unsigned n_iterations = 0; + int m, listen_fd, r; + + log_setup_service(); + + // FIXME + log_set_max_level(LOG_DEBUG); + + m = sd_listen_fds(false); + if (m < 0) + return log_error_errno(m, "Failed to determine number of listening fds: %m"); + if (m == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received."); + if (m > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time."); + + listen_fd = SD_LISTEN_FDS_START; + + r = fd_nonblock(listen_fd, false); + if (r < 0) + return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m"); + + r = varlink_server_new(&server, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocate server: %m"); + + r = varlink_server_bind_method_many( + 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 bind methods: %m"); + + r = getenv_bool("USERDB_FIXED_WORKER"); + if (r < 0) + return log_error_errno(r, "Failed to parse USERDB_FIXED_WORKER: %m"); + listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC; + + lock = userdb_nss_compat_disable(); + if (lock < 0) + return log_error_errno(r, "Failed to disable userdb NSS compatibility: %m"); + + start_time = now(CLOCK_MONOTONIC); + + for (;;) { + _cleanup_close_ int fd = -1; + usec_t n; + + /* Exit the worker in regular intervals, to flush out all memory use */ + if (n_iterations++ > ITERATIONS_MAX) { + log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations); + break; + } + + n = now(CLOCK_MONOTONIC); + if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) { + char buf[FORMAT_TIMESPAN_MAX]; + log_debug("Exiting worker, ran for %s, that's enough.", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, start_time), 0)); + break; + } + + if (last_busy_usec == USEC_INFINITY) + last_busy_usec = n; + else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) { + char buf[FORMAT_TIMESPAN_MAX]; + log_debug("Exiting worker, been idle for %s, .", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, last_busy_usec), 0)); + break; + } + + (void) rename_process("systemd-userwork: waiting..."); + + fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC); + + (void) rename_process("systemd-userwork: processing..."); + + if (fd < 0) { + if (errno == EAGAIN) + continue; /* The listening socket as SO_RECVTIMEO set, hence a time-out is + * expected after a while, let's check if it's time to exit + * though. */ + + if (errno == EINTR) + continue; /* Might be that somebody attached via strace, let's just continue + * in that case */ + + return log_error_errno(errno, "Failed to accept() from listening socket: %m"); + } + + if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) { + struct pollfd pfd = { + .fd = listen_fd, + .events = POLLIN, + }; + + /* We only slept a very short time? If so, let's see if there are more sockets + * pending, and if so, let's ask our parent for more workers */ + + if (poll(&pfd, 1, 0) < 0) + return log_error_errno(errno, "Failed to test for POLLIN on listening socket: %m"); + + if (FLAGS_SET(pfd.revents, POLLIN)) { + pid_t parent; + + parent = getppid(); + if (parent <= 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parent already died?"); + + if (kill(parent, SIGUSR1) < 0) + return log_error_errno(errno, "Failed to kill our own parent."); + } + } + + (void) process_connection(server, TAKE_FD(fd)); + last_busy_usec = USEC_INFINITY; + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/units/meson.build b/units/meson.build index cf4fe2e7bfb80..83cd88db13a80 100644 --- a/units/meson.build +++ b/units/meson.build @@ -94,6 +94,8 @@ units = [ 'sockets.target.wants/'], ['systemd-journald.socket', '', 'sockets.target.wants/'], + ['systemd-userdbd.socket', 'ENABLE_USERDB', + 'sockets.target.wants/'], ['systemd-networkd.socket', 'ENABLE_NETWORKD'], ['systemd-poweroff.service', ''], ['systemd-reboot.service', ''], @@ -180,6 +182,7 @@ in_units = [ ['systemd-nspawn@.service', ''], ['systemd-portabled.service', 'ENABLE_PORTABLED', 'dbus-org.freedesktop.portable1.service'], + ['systemd-userdbd.service', 'ENABLE_USERDB'], ['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'], ['systemd-random-seed.service', 'ENABLE_RANDOMSEED', 'sysinit.target.wants/'], diff --git a/units/systemd-userdbd.service.in b/units/systemd-userdbd.service.in new file mode 100644 index 0000000000000..d451ec60243f5 --- /dev/null +++ b/units/systemd-userdbd.service.in @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=User Database Manager +Documentation=man:systemd-userdbd.service(8) +Requires=systemd-userdbd.socket +After=systemd-userdbd.socket +Before=sysinit.target +DefaultDependencies=no + +[Service] +ExecStart=@rootlibexecdir@/systemd-userdbd +Type=notify +WatchdogSec=3min +#CapabilityBoundingSet=CAP_KILL CAP_SYS_PTRACE CAP_SYS_ADMIN CAP_SETGID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +ProtectHostname=yes +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 +SystemCallFilter=@system-service +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +IPAddressDeny=any +LimitNOFILE=@HIGH_RLIMIT_NOFILE@ diff --git a/units/systemd-userdbd.socket b/units/systemd-userdbd.socket new file mode 100644 index 0000000000000..1c749ea1d2321 --- /dev/null +++ b/units/systemd-userdbd.socket @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=User Database Manager Socket +Documentation=man:systemd-userdbd.service(8) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/userdb/io.systemd.Multiplexer +Symlinks=/run/systemd/userdb/io.systemd.NameServiceSwitch +SocketMode=0666 From a9766518293a19368bada9beb1599cdac30e3ca6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 5 Aug 2019 18:22:01 +0200 Subject: [PATCH 069/109] userdbd: add userdbctl tool as client for userdbd --- meson.build | 9 + src/userdb/meson.build | 4 + src/userdb/userdbctl.c | 753 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 766 insertions(+) create mode 100644 src/userdb/userdbctl.c diff --git a/meson.build b/meson.build index 58ce59935db7d..c5d13d83c578b 100644 --- a/meson.build +++ b/meson.build @@ -1949,6 +1949,15 @@ if conf.get('ENABLE_USERDB') == 1 install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) + + executable('userdbctl', + userdbctl_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootbindir) endif foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] diff --git a/src/userdb/meson.build b/src/userdb/meson.build index 72d2f7da08c6e..2f7e1accbf90d 100644 --- a/src/userdb/meson.build +++ b/src/userdb/meson.build @@ -9,3 +9,7 @@ systemd_userdbd_sources = files(''' userdbd-manager.h userdbd.c '''.split()) + +userdbctl_sources = files(''' + userdbctl.c +'''.split()) diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c new file mode 100644 index 0000000000000..8b10503f15730 --- /dev/null +++ b/src/userdb/userdbctl.c @@ -0,0 +1,753 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "dirent-util.h" +#include "errno-list.h" +#include "fd-util.h" +#include "format-table.h" +#include "format-util.h" +#include "group-record-show.h" +#include "main-func.h" +#include "pager.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "socket-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "user-record-show.h" +#include "user-util.h" +#include "userdb.h" +#include "verbs.h" + +static enum { + DISPLAY_CLASSIC, + DISPLAY_TABLE, + DISPLAY_FRIENDLY, + DISPLAY_JSON, + _DISPLAY_INVALID = -1 +} arg_display = _DISPLAY_INVALID; + +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +static char** arg_services = NULL; +static UserDBFlags arg_userdb_flags = 0; + +STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep); + +static int show_user(UserRecord *ur, Table *table) { + int r; + + assert(ur); + + switch (arg_display) { + + case DISPLAY_CLASSIC: + if (!uid_is_valid(ur->uid)) + break; + + printf("%s:x:" UID_FMT ":" GID_FMT ":%s:%s:%s\n", + ur->user_name, + ur->uid, + user_record_gid(ur), + strempty(user_record_real_name(ur)), + user_record_home_directory(ur), + user_record_shell(ur)); + + break; + + case DISPLAY_JSON: + json_variant_dump(ur->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0); + break; + + case DISPLAY_FRIENDLY: + user_record_show(ur, true); + + if (ur->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name); + } + + break; + + case DISPLAY_TABLE: + assert(table); + + r = table_add_many( + table, + TABLE_STRING, ur->user_name, + TABLE_STRING, user_disposition_to_string(user_record_disposition(ur)), + TABLE_UID, ur->uid, + TABLE_GID, user_record_gid(ur), + TABLE_STRING, empty_to_null(ur->real_name), + TABLE_STRING, user_record_home_directory(ur), + TABLE_STRING, user_record_shell(ur), + TABLE_INT, (int) user_record_disposition(ur)); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + break; + + default: + assert_not_reached("Unexpected display mode"); + } + + return 0; +} + +static int display_user(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + bool draw_separator = false; + int ret = 0, r; + + if (arg_display < 0) + arg_display = argc > 1 ? DISPLAY_FRIENDLY : DISPLAY_TABLE; + + if (arg_display == DISPLAY_TABLE) { + table = table_new("name", "disposition", "uid", "gid", "realname", "home", "shell", "disposition-numeric"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100); + (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100); + (void) table_set_empty_string(table, "-"); + (void) table_set_sort(table, 7, 2, (size_t) -1); + (void) table_set_display(table, 0, 1, 2, 3, 4, 5, 6, (size_t) -1); + } + + if (argc > 1) { + char **i; + + STRV_FOREACH(i, argv + 1) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + uid_t uid; + + if (parse_uid(*i, &uid) >= 0) + r = userdb_by_uid(uid, arg_userdb_flags, &ur); + else + r = userdb_by_name(*i, arg_userdb_flags, &ur); + if (r < 0) { + if (r == -ESRCH) + log_error_errno(r, "User %s does not exist.", *i); + else if (r == -EHOSTDOWN) + log_error_errno(r, "Selected user database service is not available for this request."); + else + log_error_errno(r, "Failed to find user %s: %m", *i); + + if (ret >= 0) + ret = r; + } else { + if (draw_separator && arg_display == DISPLAY_FRIENDLY) + putchar('\n'); + + r = show_user(ur, table); + if (r < 0) + return r; + + draw_separator = true; + } + } + } else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = userdb_all(arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate users: %m"); + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + + r = userdb_iterator_get(iterator, &ur); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected user database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next user: %m"); + + if (draw_separator && arg_display == DISPLAY_FRIENDLY) + putchar('\n'); + + r = show_user(ur, table); + if (r < 0) + return r; + + draw_separator = true; + } + } + + if (table) { + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + return ret; +} + +static int show_group(GroupRecord *gr, Table *table) { + int r; + + assert(gr); + + switch (arg_display) { + + case DISPLAY_CLASSIC: { + _cleanup_free_ char *m = NULL; + + if (!gid_is_valid(gr->gid)) + break; + + m = strv_join(gr->members, ","); + if (!m) + return log_oom(); + + printf("%s:x:" GID_FMT ":%s\n", + gr->group_name, + gr->gid, + m); + break; + } + + case DISPLAY_JSON: + json_variant_dump(gr->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0); + break; + + case DISPLAY_FRIENDLY: + group_record_show(gr, true); + + if (gr->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of group record of '%s', output incomplete.", gr->group_name); + } + + break; + + case DISPLAY_TABLE: + assert(table); + + r = table_add_many( + table, + TABLE_STRING, gr->group_name, + TABLE_STRING, user_disposition_to_string(group_record_disposition(gr)), + TABLE_GID, gr->gid, + TABLE_INT, (int) group_record_disposition(gr)); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + break; + + default: + assert_not_reached("Unexpected disply mode"); + } + + return 0; +} + + +static int display_group(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + bool draw_separator = false; + int ret = 0, r; + + if (arg_display < 0) + arg_display = argc > 1 ? DISPLAY_FRIENDLY : DISPLAY_TABLE; + + if (arg_display == DISPLAY_TABLE) { + table = table_new("name", "disposition", "gid", "disposition-numeric"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100); + (void) table_set_sort(table, 3, 2, (size_t) -1); + (void) table_set_display(table, 0, 1, 2, (size_t) -1); + } + + if (argc > 1) { + char **i; + + STRV_FOREACH(i, argv + 1) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + gid_t gid; + + if (parse_gid(*i, &gid) >= 0) + r = groupdb_by_gid(gid, arg_userdb_flags, &gr); + else + r = groupdb_by_name(*i, arg_userdb_flags, &gr); + if (r < 0) { + if (r == -ESRCH) + log_error_errno(r, "Group %s does not exist.", *i); + else if (r == -EHOSTDOWN) + log_error_errno(r, "Selected group database service is not available for this request."); + else + log_error_errno(r, "Failed to find group %s: %m", *i); + + if (ret >= 0) + ret = r; + } else { + if (draw_separator && arg_display == DISPLAY_FRIENDLY) + putchar('\n'); + + r = show_group(gr, table); + if (r < 0) + return r; + + draw_separator = true; + } + } + + } else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = groupdb_all(arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate groups: %m"); + + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + + r = groupdb_iterator_get(iterator, &gr); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected group database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next group: %m"); + + if (draw_separator && arg_display == DISPLAY_FRIENDLY) + putchar('\n'); + + r = show_group(gr, table); + if (r < 0) + return r; + + draw_separator = true; + } + + } + + if (table) { + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + return ret; +} + +static int show_membership(const char *user, const char *group, Table *table) { + int r; + + assert(user); + assert(group); + + switch (arg_display) { + + case DISPLAY_CLASSIC: + /* Strictly speaking there's no 'classic' output for this concept, but let's output it in + * similar style to the classic display for user/group info */ + + printf("%s:%s\n", user, group); + break; + + case DISPLAY_JSON: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("user", JSON_BUILD_STRING(user)), + JSON_BUILD_PAIR("group", JSON_BUILD_STRING(group)))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + break; + } + + case DISPLAY_FRIENDLY: + /* Hmm, this is not particularly friendly, but not sure how we could do this better */ + printf("%s: %s\n", group, user); + break; + + case DISPLAY_TABLE: + assert(table); + + r = table_add_many( + table, + TABLE_STRING, user, + TABLE_STRING, group); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + break; + + default: + assert_not_reached("Unexpected display mode"); + } + + + return 0; +} + +static int display_memberships(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + int ret = 0, r; + + if (arg_display < 0) + arg_display = DISPLAY_TABLE; + + if (arg_display == DISPLAY_TABLE) { + table = table_new("user", "group"); + if (!table) + return log_oom(); + + (void) table_set_sort(table, 0, 1, (size_t) -1); + } + + if (argc > 1) { + char **i; + + STRV_FOREACH(i, argv + 1) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + if (streq(argv[0], "users-in-group")) { + r = membershipdb_by_group(*i, arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate users in group: %m"); + } else if (streq(argv[0], "groups-of-user")) { + r = membershipdb_by_user(*i, arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate groups of user: %m"); + } else + assert_not_reached("Unexpected verb"); + + for (;;) { + _cleanup_free_ char *user = NULL, *group = NULL; + + r = membershipdb_iterator_get(iterator, &user, &group); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected membership database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next membership: %m"); + + r = show_membership(user, group, table); + if (r < 0) + return r; + } + } + } else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = membershipdb_all(arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate memberships: %m"); + + for (;;) { + _cleanup_free_ char *user = NULL, *group = NULL; + + r = membershipdb_iterator_get(iterator, &user, &group); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected membership database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next membership: %m"); + + r = show_membership(user, group, table); + if (r < 0) + return r; + } + } + + if (table) { + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + return ret; +} + +static int display_services(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_(closedirp) DIR *d = NULL; + struct dirent *de; + int r; + + d = opendir("/run/systemd/userdb/"); + if (!d) { + if (errno == ENOENT) { + log_info("No services."); + return 0; + } + + return log_error_errno(errno, "Failed to open /run/systemd/userdb/: %m"); + } + + t = table_new("service", "connectible"); + if (!t) + return log_oom(); + + (void) table_set_sort(t, 0, (size_t) -1); + + FOREACH_DIRENT(de, d, return -errno) { + _cleanup_free_ char *j = NULL, *no = NULL; + union sockaddr_union sockaddr; + _cleanup_close_ int fd = -1; + + j = path_join("/run/systemd/userdb/", de->d_name); + if (!j) + return log_oom(); + + r = sockaddr_un_set_path(&sockaddr.un, j); + if (r < 0) + return log_error_errno(r, "Path %s does not fit in AF_UNIX socket address: %m", j); + + fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return log_error_errno(r, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m"); + + if (connect(fd, &sockaddr.un, SOCKADDR_UN_LEN(sockaddr.un)) < 0) { + no = strjoin("No (", errno_to_name(errno), ")"); + if (!no) + return log_oom(); + } + + r = table_add_many(t, + TABLE_STRING, de->d_name, + TABLE_STRING, no ?: "yes", + TABLE_SET_COLOR, no ? ansi_highlight_red() : ansi_highlight_green()); + if (r < 0) + return log_error_errno(r, "Failed to add table row: %m"); + } + + if (table_get_rows(t) <= 0) { + log_info("No services."); + return 0; + } + + if (arg_display == DISPLAY_JSON) + table_print_json(t, NULL, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO); + else + table_print(t, NULL); + + return 0; +} + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + (void) pager_open(arg_pager_flags); + + r = terminal_urlify_man("userdbctl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] {COMMAND} ...\n\n" + "Show user and group information.\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" + " --display=MODE Select display mode (classic, friendly, table, json)\n" + " -j Equivalent to --display=json\n" + " -s --service=SERVICE[:SERVICE…]\n" + " Query on specified service\n" + " --with-nss=BOOL Control whether to include glibc NSS data\n" + " -N Disable inclusion of glibc NSS data and disable synthesizing\n" + " (Same as --with-nss=no --synthesize=no)\n" + " --synthesize=BOOL Synthesize root/nobody user\n" + "\nCommands:\n" + " user [USER…] Inspect user\n" + " group [GROUP…] Inspect group\n" + " users-in-group [GROUP…] Show users that are members of specified group(s)\n" + " groups-of-user [USER…] Show groups the specified user(s) is a member of\n" + " services Show enabled database services\n" + "\nSee the %s for details.\n" + , program_invocation_short_name + , link + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_DISPLAY, + ARG_WITH_NSS, + ARG_SYNTHESIZE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "display", required_argument, NULL, ARG_DISPLAY }, + { "service", required_argument, NULL, 's' }, + { "with-nss", required_argument, NULL, ARG_WITH_NSS }, + { "synthesize", required_argument, NULL, ARG_SYNTHESIZE }, + {} + }; + + const char *e; + int r; + + assert(argc >= 0); + assert(argv); + + /* We are going to update this environment variable with our own, hence let's first read what is already set */ + e = getenv("SYSTEMD_ONLY_USERDB"); + if (e) { + char **l; + + l = strv_split(e, ":"); + if (!l) + return log_oom(); + + strv_free(arg_services); + arg_services = l; + } + + for (;;) { + int c; + + c = getopt_long(argc, argv, "hjs:N", options, NULL); + if (c < 0) + break; + + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_DISPLAY: + if (streq(optarg, "classic")) + arg_display = DISPLAY_CLASSIC; + else if (streq(optarg, "friendly")) + arg_display = DISPLAY_FRIENDLY; + else if (streq(optarg, "json")) + arg_display = DISPLAY_JSON; + else if (streq(optarg, "table")) + arg_display = DISPLAY_TABLE; + else if (streq(optarg, "help")) { + puts("classic\n" + "friendly\n" + "json"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --display= mode: %s", optarg); + + break; + + case 'j': + arg_display = DISPLAY_JSON; + break; + + case 's': + if (isempty(optarg)) + arg_services = strv_free(arg_services); + else { + char **l; + + l = strv_split(optarg, ":"); + if (!l) + return log_oom(); + + r = strv_extend_strv(&arg_services, l, true); + if (r < 0) + return log_oom(); + } + + break; + + case 'N': + arg_userdb_flags |= USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE; + break; + + case ARG_WITH_NSS: + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --with-nss= parameter: %s", optarg); + + SET_FLAG(arg_userdb_flags, USERDB_AVOID_NSS, !r); + break; + + case ARG_SYNTHESIZE: + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --synthesize= parameter: %s", optarg); + + SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE, !r); + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached("Unhandled option"); + } + } + + return 1; +} + +static int run(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "user", VERB_ANY, VERB_ANY, VERB_DEFAULT, display_user }, + { "group", VERB_ANY, VERB_ANY, 0, display_group }, + { "users-in-group", VERB_ANY, VERB_ANY, 0, display_memberships }, + { "groups-of-user", VERB_ANY, VERB_ANY, 0, display_memberships }, + { "services", VERB_ANY, 1, 0, display_services }, + {} + }; + + int r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (arg_services) { + _cleanup_free_ char *e = NULL; + + e = strv_join(arg_services, ":"); + if (!e) + return log_oom(); + + if (setenv("SYSTEMD_ONLY_USERDB", e, true) < 0) + return log_error_errno(r, "Failed to set $SYSTEMD_ONLY_USERDB: %m"); + + log_info("Enabled services: %s", e); + } else { + if (unsetenv("SYSTEMD_ONLY_USERDB") < 0) + return log_error_errno(r, "Failed to unset $SYSTEMD_ONLY_USERDB: %m"); + } + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); From 01259c1a7fde8d769e62483e08e13ce7bf05982e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:48:45 +0200 Subject: [PATCH 070/109] core: make return parameter of dynamic_user_lookup_name() optional --- src/core/dynamic-user.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/dynamic-user.c b/src/core/dynamic-user.c index e7a2f64525165..5dd7de2c7c4cd 100644 --- a/src/core/dynamic-user.c +++ b/src/core/dynamic-user.c @@ -531,7 +531,6 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) { int r; assert(d); - assert(ret); /* Get the currently assigned UID for the user, if there's any. This simply pops the data from the storage socket, and pushes it back in right-away. */ @@ -547,7 +546,9 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) { if (r < 0) return r; - *ret = uid; + if (ret) + *ret = uid; + return 0; } @@ -734,7 +735,6 @@ int dynamic_user_lookup_name(Manager *m, const char *name, uid_t *ret) { assert(m); assert(name); - assert(ret); /* A friendly call for translating a dynamic user's name into its UID */ From 40274d79c7f5dc2011b91b97fddb537f66412eac Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 14:58:59 +0200 Subject: [PATCH 071/109] core: add user/group resolution varlink interface to PID 1 --- src/core/core-varlink.c | 310 ++++++++++++++++++++++++++++++++++++++++ src/core/core-varlink.h | 7 + src/core/manager.c | 8 +- src/core/manager.h | 3 + src/core/meson.build | 2 + 5 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 src/core/core-varlink.c create mode 100644 src/core/core-varlink.h diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c new file mode 100644 index 0000000000000..628b9ccaaa0dc --- /dev/null +++ b/src/core/core-varlink.c @@ -0,0 +1,310 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "core-varlink.h" +#include "mkdir.h" +#include "user-util.h" +#include "varlink.h" + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static int build_user_json(const char *user_name, uid_t uid, JsonVariant **ret) { + assert(user_name); + assert(uid_is_valid(uid)); + assert(ret); + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR("realName", JSON_BUILD_STRING("Dynamic User")), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/")), + JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)), + JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)), + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic")))))); +} + +static bool user_match_lookup_parameters(LookupParameters *p, const char *name, uid_t uid) { + assert(p); + + if (p->user_name && !streq(name, p->user_name)) + return false; + + if (uid_is_valid(p->uid) && uid != p->uid) + return false; + + return true; +} + +static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + _cleanup_free_ char *found_name = NULL; + uid_t found_uid = UID_INVALID, uid; + Manager *m = userdata; + const char *un; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.DynamicUser")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (uid_is_valid(p.uid)) + r = dynamic_user_lookup_uid(m, p.uid, &found_name); + else if (p.user_name) + r = dynamic_user_lookup_name(m, p.user_name, &found_uid); + else { + Iterator i; + DynamicUser *d; + + HASHMAP_FOREACH(d, m->dynamic_users, i) { + r = dynamic_user_current(d, &uid); + if (r == -EAGAIN) /* not realized yet? */ + continue; + if (r < 0) + return r; + + if (!user_match_lookup_parameters(&p, d->name, uid)) + continue; + + if (v) { + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_user_json(d->name, uid, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) + return r; + + uid = uid_is_valid(found_uid) ? found_uid : p.uid; + un = found_name ?: p.user_name; + + if (!user_match_lookup_parameters(&p, un, uid)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_user_json(un, uid, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int build_group_json(const char *group_name, gid_t gid, JsonVariant **ret) { + assert(group_name); + assert(gid_is_valid(gid)); + assert(ret); + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(group_name)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)), + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic")))))); +} + +static bool group_match_lookup_parameters(LookupParameters *p, const char *name, gid_t gid) { + assert(p); + + if (p->group_name && !streq(name, p->group_name)) + return false; + + if (gid_is_valid(p->gid) && gid != p->gid) + return false; + + return true; +} + +static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + _cleanup_free_ char *found_name = NULL; + uid_t found_gid = GID_INVALID, gid; + Manager *m = userdata; + const char *gn; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.DynamicUser")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (gid_is_valid(p.gid)) + r = dynamic_user_lookup_uid(m, (uid_t) p.gid, &found_name); + else if (p.group_name) + r = dynamic_user_lookup_name(m, p.group_name, (uid_t*) &found_gid); + else { + DynamicUser *d; + Iterator i; + + HASHMAP_FOREACH(d, m->dynamic_users, i) { + uid_t uid; + + r = dynamic_user_current(d, &uid); + if (r == -EAGAIN) + continue; + if (r < 0) + return r; + + if (!group_match_lookup_parameters(&p, d->name, (gid_t) uid)) + continue; + + if (v) { + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_group_json(d->name, (gid_t) uid, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) + return r; + + gid = gid_is_valid(found_gid) ? found_gid : p.gid; + gn = found_name ?: p.group_name; + + if (!group_match_lookup_parameters(&p, gn, gid)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(gn, gid, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + LookupParameters p = {}; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.DynamicUser")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + /* We don't support auxiliary groups with dynamic users. */ + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +int manager_varlink_init(Manager *m) { + _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL; + int r; + + assert(m); + + if (m->varlink_server) + return 0; + + if (!MANAGER_IS_SYSTEM(m)) + return 0; + + r = varlink_server_new(&s, VARLINK_SERVER_ACCOUNT_UID); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + varlink_server_set_userdata(s, m); + + r = varlink_server_bind_method_many( + s, + "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(s, "/run/systemd/userdb/io.systemd.DynamicUser", 0666); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + m->varlink_server = TAKE_PTR(s); + return 0; +} + +void manager_varlink_done(Manager *m) { + assert(m); + + m->varlink_server = varlink_server_unref(m->varlink_server); +} diff --git a/src/core/core-varlink.h b/src/core/core-varlink.h new file mode 100644 index 0000000000000..89818e2766caa --- /dev/null +++ b/src/core/core-varlink.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "manager.h" + +int manager_varlink_init(Manager *m); +void manager_varlink_done(Manager *m); diff --git a/src/core/manager.c b/src/core/manager.c index d9114bb0c597a..ded90f3d32787 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -31,6 +31,7 @@ #include "bus-util.h" #include "clean-ipc.h" #include "clock-util.h" +#include "core-varlink.h" #include "dbus-job.h" #include "dbus-manager.h" #include "dbus-unit.h" @@ -45,8 +46,8 @@ #include "fileio.h" #include "fs-util.h" #include "hashmap.h" -#include "io-util.h" #include "install.h" +#include "io-util.h" #include "label.h" #include "locale-setup.h" #include "log.h" @@ -1337,6 +1338,7 @@ Manager* manager_free(Manager *m) { lookup_paths_flush_generator(&m->lookup_paths); bus_done(m); + manager_varlink_done(m); exec_runtime_vacuum(m); hashmap_free(m->exec_runtime_by_id); @@ -1698,6 +1700,10 @@ int manager_startup(Manager *m, FILE *serialization, FDSet *fds) { log_warning_errno(r, "Failed to deserialized tracked clients, ignoring: %m"); m->deserialized_subscribed = strv_free(m->deserialized_subscribed); + r = manager_varlink_init(m); + if (r < 0) + log_warning_errno(r, "Failed to set up watchdog server, ignoring: %m"); + /* Third, fire things up! */ manager_coldplug(m); diff --git a/src/core/manager.h b/src/core/manager.h index 308ee013bda9d..73c68488a3ccf 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -15,6 +15,7 @@ #include "list.h" #include "prioq.h" #include "ratelimit.h" +#include "varlink.h" struct libmnt_monitor; typedef struct Unit Unit; @@ -423,6 +424,8 @@ struct Manager { unsigned notifygen; bool honor_device_enumeration; + + VarlinkServer *varlink_server; }; static inline usec_t manager_default_timeout_abort_usec(Manager *m) { diff --git a/src/core/meson.build b/src/core/meson.build index e0f08c057e8d8..3586838f59b3b 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -22,6 +22,8 @@ libcore_sources = ''' bpf-firewall.h cgroup.c cgroup.h + core-varlink.c + core-varlink.h dbus-automount.c dbus-automount.h dbus-cgroup.c From 0740e5c3b74e7ef1f2b2362073a5cf3bf41ab916 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:31:11 +0200 Subject: [PATCH 072/109] nss: hook up nss-systemd with userdb varlink bits This changes nss-systemd to use the new varlink user/group APIs for looking up everything. --- factory/etc/nsswitch.conf | 2 +- meson.build | 9 +- src/nss-systemd/nss-systemd.c | 952 +++++++++++++------------------- src/nss-systemd/nss-systemd.sym | 1 + src/nss-systemd/userdb-glue.c | 352 ++++++++++++ src/nss-systemd/userdb-glue.h | 20 + 6 files changed, 767 insertions(+), 569 deletions(-) create mode 100644 src/nss-systemd/userdb-glue.c create mode 100644 src/nss-systemd/userdb-glue.h diff --git a/factory/etc/nsswitch.conf b/factory/etc/nsswitch.conf index 5470993e34b2a..e7365cd142650 100644 --- a/factory/etc/nsswitch.conf +++ b/factory/etc/nsswitch.conf @@ -1,7 +1,7 @@ # This file is part of systemd. passwd: compat mymachines systemd -group: compat mymachines systemd +group: compat [SUCCESS=merge] mymachines [SUCCESS=merge] systemd shadow: compat hosts: files mymachines resolve [!UNAVAIL=return] dns myhostname diff --git a/meson.build b/meson.build index c5d13d83c578b..d082266910bd1 100644 --- a/meson.build +++ b/meson.build @@ -1521,7 +1521,7 @@ test_dlopen = executable( build_by_default : want_tests != 'false') foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'], - ['systemd', 'ENABLE_NSS_SYSTEMD'], + ['systemd', 'ENABLE_NSS_SYSTEMD', 'src/nss-systemd/userdb-glue.c src/nss-systemd/userdb-glue.h'], ['mymachines', 'ENABLE_NSS_MYMACHINES'], ['resolve', 'ENABLE_NSS_RESOLVE']] @@ -1532,9 +1532,14 @@ foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'], sym = 'src/nss-@0@/nss-@0@.sym'.format(module) version_script_arg = join_paths(project_source_root, sym) + sources = ['src/nss-@0@/nss-@0@.c'.format(module)] + if tuple.length() > 2 + sources += tuple[2].split() + endif + nss = shared_library( 'nss_' + module, - 'src/nss-@0@/nss-@0@.c'.format(module), + sources, disable_mempool_c, version : '2', include_directories : includes, diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c index 8ef1cd5ea9e74..ae95bba78318b 100644 --- a/src/nss-systemd/nss-systemd.c +++ b/src/nss-systemd/nss-systemd.c @@ -3,28 +3,17 @@ #include #include -#include "sd-bus.h" - -#include "alloc-util.h" -#include "bus-common-errors.h" -#include "dirent-util.h" #include "env-util.h" +#include "errno-util.h" #include "fd-util.h" -#include "format-util.h" -#include "fs-util.h" -#include "list.h" +#include "group-record-nss.h" #include "macro.h" #include "nss-util.h" #include "signal-util.h" -#include "stdio-util.h" -#include "string-util.h" +#include "strv.h" #include "user-util.h" -#include "util.h" - -#define DYNAMIC_USER_GECOS "Dynamic User" -#define DYNAMIC_USER_PASSWD "*" /* locked */ -#define DYNAMIC_USER_DIR "/" -#define DYNAMIC_USER_SHELL NOLOGIN +#include "userdb-glue.h" +#include "userdb.h" static const struct passwd root_passwd = { .pw_name = (char*) "root", @@ -60,78 +49,33 @@ static const struct group nobody_group = { .gr_mem = (char*[]) { NULL }, }; -typedef struct UserEntry UserEntry; -typedef struct GetentData GetentData; +typedef struct GetentData { + /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really reentrant since it + * shares the reading position in the stream with all other threads', we need to protect the data in + * UserDBIterator from multithreaded programs which may call setpwent(), getpwent_r(), or endpwent() + * simultaneously. So, each function locks the data by using the mutex below. */ + pthread_mutex_t mutex; + UserDBIterator *iterator; -struct UserEntry { - uid_t id; - char *name; + /* Applies to group iterations only: this is false while we iterate through groups natviely + * defined. It's true when we iterate through all memberships that extend groups that are + * non-natively (i.e. in NSS defined). */ + bool by_membership; +} GetentData; - GetentData *data; - LIST_FIELDS(UserEntry, entries); +static GetentData getpwent_data = { + .mutex = PTHREAD_MUTEX_INITIALIZER }; -struct GetentData { - /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really - * reentrant since it shares the reading position in the stream with all other threads', - * we need to protect the data in UserEntry from multithreaded programs which may call - * setpwent(), getpwent_r(), or endpwent() simultaneously. So, each function locks the - * data by using the mutex below. */ - pthread_mutex_t mutex; - - UserEntry *position; - LIST_HEAD(UserEntry, entries); +static GetentData getgrent_data = { + .mutex = PTHREAD_MUTEX_INITIALIZER }; -static GetentData getpwent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL }; -static GetentData getgrent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL }; - NSS_GETPW_PROTOTYPES(systemd); NSS_GETGR_PROTOTYPES(systemd); -enum nss_status _nss_systemd_endpwent(void) _public_; -enum nss_status _nss_systemd_setpwent(int stayopen) _public_; -enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) _public_; -enum nss_status _nss_systemd_endgrent(void) _public_; -enum nss_status _nss_systemd_setgrent(int stayopen) _public_; -enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) _public_; - -static int direct_lookup_name(const char *name, uid_t *ret) { - _cleanup_free_ char *s = NULL; - const char *path; - int r; - - assert(name); - - /* Normally, we go via the bus to resolve names. That has the benefit that it is available from any mount - * namespace and subject to proper authentication. However, there's one problem: if our module is called from - * dbus-daemon itself we really can't use D-Bus to communicate. In this case, resort to a client-side hack, - * and look for the dynamic names directly. This is pretty ugly, but breaks the cyclic dependency. */ - - path = strjoina("/run/systemd/dynamic-uid/direct:", name); - r = readlink_malloc(path, &s); - if (r < 0) - return r; - - return parse_uid(s, ret); -} - -static int direct_lookup_uid(uid_t uid, char **ret) { - char path[STRLEN("/run/systemd/dynamic-uid/direct:") + DECIMAL_STR_MAX(uid_t) + 1], *s; - int r; - - xsprintf(path, "/run/systemd/dynamic-uid/direct:" UID_FMT, uid); - - r = readlink_malloc(path, &s); - if (r < 0) - return r; - if (!valid_user_group_name(s)) { /* extra safety check */ - free(s); - return -EINVAL; - } - - *ret = s; - return 0; -} +NSS_PWENT_PROTOTYPES(systemd); +NSS_GRENT_PROTOTYPES(systemd); +NSS_INITGROUPS_PROTOTYPE(systemd); enum nss_status _nss_systemd_getpwnam_r( const char *name, @@ -139,99 +83,49 @@ enum nss_status _nss_systemd_getpwnam_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - uint32_t translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); assert(name); assert(pwd); + assert(errnop); - /* If the username is not valid, then we don't know it. Ideally libc would filter these for us anyway. We don't - * generate EINVAL here, because it isn't really out business to complain about invalid user names. */ + /* If the username is not valid, then we don't know it. Ideally libc would filter these for us + * anyway. We don't generate EINVAL here, because it isn't really out business to complain about + * invalid user names. */ if (!valid_user_group_name(name)) return NSS_STATUS_NOTFOUND; /* Synthesize entries for the root and nobody users, in case they are missing in /etc/passwd */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (streq(name, root_passwd.pw_name)) { *pwd = root_passwd; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - streq(name, nobody_passwd.pw_name)) { - *pwd = nobody_passwd; - return NSS_STATUS_SUCCESS; - } - } - - /* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */ - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) - return NSS_STATUS_NOTFOUND; - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - - if (bypass > 0) { - r = direct_lookup_name(name, (uid_t*) &translated); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByName", - &error, - &reply, - "s", - name); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) + if (streq(name, nobody_passwd.pw_name)) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *pwd = nobody_passwd; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "u", &translated); - if (r < 0) - goto fail; - } + } else if (STR_IN_SET(name, root_passwd.pw_name, nobody_passwd.pw_name)) + return NSS_STATUS_NOTFOUND; - l = strlen(name); - if (buflen < l+1) { + status = userdb_getpwnam(name, pwd, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = -e; + return status; } - memcpy(buffer, name, l+1); - - pwd->pw_name = buffer; - pwd->pw_uid = (uid_t) translated; - pwd->pw_gid = (uid_t) translated; - pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS; - pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD; - pwd->pw_dir = (char*) DYNAMIC_USER_DIR; - pwd->pw_shell = (char*) DYNAMIC_USER_SHELL; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; + return status; } enum nss_status _nss_systemd_getpwuid_r( @@ -240,100 +134,45 @@ enum nss_status _nss_systemd_getpwuid_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - _cleanup_free_ char *direct = NULL; - const char *translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + assert(pwd); + assert(errnop); + if (!uid_is_valid(uid)) return NSS_STATUS_NOTFOUND; /* Synthesize data for the root user and for nobody in case they are missing from /etc/passwd */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (uid == root_passwd.pw_uid) { *pwd = root_passwd; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - uid == nobody_passwd.pw_uid) { - *pwd = nobody_passwd; - return NSS_STATUS_SUCCESS; - } - } - if (!uid_is_dynamic(uid)) - return NSS_STATUS_NOTFOUND; - - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) - return NSS_STATUS_NOTFOUND; - - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - - if (bypass > 0) { - r = direct_lookup_uid(uid, &direct); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - - translated = direct; - - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByUID", - &error, - &reply, - "u", - (uint32_t) uid); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) + if (uid == nobody_passwd.pw_uid) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *pwd = nobody_passwd; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "s", &translated); - if (r < 0) - goto fail; - } + } else if (uid == root_passwd.pw_uid || uid == nobody_passwd.pw_uid) + return NSS_STATUS_NOTFOUND; - l = strlen(translated) + 1; - if (buflen < l) { + status = userdb_getpwuid(uid, pwd, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = -e; + return status; } - memcpy(buffer, translated, l); - - pwd->pw_name = buffer; - pwd->pw_uid = uid; - pwd->pw_gid = uid; - pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS; - pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD; - pwd->pw_dir = (char*) DYNAMIC_USER_DIR; - pwd->pw_shell = (char*) DYNAMIC_USER_SHELL; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; + return status; } #pragma GCC diagnostic ignored "-Wsizeof-pointer-memaccess" @@ -344,94 +183,46 @@ enum nss_status _nss_systemd_getgrnam_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - uint32_t translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); assert(name); assert(gr); + assert(errnop); if (!valid_user_group_name(name)) return NSS_STATUS_NOTFOUND; /* Synthesize records for root and nobody, in case they are missing form /etc/group */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (streq(name, root_group.gr_name)) { *gr = root_group; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - streq(name, nobody_group.gr_name)) { - *gr = nobody_group; - return NSS_STATUS_SUCCESS; - } - } - - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) - return NSS_STATUS_NOTFOUND; - - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - if (bypass > 0) { - r = direct_lookup_name(name, (uid_t*) &translated); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByName", - &error, - &reply, - "s", - name); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) + if (streq(name, nobody_group.gr_name)) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *gr = nobody_group; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "u", &translated); - if (r < 0) - goto fail; - } + } else if (STR_IN_SET(name, root_group.gr_name, nobody_group.gr_name)) + return NSS_STATUS_NOTFOUND; - l = sizeof(char*) + strlen(name) + 1; - if (buflen < l) { + status = userdb_getgrnam(name, gr, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = -e; + return status; } - memzero(buffer, sizeof(char*)); - strcpy(buffer + sizeof(char*), name); - - gr->gr_name = buffer + sizeof(char*); - gr->gr_gid = (gid_t) translated; - gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD; - gr->gr_mem = (char**) buffer; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; + return status; } enum nss_status _nss_systemd_getgrgid_r( @@ -440,154 +231,56 @@ enum nss_status _nss_systemd_getgrgid_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - _cleanup_free_ char *direct = NULL; - const char *translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + assert(gr); + assert(errnop); + if (!gid_is_valid(gid)) return NSS_STATUS_NOTFOUND; /* Synthesize records for root and nobody, in case they are missing from /etc/group */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (gid == root_group.gr_gid) { *gr = root_group; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - gid == nobody_group.gr_gid) { - *gr = nobody_group; - return NSS_STATUS_SUCCESS; - } - } - - if (!gid_is_dynamic(gid)) - return NSS_STATUS_NOTFOUND; - - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) - return NSS_STATUS_NOTFOUND; - - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - if (bypass > 0) { - r = direct_lookup_uid(gid, &direct); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - - translated = direct; - - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByUID", - &error, - &reply, - "u", - (uint32_t) gid); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) + if (gid == nobody_group.gr_gid) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *gr = nobody_group; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "s", &translated); - if (r < 0) - goto fail; - } + } else if (gid == root_group.gr_gid || gid == nobody_group.gr_gid) + return NSS_STATUS_NOTFOUND; - l = sizeof(char*) + strlen(translated) + 1; - if (buflen < l) { + status = userdb_getgrgid(gid, gr, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = -e; + return status; } - memzero(buffer, sizeof(char*)); - strcpy(buffer + sizeof(char*), translated); - - gr->gr_name = buffer + sizeof(char*); - gr->gr_gid = gid; - gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD; - gr->gr_mem = (char**) buffer; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; -} - -static void user_entry_free(UserEntry *p) { - if (!p) - return; - - if (p->data) - LIST_REMOVE(entries, p->data->entries, p); - - free(p->name); - free(p); -} - -static int user_entry_add(GetentData *data, const char *name, uid_t id) { - UserEntry *p; - - assert(data); - - /* This happens when User= or Group= already exists statically. */ - if (!uid_is_dynamic(id)) - return -EINVAL; - - p = new0(UserEntry, 1); - if (!p) - return -ENOMEM; - - p->name = strdup(name); - if (!p->name) { - free(p); - return -ENOMEM; - } - p->id = id; - p->data = data; - - LIST_PREPEND(entries, data->entries, p); - - return 0; -} - -static void systemd_endent(GetentData *data) { - UserEntry *p; - - assert(data); - - while ((p = data->entries)) - user_entry_free(p); - - data->position = NULL; + return status; } static enum nss_status nss_systemd_endent(GetentData *p) { PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + assert(p); + assert_se(pthread_mutex_lock(&p->mutex) == 0); - systemd_endent(p); + p->iterator = userdb_iterator_free(p->iterator); + p->by_membership = false; assert_se(pthread_mutex_unlock(&p->mutex) == 0); return NSS_STATUS_SUCCESS; @@ -601,235 +294,362 @@ enum nss_status _nss_systemd_endgrent(void) { return nss_systemd_endent(&getgrent_data); } -static int direct_enumeration(GetentData *p) { - _cleanup_closedir_ DIR *d = NULL; - struct dirent *de; - int r; +enum nss_status _nss_systemd_setpwent(int stayopen) { + enum nss_status ret; - assert(p); + PROTECT_ERRNO; + BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - d = opendir("/run/systemd/dynamic-uid/"); - if (!d) - return -errno; + if (userdb_nss_compat_is_enabled() <= 0) + return NSS_STATUS_UNAVAIL; - FOREACH_DIRENT(de, d, return -errno) { - _cleanup_free_ char *name = NULL; - uid_t uid, verified; + assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); - if (!dirent_is_file(de)) - continue; + getpwent_data.iterator = userdb_iterator_free(getpwent_data.iterator); + getpwent_data.by_membership = false; - r = parse_uid(de->d_name, &uid); - if (r < 0) - continue; + ret = userdb_all(nss_glue_userdb_flags(), &getpwent_data.iterator) < 0 ? + NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; - r = direct_lookup_uid(uid, &name); - if (r == -ENOMEM) - return r; - if (r < 0) - continue; + assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); + return ret; +} - r = direct_lookup_name(name, &verified); - if (r < 0) - continue; +enum nss_status _nss_systemd_setgrent(int stayopen) { + enum nss_status ret; - if (uid != verified) - continue; + PROTECT_ERRNO; + BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - r = user_entry_add(p, name, uid); - if (r == -ENOMEM) - return r; - if (r < 0) - continue; - } + if (userdb_nss_compat_is_enabled() <= 0) + return NSS_STATUS_UNAVAIL; + + assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0); + + getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator); + getpwent_data.by_membership = false; + + ret = groupdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator) < 0 ? + NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; - return 0; + assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0); + return ret; } -static enum nss_status systemd_setent(GetentData *p) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - const char *name; - uid_t id; - int bypass, r; +enum nss_status _nss_systemd_getpwent_r( + struct passwd *result, + char *buffer, size_t buflen, + int *errnop) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + enum nss_status ret; + int r; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - assert(p); + assert(result); + assert(errnop); - assert_se(pthread_mutex_lock(&p->mutex) == 0); + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } - systemd_endent(p); + assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) + if (!getpwent_data.iterator) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + ret = NSS_STATUS_UNAVAIL; goto finish; - - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; } - if (bypass > 0) { - r = direct_enumeration(p); - if (r < 0) - goto fail; - + r = userdb_iterator_get(getpwent_data.iterator, &ur); + if (r == -ESRCH) { + ret = NSS_STATUS_NOTFOUND; + goto finish; + } + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; goto finish; } - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "GetDynamicUsers", - &error, - &reply, - NULL); - if (r < 0) - goto fail; - - r = sd_bus_message_enter_container(reply, 'a', "(us)"); - if (r < 0) - goto fail; - - while ((r = sd_bus_message_read(reply, "(us)", &id, &name)) > 0) { - r = user_entry_add(p, name, id); - if (r == -ENOMEM) - goto fail; - if (r < 0) - continue; + r = nss_pack_user_record(ur, result, buffer, buflen); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_TRYAGAIN; + goto finish; } - if (r < 0) - goto fail; - r = sd_bus_message_exit_container(reply); - if (r < 0) - goto fail; + ret = NSS_STATUS_SUCCESS; finish: - p->position = p->entries; - assert_se(pthread_mutex_unlock(&p->mutex) == 0); - - return NSS_STATUS_SUCCESS; - -fail: - systemd_endent(p); - assert_se(pthread_mutex_unlock(&p->mutex) == 0); - - return NSS_STATUS_UNAVAIL; -} - -enum nss_status _nss_systemd_setpwent(int stayopen) { - return systemd_setent(&getpwent_data); + assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); + return ret; } -enum nss_status _nss_systemd_setgrent(int stayopen) { - return systemd_setent(&getgrent_data); -} +enum nss_status _nss_systemd_getgrent_r( + struct group *result, + char *buffer, size_t buflen, + int *errnop) { -enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + _cleanup_free_ char **members = NULL; enum nss_status ret; - UserEntry *p; - size_t len; + int r; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); assert(result); - assert(buffer); assert(errnop); - assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0); - LIST_FOREACH(entries, p, getpwent_data.position) { - len = strlen(p->name) + 1; - if (buflen < len) { + if (!getgrent_data.iterator) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + if (!getgrent_data.by_membership) { + r = groupdb_iterator_get(getgrent_data.iterator, &gr); + if (r == -ESRCH) { + /* So we finished iterating native groups now. let's now continue with iterating + * native memberships, and generate additional group entries for any groups + * referenced there that are defined in NSS only. This means for those groups there + * will be two or more entries generated during iteration, but this is apparently how + * this is supposed to work, and what other implementations do too. Clients are + * supposed to merge the group records found during iteration automatically. */ + getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator); + + r = membershipdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + getgrent_data.by_membership = true; + } else if (r < 0) { UNPROTECT_ERRNO; - *errnop = ERANGE; - ret = NSS_STATUS_TRYAGAIN; - goto finalize; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } else if (!STR_IN_SET(gr->group_name, root_group.gr_name, nobody_group.gr_name)) { + r = membershipdb_by_group_strv(gr->group_name, nss_glue_userdb_flags(), &members); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } } + } - memcpy(buffer, p->name, len); - - result->pw_name = buffer; - result->pw_uid = p->id; - result->pw_gid = p->id; - result->pw_gecos = (char*) DYNAMIC_USER_GECOS; - result->pw_passwd = (char*) DYNAMIC_USER_PASSWD; - result->pw_dir = (char*) DYNAMIC_USER_DIR; - result->pw_shell = (char*) DYNAMIC_USER_SHELL; - break; + if (getgrent_data.by_membership) { + _cleanup_close_ int lock_fd = -1; + + for (;;) { + _cleanup_free_ char *user_name = NULL, *group_name = NULL; + + r = membershipdb_iterator_get(getgrent_data.iterator, &user_name, &group_name); + if (r == -ESRCH) { + ret = NSS_STATUS_NOTFOUND; + goto finish; + } + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name)) + continue; + if (STR_IN_SET(group_name, root_group.gr_name, nobody_group.gr_name)) + continue; + + /* We are about to recursively call into NSS, let's make sure we disable recursion into our own code. */ + if (lock_fd < 0) { + lock_fd = userdb_nss_compat_disable(); + if (lock_fd < 0 && lock_fd != -EBUSY) { + UNPROTECT_ERRNO; + *errnop = -lock_fd; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + } + + r = nss_group_record_by_name(group_name, &gr); + if (r == -ESRCH) + continue; + if (r < 0) { + log_debug_errno(r, "Failed to do NSS check for group '%s', ignoring: %m", group_name); + continue; + } + + members = strv_new(user_name); + if (!members) { + UNPROTECT_ERRNO; + *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + /* Note that we currently generate one group entry per user that is part of a + * group. It's a bit ugly, but equivalent to generating a single entry with a set of + * members in them. */ + break; + } } - if (!p) { - ret = NSS_STATUS_NOTFOUND; - goto finalize; + + r = nss_pack_group_record(gr, members, result, buffer, buflen); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_TRYAGAIN; + goto finish; } - /* On success, step to the next entry. */ - p = p->entries_next; ret = NSS_STATUS_SUCCESS; -finalize: - /* Save position for the next call. */ - getpwent_data.position = p; - - assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); - +finish: + assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0); return ret; } -enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) { - enum nss_status ret; - UserEntry *p; - size_t len; +enum nss_status _nss_systemd_initgroups_dyn( + const char *user_name, + gid_t gid, + long *start, + long *size, + gid_t **groupsp, + long int limit, + int *errnop) { + + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + bool any = false; + int r; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - assert(result); - assert(buffer); + assert(user_name); + assert(start); + assert(size); + assert(groupsp); assert(errnop); - assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0); + if (!valid_user_group_name(user_name)) + return NSS_STATUS_NOTFOUND; + + /* Don't allow extending these two special users, the same as we won't resolve them via getpwnam() */ + if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name)) + return NSS_STATUS_NOTFOUND; + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + r = membershipdb_by_user(user_name, nss_glue_userdb_flags(), &iterator); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } - LIST_FOREACH(entries, p, getgrent_data.position) { - len = sizeof(char*) + strlen(p->name) + 1; - if (buflen < len) { + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + _cleanup_free_ char *group_name = NULL; + + r = membershipdb_iterator_get(iterator, NULL, &group_name); + if (r == -ESRCH) + break; + if (r < 0) { UNPROTECT_ERRNO; - *errnop = ERANGE; - ret = NSS_STATUS_TRYAGAIN; - goto finalize; + *errnop = -r; + return NSS_STATUS_UNAVAIL; } - memzero(buffer, sizeof(char*)); - strcpy(buffer + sizeof(char*), p->name); + /* The group might be defined via traditional NSS only, hence let's do a full look-up without + * disabling NSS. This means we are operating recursively here. */ - result->gr_name = buffer + sizeof(char*); - result->gr_gid = p->id; - result->gr_passwd = (char*) DYNAMIC_USER_PASSWD; - result->gr_mem = (char**) buffer; - break; - } - if (!p) { - ret = NSS_STATUS_NOTFOUND; - goto finalize; - } + r = groupdb_by_name(group_name, nss_glue_userdb_flags() & ~USERDB_AVOID_NSS, &g); + if (r == -ESRCH) + continue; + if (r < 0) { + log_debug_errno(r, "Failed to resolve group '%s', ignoring: %m", group_name); + continue; + } - /* On success, step to the next entry. */ - p = p->entries_next; - ret = NSS_STATUS_SUCCESS; + if (g->gid == gid) + continue; -finalize: - /* Save position for the next call. */ - getgrent_data.position = p; + if (*start >= *size) { + gid_t *new_groups; + long new_size; + + if (limit > 0 && *size >= limit) /* Reached the limit.? */ + break; + + if (*size > LONG_MAX/2) { /* Check for overflow */ + UNPROTECT_ERRNO; + *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + new_size = *start * 2; + if (limit > 0 && new_size > limit) + new_size = limit; + + /* Enlarge buffer */ + new_groups = realloc(*groupsp, new_size * sizeof(**groupsp)); + if (!new_groups) { + UNPROTECT_ERRNO; + *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + *groupsp = new_groups; + *size = new_size; + } - assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0); + (*groupsp)[(*start)++] = g->gid; + any = true; + } - return ret; + return any ? NSS_STATUS_SUCCESS : NSS_STATUS_NOTFOUND; } diff --git a/src/nss-systemd/nss-systemd.sym b/src/nss-systemd/nss-systemd.sym index ff63382b152c9..77e1fbe93f227 100644 --- a/src/nss-systemd/nss-systemd.sym +++ b/src/nss-systemd/nss-systemd.sym @@ -19,5 +19,6 @@ global: _nss_systemd_endgrent; _nss_systemd_setgrent; _nss_systemd_getgrent_r; + _nss_systemd_initgroups_dyn; local: *; }; diff --git a/src/nss-systemd/userdb-glue.c b/src/nss-systemd/userdb-glue.c new file mode 100644 index 0000000000000..810ac56286d18 --- /dev/null +++ b/src/nss-systemd/userdb-glue.c @@ -0,0 +1,352 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "env-util.h" +#include "fd-util.h" +#include "group-record-nss.h" +#include "strv.h" +#include "user-record.h" +#include "userdb-glue.h" +#include "userdb.h" + +UserDBFlags nss_glue_userdb_flags(void) { + UserDBFlags flags = USERDB_AVOID_NSS; + + /* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */ + if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) + flags |= USERDB_AVOID_DYNAMIC_USER; + + return flags; +} + +int nss_pack_user_record( + UserRecord *hr, + struct passwd *pwd, + char *buffer, + size_t buflen) { + + const char *rn, *hd, *shell; + size_t required; + + assert(hr); + assert(pwd); + + assert_se(hr->user_name); + required = strlen(hr->user_name) + 1; + + assert_se(rn = user_record_real_name(hr)); + required += strlen(rn) + 1; + + assert_se(hd = user_record_home_directory(hr)); + required += strlen(hd) + 1; + + assert_se(shell = user_record_shell(hr)); + required += strlen(shell) + 1; + + if (buflen < required) + return -ERANGE; + + *pwd = (struct passwd) { + .pw_name = buffer, + .pw_uid = hr->uid, + .pw_gid = user_record_gid(hr), + .pw_passwd = (char*) "x", /* means: see shadow file */ + }; + + assert(buffer); + + pwd->pw_gecos = stpcpy(pwd->pw_name, hr->user_name) + 1; + pwd->pw_dir = stpcpy(pwd->pw_gecos, rn) + 1; + pwd->pw_shell = stpcpy(pwd->pw_dir, hd) + 1; + strcpy(pwd->pw_shell, shell); + + return 0; +} + +enum nss_status userdb_getpwnam( + const char *name, + struct passwd *pwd, + char *buffer, size_t buflen, + int *errnop) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(pwd); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + r = userdb_by_name(name, nss_glue_userdb_flags(), &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + r = nss_pack_user_record(hr, pwd, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} + +enum nss_status userdb_getpwuid( + uid_t uid, + struct passwd *pwd, + char *buffer, + size_t buflen, + int *errnop) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(pwd); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + r = userdb_by_uid(uid, nss_glue_userdb_flags(), &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + r = nss_pack_user_record(hr, pwd, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} + +int nss_pack_group_record( + GroupRecord *g, + char **extra_members, + struct group *gr, + char *buffer, + size_t buflen) { + + char **array = NULL, *p, **m; + size_t required, n = 0, i = 0; + + assert(g); + assert(gr); + + assert_se(g->group_name); + required = strlen(g->group_name) + 1; + + STRV_FOREACH(m, g->members) { + required += sizeof(char*); /* space for ptr array entry */ + required += strlen(*m) + 1; + n++; + } + STRV_FOREACH(m, extra_members) { + if (strv_contains(g->members, *m)) + continue; + + required += sizeof(char*); + required += strlen(*m) + 1; + n++; + } + + required += sizeof(char*); /* trailing NULL in ptr array entry */ + + if (buflen < required) + return -ERANGE; + + array = (char**) buffer; /* place ptr array at beginning of buffer, under assumption buffer is aligned */ + p = buffer + sizeof(void*) * (n + 1); /* place member strings right after the ptr array */ + + STRV_FOREACH(m, g->members) { + array[i++] = p; + p = stpcpy(p, *m) + 1; + } + STRV_FOREACH(m, extra_members) { + if (strv_contains(g->members, *m)) + continue; + + array[i++] = p; + p = stpcpy(p, *m) + 1; + } + + assert_se(i == n); + array[n] = NULL; + + *gr = (struct group) { + .gr_name = strcpy(p, g->group_name), + .gr_gid = g->gid, + .gr_passwd = (char*) "x", /* means: see shadow file */ + .gr_mem = array, + }; + + return 0; +} + +enum nss_status userdb_getgrnam( + const char *name, + struct group *gr, + char *buffer, + size_t buflen, + int *errnop) { + + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + _cleanup_strv_free_ char **members = NULL; + int r; + + assert(gr); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + r = groupdb_by_name(name, nss_glue_userdb_flags(), &g); + if (r < 0 && r != -ESRCH) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + r = membershipdb_by_group_strv(name, nss_glue_userdb_flags(), &members); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + if (!g) { + _cleanup_close_ int lock_fd = -1; + + if (strv_isempty(members)) + return NSS_STATUS_NOTFOUND; + + /* Grmbl, so we are supposed to extend a group entry, but the group entry itself is not + * accessible via non-NSS. Hence let's do what we have to do, and query NSS after all to + * acquire it, so that we can extend it (that's because glibc's group merging feature will + * merge groups only if both GID and name match and thus we need to have both first). It + * sucks behaving recursively likely this, but it's apparently what everybody does. We break + * the recursion for ourselves via the userdb_nss_compat_disable() lock. */ + + lock_fd = userdb_nss_compat_disable(); + if (lock_fd < 0 && lock_fd != -EBUSY) + return lock_fd; + + r = nss_group_record_by_name(name, &g); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + } + + r = nss_pack_group_record(g, members, gr, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} + +enum nss_status userdb_getgrgid( + gid_t gid, + struct group *gr, + char *buffer, + size_t buflen, + int *errnop) { + + + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + _cleanup_strv_free_ char **members = NULL; + bool from_nss; + int r; + + assert(gr); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + r = groupdb_by_gid(gid, nss_glue_userdb_flags(), &g); + if (r < 0 && r != -ESRCH) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + if (!g) { + _cleanup_close_ int lock_fd = -1; + + /* So, quite possibly we have to extend an existing group record with additional members. But + * to do this we need to know the group name first. The group didn't exist via non-NSS + * queries though, hence let's try to acquire it here recursively via NSS. */ + + lock_fd = userdb_nss_compat_disable(); + if (lock_fd < 0 && lock_fd != -EBUSY) + return lock_fd; + + r = nss_group_record_by_gid(gid, &g); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + from_nss = true; + } else + from_nss = false; + + r = membershipdb_by_group_strv(g->group_name, nss_glue_userdb_flags(), &members); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + /* If we acquired the record via NSS then there's no reason to respond unless we have to agument the + * list of members of the group */ + if (from_nss && strv_isempty(members)) + return NSS_STATUS_NOTFOUND; + + r = nss_pack_group_record(g, members, gr, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} diff --git a/src/nss-systemd/userdb-glue.h b/src/nss-systemd/userdb-glue.h new file mode 100644 index 0000000000000..02add24b6b814 --- /dev/null +++ b/src/nss-systemd/userdb-glue.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include +#include +#include + +#include "userdb.h" + +UserDBFlags nss_glue_userdb_flags(void); + +int nss_pack_user_record(UserRecord *hr, struct passwd *pwd, char *buffer, size_t buflen); +int nss_pack_group_record(GroupRecord *g, char **extra_members, struct group *gr, char *buffer, size_t buflen); + +enum nss_status userdb_getpwnam(const char *name, struct passwd *pwd, char *buffer, size_t buflen, int *errnop); +enum nss_status userdb_getpwuid(uid_t uid, struct passwd *pwd, char *buffer, size_t buflen, int *errnop); + +enum nss_status userdb_getgrnam(const char *name, struct group *gr, char *buffer, size_t buflen, int *errnop); +enum nss_status userdb_getgrgid(gid_t gid, struct group *gr, char *buffer, size_t buflen, int *errnop); From ab11443e210e02f69b8436a6cd8758ad4cf31e37 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:35:39 +0200 Subject: [PATCH 073/109] homed: add new systemd-homed service that can manage LUKS homes --- meson.build | 39 +- meson_options.txt | 2 + src/home/home-util.c | 157 + src/home/home-util.h | 23 + src/home/homed-bus.c | 64 + src/home/homed-bus.h | 10 + src/home/homed-home-bus.c | 877 ++++ src/home/homed-home-bus.h | 36 + src/home/homed-home.c | 2684 +++++++++++ src/home/homed-home.h | 168 + src/home/homed-manager-bus.c | 690 +++ src/home/homed-manager-bus.h | 6 + src/home/homed-manager.c | 1676 +++++++ src/home/homed-manager.h | 67 + src/home/homed-operation.c | 76 + src/home/homed-operation.h | 62 + src/home/homed-varlink.c | 368 ++ src/home/homed-varlink.h | 8 + src/home/homed.c | 49 + src/home/homework.c | 5017 +++++++++++++++++++++ src/home/meson.build | 44 + src/home/org.freedesktop.home1.conf | 193 + src/home/org.freedesktop.home1.policy | 72 + src/home/org.freedesktop.home1.service | 7 + src/home/pwquality-util.c | 133 + src/home/pwquality-util.h | 7 + src/home/user-record-sign.c | 174 + src/home/user-record-sign.h | 19 + src/home/user-record-util.c | 1007 +++++ src/home/user-record-util.h | 53 + src/libsystemd/sd-bus/bus-common-errors.c | 23 + src/libsystemd/sd-bus/bus-common-errors.h | 24 + src/shared/gpt.h | 1 + units/meson.build | 2 + units/systemd-homed.service.in | 29 + 35 files changed, 13866 insertions(+), 1 deletion(-) create mode 100644 src/home/home-util.c create mode 100644 src/home/home-util.h create mode 100644 src/home/homed-bus.c create mode 100644 src/home/homed-bus.h create mode 100644 src/home/homed-home-bus.c create mode 100644 src/home/homed-home-bus.h create mode 100644 src/home/homed-home.c create mode 100644 src/home/homed-home.h create mode 100644 src/home/homed-manager-bus.c create mode 100644 src/home/homed-manager-bus.h create mode 100644 src/home/homed-manager.c create mode 100644 src/home/homed-manager.h create mode 100644 src/home/homed-operation.c create mode 100644 src/home/homed-operation.h create mode 100644 src/home/homed-varlink.c create mode 100644 src/home/homed-varlink.h create mode 100644 src/home/homed.c create mode 100644 src/home/homework.c create mode 100644 src/home/meson.build create mode 100644 src/home/org.freedesktop.home1.conf create mode 100644 src/home/org.freedesktop.home1.policy create mode 100644 src/home/org.freedesktop.home1.service create mode 100644 src/home/pwquality-util.c create mode 100644 src/home/pwquality-util.h create mode 100644 src/home/user-record-sign.c create mode 100644 src/home/user-record-sign.h create mode 100644 src/home/user-record-util.c create mode 100644 src/home/user-record-util.h create mode 100644 units/systemd-homed.service.in diff --git a/meson.build b/meson.build index d082266910bd1..c07f9cf88bf78 100644 --- a/meson.build +++ b/meson.build @@ -242,6 +242,7 @@ conf.set_quoted('SYSTEMD_EXPORT_PATH', join_paths(rootlib conf.set_quoted('VENDOR_KEYRING_PATH', join_paths(rootlibexecdir, 'import-pubring.gpg')) conf.set_quoted('USER_KEYRING_PATH', join_paths(pkgsysconfdir, 'import-pubring.gpg')) conf.set_quoted('DOCUMENT_ROOT', join_paths(pkgdatadir, 'gatewayd')) +conf.set_quoted('SYSTEMD_HOMEWORK_PATH', join_paths(rootlibexecdir, 'systemd-homework')) conf.set_quoted('SYSTEMD_USERWORK_PATH', join_paths(rootlibexecdir, 'systemd-userwork')) conf.set10('MEMORY_ACCOUNTING_DEFAULT', memory_accounting_default) conf.set_quoted('MEMORY_ACCOUNTING_DEFAULT_YES_NO', memory_accounting_default ? 'yes' : 'no') @@ -837,6 +838,12 @@ endif libmount = dependency('mount', version : fuzzer_build ? '>= 0' : '>= 2.30') +want_libfdisk = get_option('homed') +libfdisk = dependency('fdisk', required : want_libfdisk) + +libpwquality = dependency('pwquality') +conf.set10('HAVE_LIBPWQUALITY', libpwquality.found()) + want_seccomp = get_option('seccomp') if want_seccomp != 'false' and not skip_deps libseccomp = dependency('libseccomp', @@ -961,7 +968,7 @@ conf.set10('HAVE_MICROHTTPD', have) want_libcryptsetup = get_option('libcryptsetup') if want_libcryptsetup != 'false' and not skip_deps libcryptsetup = dependency('libcryptsetup', - version : '>= 1.6.0', + version : '>= 2.2.0', required : want_libcryptsetup == 'true') have = libcryptsetup.found() have_sector = cc.has_member( @@ -1281,6 +1288,7 @@ foreach term : ['utmp', 'machined', 'portabled', 'userdb', + 'homed', 'networkd', 'timedated', 'timesyncd', @@ -1495,6 +1503,7 @@ subdir('src/locale') subdir('src/machine') subdir('src/portable') subdir('src/userdb') +subdir('src/home') subdir('src/nspawn') subdir('src/resolve') subdir('src/timedate') @@ -1965,6 +1974,34 @@ if conf.get('ENABLE_USERDB') == 1 install_dir : rootbindir) endif +if conf.get('ENABLE_HOMED') == 1 + executable('systemd-homework', + systemd_homework_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcryptsetup, + libblkid, + libcrypt, + libopenssl, + libfdisk], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) + + executable('systemd-homed', + systemd_homed_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcrypt, + libopenssl, + libpwquality], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) +endif + foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] meson.add_install_script(meson_make_symlink, join_paths(rootbindir, 'systemctl'), diff --git a/meson_options.txt b/meson_options.txt index 91a9c14ee64b9..0b1403b44813b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -92,6 +92,8 @@ option('portabled', type : 'boolean', description : 'install the systemd-portabled stack') option('userdb', type : 'boolean', description : 'install the systemd-userdbd stack') +option('homed', type : 'boolean', + description : 'install the systemd-homed stack') option('networkd', type : 'boolean', description : 'install the systemd-networkd stack') option('timedated', type : 'boolean', diff --git a/src/home/home-util.c b/src/home/home-util.c new file mode 100644 index 0000000000000..37f614425bc05 --- /dev/null +++ b/src/home/home-util.c @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_CRYPT_H +#include +#endif + +#include "dns-domain.h" +#include "errno-util.h" +#include "home-util.h" +#include "memory-util.h" +#include "path-util.h" +#include "string-util.h" +#include "strv.h" +#include "user-util.h" + +bool suitable_user_name(const char *name) { + + /* Checks whether the specified name is suitable for management via homed. Note that our client side + * usually validate susing a simple valid_user_group_name(), while server side we are a bit more + * restrictive, so that we can change the rules server side without having to update things client + * side, too. */ + + if (!valid_user_group_name(name)) + return false; + + /* We generally rely on NSS to tell us which users not to care for, but let's filter out some + * particularly well-known users. */ + if (STR_IN_SET(name, + "root", + "nobody", + NOBODY_USER_NAME, NOBODY_GROUP_NAME)) + return false; + + /* Let's also defend our own namespace, as well as Debian's (unwritten?) logic of prefixing system + * users with underscores. */ + if (STARTSWITH_SET(name, "systemd-", "_")) + return false; + + return true; +} + +int suitable_realm(const char *realm) { + _cleanup_free_ char *normalized = NULL; + int r; + + /* Similar to the above: let's validate the realm a bit stricter server-side than client side */ + + r = dns_name_normalize(realm, 0, &normalized); /* this also checks general validity */ + if (r == -EINVAL) + return 0; + if (r < 0) + return r; + + if (!streq(realm, normalized)) /* is this normalized? */ + return false; + + if (dns_name_is_root(realm) || dns_name_is_single_label(realm)) /* Don't allow top level domain nor single label domains */ + return false; + + return true; +} + +int suitable_image_path(const char *path) { + + return !empty_or_root(path) && + path_is_valid(path) && + path_is_absolute(path); +} + +int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm) { + _cleanup_free_ char *un = NULL, *rr = NULL; + const char *c; + int r; + + assert(t); + assert(ret_user_name); + assert(ret_realm); + + c = strchr(t, '@'); + if (!c) { + if (!suitable_user_name(t)) + return -EINVAL; + + un = strdup(t); + if (!un) + return -ENOMEM; + } else { + un = strndup(t, c - t); + if (!un) + return -ENOMEM; + + if (!suitable_user_name(un)) + return -EINVAL; + + r = suitable_realm(c + 1); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + rr = strdup(c + 1); + if (!rr) + return -ENOMEM; + } + + *ret_user_name = TAKE_PTR(un); + *ret_realm = TAKE_PTR(rr); + + return 0; +} + +int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) { + _cleanup_(erase_and_freep) char *formatted = NULL; + JsonVariant *v; + int r; + + assert(m); + assert(secret); + + if (!FLAGS_SET(secret->mask, USER_RECORD_SECRET)) + return -EINVAL; + + v = json_variant_by_key(secret->json, "secret"); + if (!v) + return -EINVAL; + + r = json_variant_format(v, 0, &formatted); + if (r < 0) + return r; + + return sd_bus_message_append(m, "s", formatted); +} + +int test_password(char **hashed_password, const char *password) { + char **hpw; + + STRV_FOREACH(hpw, hashed_password) { + struct crypt_data cc = {}; + const char *k; + bool b; + + errno = 0; + k = crypt_r(password, *hpw, &cc); + if (!k) { + explicit_bzero_safe(&cc, sizeof(cc)); + return errno_or_else(EINVAL); + } + + b = streq(k, *hpw); + explicit_bzero_safe(&cc, sizeof(cc)); + + if (b) + return true; + } + + return false; +} diff --git a/src/home/home-util.h b/src/home/home-util.h new file mode 100644 index 0000000000000..fea46d5e2e6a6 --- /dev/null +++ b/src/home/home-util.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" + +#include "time-util.h" +#include "user-record.h" + +bool suitable_user_name(const char *name); +int suitable_realm(const char *realm); +int suitable_image_path(const char *path); + +int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm); + +int bus_message_append_secret(sd_bus_message *m, UserRecord *secret); + +/* Many of our operations might be slow due to crypto, fsck, recursive chown() and so on. For these + * operations permit a *very* long time-out */ +#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE) + +int test_password(char **hashed_password, const char *password); diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c new file mode 100644 index 0000000000000..0193089668d71 --- /dev/null +++ b/src/home/homed-bus.c @@ -0,0 +1,64 @@ +#include "homed-bus.h" +#include "strv.h" + +int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *full = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + unsigned line = 0, column = 0; + const char *json; + int r; + + assert(ret); + + r = sd_bus_message_read(m, "s", &json); + if (r < 0) + return r; + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON secret record at %u:%u: %m", line, column); + + r = json_build(&full, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("secret", JSON_BUILD_VARIANT(v)))); + if (r < 0) + return r; + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = user_record_load(hr, full, USER_RECORD_REQUIRE_SECRET); + if (r < 0) + return r; + + *ret = TAKE_PTR(hr); + return 0; +} + +int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + unsigned line = 0, column = 0; + const char *json; + int r; + + assert(ret); + + r = sd_bus_message_read(m, "s", &json); + if (r < 0) + return r; + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON identity record at %u:%u: %m", line, column); + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = user_record_load(hr, v, flags); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "JSON data is not a valid identity record"); + + *ret = TAKE_PTR(hr); + return 0; +} diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h new file mode 100644 index 0000000000000..20f13b43ade38 --- /dev/null +++ b/src/home/homed-bus.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +#include "user-record.h" +#include "json.h" + +int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error); +int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error); diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c new file mode 100644 index 0000000000000..0f9229aafbd05 --- /dev/null +++ b/src/home/homed-home-bus.c @@ -0,0 +1,877 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "bus-common-errors.h" +#include "bus-util.h" +#include "fd-util.h" +#include "homed-bus.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-util.h" + +static int property_get_unix_record( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + + assert(bus); + assert(reply); + assert(h); + + return sd_bus_message_append( + reply, "(suusss)", + h->user_name, + (uint32_t) h->uid, + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL); +} + +static int property_get_state( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + + assert(bus); + assert(reply); + assert(h); + + return sd_bus_message_append(reply, "s", home_state_to_string(home_get_state(h))); +} + +int bus_home_client_is_trusted(Home *h, sd_bus_message *message) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + uid_t euid; + int r; + + assert(h); + + if (!message) + return -EINVAL; + + r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds); + if (r < 0) + return r; + + r = sd_bus_creds_get_euid(creds, &euid); + if (r < 0) + return r; + + return euid == 0 || h->uid == euid; +} + +int bus_home_get_record_json( + Home *h, + sd_bus_message *message, + char **ret, + bool *ret_incomplete) { + + _cleanup_(user_record_unrefp) UserRecord *augmented = NULL; + UserRecordLoadFlags flags; + int r, trusted; + + assert(h); + assert(ret); + + trusted = bus_home_client_is_trusted(h, message); + if (trusted < 0) { + log_warning_errno(trusted, "Failed to determine whether client is trusted, assuming untrusted."); + trusted = false; + } + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = home_augment_status(h, flags, &augmented); + if (r < 0) + return r; + + r = json_variant_format(augmented->json, 0, ret); + if (r < 0) + return r; + + if (ret_incomplete) + *ret_incomplete = augmented->incomplete; + + return 0; +} + +static int property_get_user_record( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL; + Home *h = userdata; + bool incomplete; + int r; + + assert(bus); + assert(reply); + assert(h); + + r = bus_home_get_record_json(h, sd_bus_get_current_message(bus), &json, &incomplete); + if (r < 0) + return r; + + return sd_bus_message_append(reply, "(sb)", json, incomplete); +} + +int bus_home_method_activate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_activate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_deactivate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = home_deactivate(h, false, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_unregister( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.remove-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_unregister(h, error); + if (r < 0) + return r; + + assert(r > 0); + + /* Note that home_unregister() destroyed 'h' here, so no more accesses */ + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_home_method_realize( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_create(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + h->unregister_on_failure = false; + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_remove( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.remove-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_remove(h, error); + if (r < 0) + return r; + if (r > 0) /* Done already. Note that home_remove() destroyed 'h' here, so no more accesses */ + return sd_bus_reply_method_return(message, NULL); + + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_fixate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_fixate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_authenticate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.authenticate-home", + NULL, + true, + h->uid, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_authenticate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) { + int r; + + assert(h); + assert(message); + assert(hr); + + r = user_record_is_supported(hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.update-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_update(h, hr, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_update( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + return bus_home_method_update_record(h, message, hr, error); +} + +int bus_home_method_resize( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + uint64_t sz; + int r; + + assert(message); + assert(h); + + r = sd_bus_message_read(message, "t", &sz); + if (r < 0) + return r; + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.resize-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_resize(h, sz, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_change_password( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *new_secret = NULL, *old_secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &new_secret, error); + if (r < 0) + return r; + + r = bus_message_read_secret(message, &old_secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.passwd-home", + NULL, + true, + h->uid, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_passwd(h, new_secret, old_secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_lock( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = home_lock(h, error); + if (r < 0) + return r; + if (r > 0) /* Done */ + return sd_bus_reply_method_return(message, NULL); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_unlock( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_unlock(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_acquire( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + _cleanup_(operation_unrefp) Operation *o = NULL; + _cleanup_close_ int fd = -1; + int r, please_suspend; + Home *h = userdata; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "b", &please_suspend); + if (r < 0) + return r; + + /* This operation might not be something we can executed immediately, hence queue it */ + fd = home_create_fifo(h, please_suspend); + if (fd < 0) + return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name); + + o = operation_new(OPERATION_ACQUIRE, message); + if (!o) + return -ENOMEM; + + o->secret = TAKE_PTR(secret); + o->send_fd = TAKE_FD(fd); + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_ref( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_close_ int fd = -1; + Home *h = userdata; + HomeState state; + int please_suspend, r; + + assert(message); + assert(h); + + r = sd_bus_message_read(message, "b", &please_suspend); + if (r < 0) + return r; + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + default: + if (HOME_STATE_IS_ACTIVE(state)) + break; + + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + fd = home_create_fifo(h, please_suspend); + if (fd < 0) + return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name); + + return sd_bus_reply_method_return(message, "h", fd); +} + +int bus_home_method_release( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(operation_unrefp) Operation *o = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + o = operation_new(OPERATION_RELEASE, message); + if (!o) + return -ENOMEM; + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + return 1; +} + +/* We map a uid_t as uint32_t bus property, let's ensure this is safe. */ +assert_cc(sizeof(uid_t) == sizeof(uint32_t)); + +const sd_bus_vtable home_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("UserName", "s", NULL, offsetof(Home, user_name), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("UID", "u", NULL, offsetof(Home, uid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("UnixRecord", "(suusss)", property_get_unix_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("State", "s", property_get_state, 0, 0), + SD_BUS_PROPERTY("UserRecord", "(sb)", property_get_user_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Activate", "s", NULL, bus_home_method_activate, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Deactivate", NULL, NULL, bus_home_method_deactivate, 0), + SD_BUS_METHOD("Unregister", NULL, NULL, bus_home_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Realize", "s", NULL, bus_home_method_realize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Remove", NULL, NULL, bus_home_method_remove, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Fixate", "s", NULL, bus_home_method_fixate, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Authenticate", "s", NULL, bus_home_method_authenticate, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Update", "s", NULL, bus_home_method_update, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Resize", "ts", NULL, bus_home_method_resize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ChangePassword", "ss", NULL, bus_home_method_change_password, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Lock", NULL, NULL, bus_home_method_lock, 0), + SD_BUS_METHOD("Unlock", "s", NULL, bus_home_method_unlock, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Acquire", "sb", "h", bus_home_method_acquire, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Ref", "b", "h", bus_home_method_ref, 0), + SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0), + SD_BUS_VTABLE_END +}; + +int bus_home_path(Home *h, char **ret) { + assert(ret); + + return sd_bus_path_encode("/org/freedesktop/home1/home", h->user_name, ret); +} + +int bus_home_object_find( + sd_bus *bus, + const char *path, + const char *interface, + void *userdata, + void **found, + sd_bus_error *error) { + + _cleanup_free_ char *e = NULL; + Manager *m = userdata; + uid_t uid; + Home *h; + int r; + + r = sd_bus_path_decode(path, "/org/freedesktop/home1/home", &e); + if (r <= 0) + return 0; + + if (parse_uid(e, &uid) >= 0) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + else + h = hashmap_get(m->homes_by_name, e); + if (!h) + return 0; + + *found = h; + return 1; +} + +int bus_home_node_enumerator( + sd_bus *bus, + const char *path, + void *userdata, + char ***nodes, + sd_bus_error *error) { + + _cleanup_strv_free_ char **l = NULL; + Manager *m = userdata; + size_t k = 0; + Iterator i; + Home *h; + int r; + + assert(nodes); + + l = new0(char*, hashmap_size(m->homes_by_uid) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(h, m->homes_by_uid, i) { + r = bus_home_path(h, l + k); + if (r < 0) + return r; + } + + *nodes = TAKE_PTR(l); + return 1; +} + +static int on_deferred_change(sd_event_source *s, void *userdata) { + _cleanup_free_ char *path = NULL; + Home *h = userdata; + int r; + + assert(h); + + h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source); + + r = bus_home_path(h, &path); + if (r < 0) { + log_warning_errno(r, "Failed to generate home bus path, ignoring: %m"); + return 0; + } + + if (h->announced) + r = sd_bus_emit_properties_changed_strv(h->manager->bus, path, "org.freedesktop.home1.Home", NULL); + else + r = sd_bus_emit_object_added(h->manager->bus, path); + if (r < 0) + log_warning_errno(r, "Failed to send home change event, ignoring: %m"); + else + h->announced = true; + + return 0; +} + +int bus_home_emit_change(Home *h) { + int r; + + assert(h); + + if (h->deferred_change_event_source) + return 1; + + if (!h->manager->event) + return 0; + + if (IN_SET(sd_event_get_state(h->manager->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + r = sd_event_add_defer(h->manager->event, &h->deferred_change_event_source, on_deferred_change, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate deferred change event source: %m"); + + r = sd_event_source_set_priority(h->deferred_change_event_source, SD_EVENT_PRIORITY_IDLE+5); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(h->deferred_change_event_source, "deferred-change-event"); + return 1; +} + +int bus_home_emit_remove(Home *h) { + _cleanup_free_ char *path = NULL; + int r; + + assert(h); + + if (!h->announced) + return 0; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + r = sd_bus_emit_object_removed(h->manager->bus, path); + if (r < 0) + return r; + + h->announced = false; + return 1; +} diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h new file mode 100644 index 0000000000000..20516b1205350 --- /dev/null +++ b/src/home/homed-home-bus.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +#include "homed-home.h" + +int bus_home_client_is_trusted(Home *h, sd_bus_message *message); +int bus_home_get_record_json(Home *h, sd_bus_message *message, char **ret, bool *ret_incomplete); + +int bus_home_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_deactivate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_realize(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_fixate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_authenticate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_update(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, sd_bus_error *error); +int bus_home_method_resize(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_change_password(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_unlock(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_acquire(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_ref(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_release(sd_bus_message *message, void *userdata, sd_bus_error *error); + +extern const sd_bus_vtable home_vtable[]; + +int bus_home_path(Home *h, char **ret); + +int bus_home_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error); +int bus_home_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error); + +int bus_home_emit_change(Home *h); +int bus_home_emit_remove(Home *h); diff --git a/src/home/homed-home.c b/src/home/homed-home.c new file mode 100644 index 0000000000000..f553a40a4b873 --- /dev/null +++ b/src/home/homed-home.c @@ -0,0 +1,2684 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_LINUX_MEMFD_H +#include +#endif + +#include +#include + +#include "blockdev-util.h" +#include "btrfs-util.h" +#include "bus-common-errors.h" +#include "env-util.h" +#include "errno-list.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "home-util.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "missing.h" +#include "mkdir.h" +#include "path-util.h" +#include "process-util.h" +#include "pwquality-util.h" +#include "resize-fs.h" +#include "set.h" +#include "signal-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "strv.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" + +#define HOME_USERS_MAX 500 +#define PENDING_OPERATIONS_MAX 100 + +assert_cc(HOME_UID_MIN <= HOME_UID_MAX); +assert_cc(HOME_USERS_MAX <= (HOME_UID_MAX - HOME_UID_MIN + 1)); + +static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret); + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref); + +static int suitable_home_record(UserRecord *hr) { + int r; + + assert(hr); + + if (!hr->user_name) + return -EUNATCH; + + /* We are a bit more restrictive with what we accept as homed-managed user than what we accept in + * home records in general. Let's enforce the stricter rule here. */ + if (!suitable_user_name(hr->user_name)) + return -EINVAL; + if (!uid_is_valid(hr->uid)) + return -EINVAL; + + /* Insist we are outside of the dynamic and system range */ + if (uid_is_system(hr->uid) || gid_is_system(user_record_gid(hr)) || + uid_is_dynamic(hr->uid) || gid_is_dynamic(user_record_gid(hr))) + return -EADDRNOTAVAIL; + + /* Insist that GID and UID match */ + if (user_record_gid(hr) != (gid_t) hr->uid) + return -EBADSLT; + + /* Similar for the realm */ + if (hr->realm) { + r = suitable_realm(hr->realm); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + return 0; +} + +int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { + _cleanup_(home_freep) Home *home = NULL; + _cleanup_free_ char *nm = NULL, *ns = NULL; + int r; + + assert(m); + assert(hr); + + r = suitable_home_record(hr); + if (r < 0) + return r; + + if (hashmap_contains(m->homes_by_name, hr->user_name)) + return -EBUSY; + + if (hashmap_contains(m->homes_by_uid, UID_TO_PTR(hr->uid))) + return -EBUSY; + + if (sysfs && hashmap_contains(m->homes_by_sysfs, sysfs)) + return -EBUSY; + + if (hashmap_size(m->homes_by_name) >= HOME_USERS_MAX) + return -EUSERS; + + nm = strdup(hr->user_name); + if (!nm) + return -ENOMEM; + + if (sysfs) { + ns = strdup(sysfs); + if (!ns) + return -ENOMEM; + } + + home = new(Home, 1); + if (!home) + return -ENOMEM; + + *home = (Home) { + .manager = m, + .user_name = TAKE_PTR(nm), + .uid = hr->uid, + .state = _HOME_STATE_INVALID, + .worker_stdout_fd = -1, + .sysfs = TAKE_PTR(ns), + .signed_locally = -1, + }; + + r = hashmap_put(m->homes_by_name, home->user_name, home); + if (r < 0) + return r; + + r = hashmap_put(m->homes_by_uid, UID_TO_PTR(home->uid), home); + if (r < 0) + return r; + + if (home->sysfs) { + r = hashmap_put(m->homes_by_sysfs, home->sysfs, home); + if (r < 0) + return r; + } + + r = user_record_clone(hr, USER_RECORD_LOAD_MASK_SECRET, &home->record); + if (r < 0) + return r; + + (void) bus_manager_emit_auto_login_changed(m); + (void) bus_home_emit_change(home); + + if (ret) + *ret = TAKE_PTR(home); + else + TAKE_PTR(home); + + return 0; +} + +Home *home_free(Home *h) { + + if (!h) + return NULL; + + if (h->manager) { + (void) bus_home_emit_remove(h); + (void) bus_manager_emit_auto_login_changed(h->manager); + + if (h->user_name) + (void) hashmap_remove_value(h->manager->homes_by_name, h->user_name, h); + + if (uid_is_valid(h->uid)) + (void) hashmap_remove_value(h->manager->homes_by_uid, UID_TO_PTR(h->uid), h); + + if (h->sysfs) + (void) hashmap_remove_value(h->manager->homes_by_sysfs, h->sysfs, h); + + if (h->worker_pid > 0) + (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h); + + if (h->manager->gc_focus == h) + h->manager->gc_focus = NULL; + } + + user_record_unref(h->record); + user_record_unref(h->secret); + + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + safe_close(h->worker_stdout_fd); + free(h->user_name); + free(h->sysfs); + + h->ref_event_source_please_suspend = sd_event_source_unref(h->ref_event_source_please_suspend); + h->ref_event_source_dont_suspend = sd_event_source_unref(h->ref_event_source_dont_suspend); + + h->pending_operations = ordered_set_free(h->pending_operations); + h->pending_event_source = sd_event_source_unref(h->pending_event_source); + h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source); + + h->current_operation = operation_unref(h->current_operation); + + return mfree(h); +} + +int home_set_record(Home *h, UserRecord *hr) { + _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL; + Home *other; + int r; + + assert(h); + assert(h->user_name); + assert(h->record); + assert(hr); + + if (user_record_equal(h->record, hr)) + return 0; + + r = suitable_home_record(hr); + if (r < 0) + return r; + + if (!user_record_compatible(h->record, hr)) + return -EREMCHG; + + if (!FLAGS_SET(hr->mask, USER_RECORD_REGULAR) || + FLAGS_SET(hr->mask, USER_RECORD_SECRET)) + return -EINVAL; + + if (FLAGS_SET(h->record->mask, USER_RECORD_STATUS)) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + /* Hmm, the existing record has status fields? If so, copy them over */ + + v = json_variant_ref(hr->json); + r = json_variant_set_field(&v, "status", json_variant_by_key(h->record->json, "status")); + if (r < 0) + return r; + + new_hr = user_record_new(); + if (!new_hr) + return -ENOMEM; + + r = user_record_load(new_hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return r; + + hr = new_hr; + } + + other = hashmap_get(h->manager->homes_by_uid, UID_TO_PTR(hr->uid)); + if (other && other != h) + return -EBUSY; + + if (h->uid != hr->uid) { + r = hashmap_remove_and_replace(h->manager->homes_by_uid, UID_TO_PTR(h->uid), UID_TO_PTR(hr->uid), h); + if (r < 0) + return r; + } + + user_record_unref(h->record); + h->record = user_record_ref(hr); + h->uid = h->record->uid; + + /* The updated record might have a different autologin setting, trigger a PropertiesChanged event for it */ + (void) bus_manager_emit_auto_login_changed(h->manager); + (void) bus_home_emit_change(h); + + return 0; +} + +int home_save_record(Home *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ char *text = NULL; + const char *fn; + int r; + + assert(h); + + v = json_variant_ref(h->record->json); + r = json_variant_normalize(&v); + if (r < 0) + log_warning_errno(r, "User record could not be normalized."); + + r = json_variant_format(v, JSON_FORMAT_PRETTY|JSON_FORMAT_NEWLINE, &text); + if (r < 0) + return r; + + (void) mkdir("/var/lib/systemd/", 0755); + (void) mkdir("/var/lib/systemd/home/", 0700); + + fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity"); + + r = write_string_file(fn, text, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MODE_0600); + if (r < 0) + return r; + + return 0; +} + +int home_unlink_record(Home *h) { + const char *fn; + + assert(h); + + fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity"); + if (unlink(fn) < 0 && errno != ENOENT) + return -errno; + + fn = strjoina("/run/systemd/home/", h->user_name, ".ref"); + if (unlink(fn) < 0 && errno != ENOENT) + return -errno; + + return 0; +} + +static void home_set_state(Home *h, HomeState state) { + HomeState old_state, new_state; + + assert(h); + + old_state = home_get_state(h); + h->state = state; + new_state = home_get_state(h); /* Query the new state, since the 'state' variable might be set to -1, + * in which case we synthesize an high-level state on demand */ + + log_info("%s: changing state %s → %s", h->user_name, + home_state_to_string(old_state), + home_state_to_string(new_state)); + + if (HOME_STATE_IS_EXECUTING_OPERATION(old_state) && !HOME_STATE_IS_EXECUTING_OPERATION(new_state)) { + /* If we just finished executing some operation, process the queue of pending operations. And + * enqueue it for GC too. */ + + home_schedule_operation(h, NULL, NULL); + manager_enqueue_gc(h->manager, h); + } +} + +static int home_parse_worker_stdout(int _fd, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_close_ int fd = _fd; /* take possession, even on failure */ + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_fclose_ FILE *f = NULL; + unsigned line, column; + struct stat st; + int r; + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat stdout fd: %m"); + + assert(S_ISREG(st.st_mode)); + + if (st.st_size == 0) { /* empty record */ + *ret = NULL; + return 0; + } + + if (lseek(fd, SEEK_SET, 0) == (off_t) -1) + return log_error_errno(errno, "Failed to seek to beginning of memfd: %m"); + + f = fdopen(fd, "r"); + if (!f) + return log_error_errno(errno, "Failed to reopen memfd: %m"); + + TAKE_FD(fd); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *text = NULL; + + r = read_full_stream(f, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read from client: %m"); + + log_debug("Got from worker: %s", text); + rewind(f); + } + + r = json_parse_file(f, "stdout", JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return log_error_errno(r, "Failed to load home record identity: %m"); + + *ret = TAKE_PTR(hr); + return 1; +} + +static int home_verify_user_record(Home *h, UserRecord *hr, bool *ret_signed_locally, sd_bus_error *ret_error) { + int is_signed; + + assert(h); + assert(hr); + assert(ret_signed_locally); + + is_signed = manager_verify_user_record(h->manager, hr); + switch (is_signed) { + + case USER_RECORD_SIGNED_EXCLUSIVE: + log_info("Home %s is signed exclusively by our key, accepting.", hr->user_name); + *ret_signed_locally = true; + return 0; + + case USER_RECORD_SIGNED: + log_info("Home %s is signed by our key (and others), accepting.", hr->user_name); + *ret_signed_locally = false; + return 0; + + case USER_RECORD_FOREIGN: + log_info("Home %s is signed by foreign key we like, accepting.", hr->user_name); + *ret_signed_locally = false; + return 0; + + case USER_RECORD_UNSIGNED: + sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed at all, refusing.", hr->user_name); + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Home %s contains user record that is not signed at all, refusing.", hr->user_name); + + case -ENOKEY: + sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed by any known key, refusing.", hr->user_name); + return log_error_errno(is_signed, "Home %s contians user record that is not signed by any known key, refusing.", hr->user_name); + + default: + assert(is_signed < 0); + return log_error_errno(is_signed, "Failed to verify signature on user record for %s, refusing fixation: %m", hr->user_name); + } +} + +static int convert_worker_errno(Home *h, int e, sd_bus_error *error) { + /* Converts the error numbers the worker process returned into somewhat sensible dbus errors */ + + switch (e) { + + case -EMSGSIZE: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type cannot shrinked"); + case -ETXTBSY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type can only be shrinked offline"); + case -ERANGE: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File system size too small"); + case -ENOLINK: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected storage backend"); + case -EPROTONOSUPPORT: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected file system"); + case -ENOTTY: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on storage backend"); + case -ESOCKTNOSUPPORT: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on file system"); + case -ENOKEY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name); + case -EBUSY: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + case -ENOEXEC: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is currently not active", h->user_name); + case -ENOSPC: + return sd_bus_error_setf(error, BUS_ERROR_NO_DISK_SPACE, "Not enough disk space for home %s", h->user_name); + } + + return 0; +} + +static void home_count_bad_authentication(Home *h, bool save) { + int r; + + assert(h); + + r = user_record_bad_authentication(h->record); + if (r < 0) { + log_warning_errno(r, "Failed to increase bad authentication counter, ignoring: %m"); + return; + } + + if (save) { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } +} + +static void home_fixate_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + bool signed_locally; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); + + secret = TAKE_PTR(h->secret); /* Take possession */ + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, false); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Fixation failed: %m"); + goto fail; + } + if (!hr) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Did not receive user record from worker process, fixation failed."); + goto fail; + } + + r = home_verify_user_record(h, hr, &signed_locally, &error); + if (r < 0) + goto fail; + + r = home_set_record(h, hr); + if (r < 0) { + log_error_errno(r, "Failed to update home record: %m"); + goto fail; + } + + h->signed_locally = signed_locally; + + /* When we finished fixating (and don't follow-up with activation), let's count this as good authentication */ + if (h->state == HOME_FIXATING) { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + } + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + + if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) { + + r = home_start_work(h, "activate", h->record, secret); + if (r < 0) { + h->current_operation = operation_result_unref(h->current_operation, r, NULL); + home_set_state(h, _HOME_STATE_INVALID); + } else + home_set_state(h, h->state == HOME_FIXATING_FOR_ACTIVATION ? HOME_ACTIVATING : HOME_ACTIVATING_FOR_ACQUIRE); + + return; + } + + log_debug("Fixation of %s completed.", h->user_name); + + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + + /* Reset the state to "invalid", which makes home_get_state() test if the image exists and returns + * HOME_ABSENT vs. HOME_INACTIVE as necessary. */ + home_set_state(h, _HOME_STATE_INVALID); + return; + +fail: + /* If fixation fails, we stay in unfixated state! */ + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, HOME_UNFIXATED); +} + +static void home_activate_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Activation failed: %m"); + goto finish; + } + + if (hr) { + bool signed_locally; + + r = home_verify_user_record(h, hr, &signed_locally, &error); + if (r < 0) + goto finish; + + r = home_set_record(h, hr); + if (r < 0) { + log_error_errno(r, "Failed to update home record, ignoring: %m"); + goto finish; + } + + h->signed_locally = signed_locally; + + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + + log_debug("Activation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_deactivate_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(h->state == HOME_DEACTIVATING); + assert(!hr); /* We don't expect a record on this operation */ + + if (ret < 0) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Deactivation of %s failed: %m", h->user_name); + goto finish; + } + + log_debug("Deactivation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_remove_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + Manager *m; + int r; + + assert(h); + assert(h->state == HOME_REMOVING); + assert(!hr); /* We don't expect a record on this operation */ + + m = h->manager; + + if (ret < 0 && ret != -EALREADY) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Removing %s failed: %m", h->user_name); + goto fail; + } + + /* For a couple of storage types we can't delete the actual data storage when called (such as LUKS on + * partitions like USB sticks, or so). Sometimes these storage locations are among those we normally + * automatically discover in /home or in udev. When such a home is deleted let's hence issue a rescan + * after completion, so that "unfixated" entries are rediscovered. */ + if (!IN_SET(user_record_test_image_path(h->record), USER_TEST_UNDEFINED, USER_TEST_ABSENT)) + manager_enqueue_rescan(m); + + /* The image is now removed from disk. Now also remove our stored record */ + r = home_unlink_record(h); + if (r < 0) { + log_error_errno(r, "Removing record file failed: %m"); + goto fail; + } + + log_debug("Removal of %s completed.", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + + /* Unload this record from memory too now. */ + h = home_free(h); + return; + +fail: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_create_finish(Home *h, int ret, UserRecord *hr) { + int r; + + assert(h); + assert(h->state == HOME_CREATING); + + if (ret < 0) { + sd_bus_error error = SD_BUS_ERROR_NULL; + + (void) convert_worker_errno(h, ret, &error); + log_error_errno(ret, "Operation on %s failed: %m", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, ret, &error); + + if (h->unregister_on_failure) { + (void) home_unlink_record(h); + h = home_free(h); + return; + } + + home_set_state(h, _HOME_STATE_INVALID); + return; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + } + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to save record to disk, ignoring: %m"); + + log_debug("Creation of %s completed.", h->user_name); + + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_change_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Change operation failed: %m"); + goto finish; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + else { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + } + + log_debug("Change operation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_locking_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(h->state == HOME_LOCKING); + + if (ret < 0) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Locking operation failed: %m"); + goto finish; + } + + log_debug("Locking operation of %s completed.", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + home_set_state(h, HOME_LOCKED); + return; + +finish: + /* If a specific home doesn't know the concept of locking, then that's totally OK, don't propagate + * the error if we are executing a LockAllHomes() operation. */ + + if (h->current_operation->type == OPERATION_LOCK_ALL && r == -ENOTTY) + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + else + h->current_operation = operation_result_unref(h->current_operation, r, &error); + + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_unlocking_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Unlocking operation failed: %m"); + goto finish; + } + + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + else { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + + log_debug("Unlocking operation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_authenticating_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Authentication failed: %m"); + goto finish; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + else { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + } + + log_debug("Authentication of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void *userdata) { + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_fclose_ FILE *f = NULL; + Home *h = userdata; + int ret; + + assert(s); + assert(si); + assert(h); + + assert(h->worker_pid == si->si_pid); + assert(h->worker_event_source); + assert(h->worker_stdout_fd >= 0); + + (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h); + + h->worker_pid = 0; + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + + if (si->si_code != CLD_EXITED) { + assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED)); + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker process died abnormally with signal %s.", signal_to_string(si->si_status)); + } else if (si->si_status != EXIT_SUCCESS) { + /* If we received an error code via sd_notify(), use it */ + if (h->worker_error_code != 0) + ret = log_debug_errno(h->worker_error_code, "Worker reported error code %s.", errno_to_name(h->worker_error_code)); + else + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker exited with exit code %i.", si->si_status); + } else + ret = home_parse_worker_stdout(TAKE_FD(h->worker_stdout_fd), &hr); + + h->worker_stdout_fd = safe_close(h->worker_stdout_fd); + + switch (h->state) { + + case HOME_FIXATING: + case HOME_FIXATING_FOR_ACTIVATION: + case HOME_FIXATING_FOR_ACQUIRE: + home_fixate_finish(h, ret, hr); + break; + + case HOME_ACTIVATING: + case HOME_ACTIVATING_FOR_ACQUIRE: + home_activate_finish(h, ret, hr); + break; + + case HOME_DEACTIVATING: + home_deactivate_finish(h, ret, hr); + break; + + case HOME_LOCKING: + home_locking_finish(h, ret, hr); + break; + + case HOME_UNLOCKING: + case HOME_UNLOCKING_FOR_ACQUIRE: + home_unlocking_finish(h, ret, hr); + break; + + case HOME_CREATING: + home_create_finish(h, ret, hr); + break; + + case HOME_REMOVING: + home_remove_finish(h, ret, hr); + break; + + case HOME_UPDATING: + case HOME_UPDATING_WHILE_ACTIVE: + case HOME_RESIZING: + case HOME_RESIZING_WHILE_ACTIVE: + case HOME_PASSWD: + case HOME_PASSWD_WHILE_ACTIVE: + home_change_finish(h, ret, hr); + break; + + case HOME_AUTHENTICATING: + case HOME_AUTHENTICATING_WHILE_ACTIVE: + case HOME_AUTHENTICATING_FOR_ACQUIRE: + home_authenticating_finish(h, ret, hr); + break; + + default: + assert_not_reached("Unexpected state after worker exited"); + } + + return 0; +} + +static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(erase_and_freep) char *formatted = NULL; + _cleanup_close_ int stdin_fd = -1, stdout_fd = -1; + pid_t pid = 0; + int r; + + assert(h); + assert(verb); + assert(hr); + + if (h->worker_pid != 0) + return -EBUSY; + + assert(h->worker_stdout_fd < 0); + assert(!h->worker_event_source); + + v = json_variant_ref(hr->json); + + if (secret) { + JsonVariant *sub = NULL; + + sub = json_variant_by_key(secret->json, "secret"); + if (!sub) + return -ENOKEY; + + r = json_variant_set_field(&v, "secret", sub); + if (r < 0) + return r; + } + + r = json_variant_format(v, 0, &formatted); + if (r < 0) + return r; + + stdin_fd = acquire_data_fd(formatted, strlen(formatted), 0); + if (stdin_fd < 0) + return stdin_fd; + + log_debug("Sending to worker: %s", formatted); + + stdout_fd = memfd_create("homework-stdout", MFD_CLOEXEC); + if (stdout_fd < 0) + return -errno; + + r = safe_fork_full("(sd-homework)", + (int[]) { stdin_fd, stdout_fd }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + if (setenv("NOTIFY_SOCKET", "/run/systemd/home/notify", 1) < 0) { + log_error_errno(errno, "Failed to set $NOTIFY_SOCKET: %m"); + _exit(EXIT_FAILURE); + } + + r = rearrange_stdio(stdin_fd, stdout_fd, STDERR_FILENO); + if (r < 0) { + log_error_errno(r, "Failed to rearrange stdin/stdout/stderr: %m"); + _exit(EXIT_FAILURE); + } + + stdin_fd = stdout_fd = -1; /* have been invalidated by rearrange_stdio() */ + + // FIXME: drop this line + /* execl("/usr/bin/valgrind", "/usr/bin/valgrind", "/home/lennart/projects/systemd/build/systemd-homework", */ + /* /\* SYSTEMD_HOMEWORK_PATH,*\/ verb, NULL); */ + execl("/home/lennart/projects/systemd/build/systemd-homework", + SYSTEMD_HOMEWORK_PATH, verb, NULL); + + execl(SYSTEMD_HOMEWORK_PATH, SYSTEMD_HOMEWORK_PATH, verb, NULL); + log_error_errno(errno, "Failed to invoke " SYSTEMD_HOMEWORK_PATH ": %m"); + _exit(EXIT_FAILURE); + } + + r = sd_event_add_child(h->manager->event, &h->worker_event_source, pid, WEXITED, home_on_worker_process, h); + if (r < 0) + return r; + + (void) sd_event_source_set_description(h->worker_event_source, "worker"); + + r = hashmap_put(h->manager->homes_by_worker_pid, PID_TO_PTR(pid), h); + if (r < 0) { + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + return r; + } + + h->worker_stdout_fd = TAKE_FD(stdout_fd); + h->worker_pid = pid; + h->worker_error_code = 0; + + return 0; +} + +static int home_ratelimit(Home *h, sd_bus_error *error) { + int r, ret; + + assert(h); + + ret = user_record_ratelimit(h->record); + if (ret < 0) + return ret; + + if (h->state != HOME_UNFIXATED) { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to save updated record, ignoring: %m"); + } + + if (ret == 0) { + char buf[FORMAT_TIMESPAN_MAX]; + usec_t t, n; + + n = now(CLOCK_REALTIME); + t = user_record_ratelimit_next_try(h->record); + + if (t != USEC_INFINITY && t > n) + return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again in %s!", + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC)); + + return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again later."); + } + + return 0; +} + +static int home_fixate_internal( + Home *h, + UserRecord *secret, + HomeState for_state, + sd_bus_error *error) { + + int r; + + assert(h); + assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); + + r = home_start_work(h, "inspect", h->record, secret); + if (r < 0) + return r; + + if (for_state == HOME_FIXATING_FOR_ACTIVATION) { + /* Remember the secret data, since we need it for the activation again, later on. */ + user_record_unref(h->secret); + h->secret = user_record_ref(secret); + } + + home_set_state(h, for_state); + return 0; +} + +int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_FIXATED, "Home %s is already fixated.", h->user_name); + case HOME_UNFIXATED: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_fixate_internal(h, secret, HOME_FIXATING, error); +} + +static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); + + r = home_start_work(h, "activate", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + return home_fixate_internal(h, secret, HOME_FIXATING_FOR_ACTIVATION, error); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_ACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_ACTIVE, "Home %s is already active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_activate_internal(h, secret, HOME_ACTIVATING, error); +} + +static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); + + r = home_start_work(h, "inspect", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_authenticate_internal(h, secret, state == HOME_ACTIVE ? HOME_AUTHENTICATING_WHILE_ACTIVE : HOME_AUTHENTICATING, error); +} + +static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) { + int r; + + assert(h); + + r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_DEACTIVATING); + return 0; +} + +int home_deactivate(Home *h, bool force, sd_bus_error *error) { + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + return home_deactivate_internal(h, force, error); +} + +int home_create(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_INACTIVE: + if (h->record->storage < 0) + break; /* if no storage is defined we don't know what precisely to look for, hence + * HOME_INACTIVE is OK in that case too. */ + + if (IN_SET(user_record_test_image_path(h->record), USER_TEST_MAYBE, USER_TEST_UNDEFINED)) + break; /* And if the image path test isn't conclusive, let's also go on */ + + _fallthrough_; + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Home of user %s already exists.", h->user_name); + case HOME_ABSENT: + break; + case HOME_ACTIVE: + case HOME_LOCKED: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + if (h->record->enforce_password_policy == false) + log_debug("Password quality check turned off for account, skipping."); + else { + r = quality_check_password(h->record, secret, error); + if (r < 0) + return r; + } + + r = home_start_work(h, "create", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, HOME_CREATING); + return 0; +} + +int home_remove(Home *h, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: /* If the home directory is absent, then this is just like unregistering */ + return home_unregister(h, error); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + break; + case HOME_ACTIVE: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + r = home_start_work(h, "remove", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_REMOVING); + return 0; +} + +static int user_record_extend_with_binding(UserRecord *hr, UserRecord *with_binding, UserRecordLoadFlags flags, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *nr = NULL; + JsonVariant *binding; + int r; + + assert(hr); + assert(with_binding); + assert(ret); + + assert_se(v = json_variant_ref(hr->json)); + + binding = json_variant_by_key(with_binding->json, "binding"); + if (binding) { + r = json_variant_set_field(&v, "binding", binding); + if (r < 0) + return r; + } + + nr = user_record_new(); + if (!nr) + return -ENOMEM; + + r = user_record_load(nr, v, flags); + if (r < 0) + return r; + + *ret = TAKE_PTR(nr); + return 0; +} + +static int home_update_internal(Home *h, const char *verb, UserRecord *hr, UserRecord *secret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL; + int r, c; + + assert(h); + assert(verb); + assert(hr); + + if (!user_record_compatible(hr, h->record)) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Updated user record is not compatible with existing one."); + c = user_record_compare_last_change(hr, h->record); /* refuse downgrades */ + if (c < 0) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_DOWNGRADE, "Refusing to update to older home record."); + + if (!secret && FLAGS_SET(hr->mask, USER_RECORD_SECRET)) { + r = user_record_clone(hr, USER_RECORD_EXTRACT_SECRET, &saved_secret); + if (r < 0) + return r; + + secret = saved_secret; + } + + r = manager_verify_user_record(h->manager, hr); + switch (r) { + + case USER_RECORD_UNSIGNED: + if (h->signed_locally <= 0) /* If the existing record is not owned by us, don't accept an + * unsigned new record. i.e. only implicitly sign new records + * that where previously signed by us too. */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + + /* The updated record is not signed, then do so now */ + r = manager_sign_user_record(h->manager, hr, &signed_hr, error); + if (r < 0) + return r; + + hr = signed_hr; + break; + + case USER_RECORD_SIGNED_EXCLUSIVE: + case USER_RECORD_SIGNED: + case USER_RECORD_FOREIGN: + /* Has already been signed. Great! */ + break; + + case -ENOKEY: + default: + return r; + } + + r = user_record_extend_with_binding(hr, h->record, USER_RECORD_LOAD_MASK_SECRET, &new_hr); + if (r < 0) + return r; + + if (c == 0) { + /* different payload but same lastChangeUSec field? That's not cool! */ + + r = user_record_masked_equal(new_hr, h->record, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing."); + } + + r = home_start_work(h, verb, new_hr, secret); + if (r < 0) + return r; + + return 0; +} + +int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + assert(hr); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + r = home_update_internal(h, "update", hr, NULL, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_UPDATING_WHILE_ACTIVE : HOME_UPDATING); + return 0; +} + +int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *c = NULL; + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + if (disk_size == UINT64_MAX || disk_size == h->record->disk_size) { + if (h->record->disk_size == UINT64_MAX) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not disk size to resize to specified."); + + c = user_record_ref(h->record); /* Shortcut if size is unspecified or matches the record */ + } else { + _cleanup_(user_record_unrefp) UserRecord *signed_c = NULL; + + if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + + r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c); + if (r < 0) + return r; + + r = user_record_set_disk_size(c, disk_size); + if (r == -ERANGE) + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "Requested size for home %s out of acceptable range.", h->user_name); + if (r < 0) + return r; + + r = user_record_update_last_changed(c, false); + if (r == -ECHRNG) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name); + if (r < 0) + return r; + + r = manager_sign_user_record(h->manager, c, &signed_c, error); + if (r < 0) + return r; + + user_record_unref(c); + c = TAKE_PTR(signed_c); + } + + r = home_update_internal(h, "resize", c, secret, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_RESIZING_WHILE_ACTIVE : HOME_RESIZING); + return 0; +} + +int home_passwd(Home *h, + UserRecord *new_secret, + UserRecord *old_secret, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *c = NULL, *merged_secret = NULL, *signed_c = NULL; + HomeState state; + int r; + + assert(h); + + if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + if (strv_isempty(new_secret->password)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No password to set specified."); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c); + if (r < 0) + return r; + + merged_secret = user_record_new(); + if (!merged_secret) + return -ENOMEM; + + r = user_record_merge_secret(merged_secret, old_secret); + if (r < 0) + return r; + + r = user_record_merge_secret(merged_secret, new_secret); + if (r < 0) + return r; + + r = user_record_make_hashed_password(c, new_secret); + if (r < 0) + return r; + + r = user_record_update_last_changed(c, true); + if (r == -ECHRNG) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name); + if (r < 0) + return r; + + r = manager_sign_user_record(h->manager, c, &signed_c, error); + if (r < 0) + return r; + + if (c->enforce_password_policy == false) + log_debug("Password quality check turned off for account, skipping."); + else { + r = quality_check_password(c, merged_secret, error); + if (r < 0) + return r; + } + + r = home_update_internal(h, "passwd", signed_c, merged_secret, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_PASSWD_WHILE_ACTIVE : HOME_PASSWD); + return 0; +} + +int home_unregister(Home *h, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s is not registered.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_ABSENT: + case HOME_INACTIVE: + break; + case HOME_ACTIVE: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + r = home_unlink_record(h); + if (r < 0) + return r; + + /* And destroy the whole entry. The caller needs to be prepared for that. */ + h = home_free(h); + return 1; +} + +int home_lock(Home *h, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is already locked.", h->user_name); + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_start_work(h, "lock", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_LOCKING); + return 0; +} + +static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); + + r = home_start_work(h, "unlock", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + assert(h); + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + case HOME_ACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_LOCKED, "Home %s is not locked.", h->user_name); + case HOME_LOCKED: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + return home_unlock_internal(h, secret, HOME_UNLOCKING, error); +} + +HomeState home_get_state(Home *h) { + assert(h); + + /* When the state field is initialized, it counts. */ + if (h->state >= 0) + return h->state; + + /* Otherwise, let's see if the home directory is mounted. If so, we assume for sure the home + * directory is active */ + if (user_record_test_home_directory(h->record) == USER_TEST_MOUNTED) + return HOME_ACTIVE; + + /* And if we see the image being gone, we report this as absent */ + if (user_record_test_image_path(h->record) == USER_TEST_ABSENT) + return HOME_ABSENT; + + /* And for all other cases we return "inactive". */ + return HOME_INACTIVE; +} + +void home_process_notify(Home *h, char **l) { + const char *e; + int error; + int r; + + assert(h); + + e = strv_env_get(l, "ERRNO"); + if (!e) { + log_debug("Got notify message lacking ERRNO= field, ignoring."); + return; + } + + r = safe_atoi(e, &error); + if (r < 0) { + log_debug_errno(r, "Failed to parse receieved error number, ignoring: %s", e); + return; + } + if (error <= 0) { + log_debug("Error number is out of range: %i", error); + return; + } + + h->worker_error_code = error; +} + +int home_killall(Home *h) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *unit = NULL; + int r; + + assert(h); + + if (!uid_is_valid(h->uid)) + return 0; + + assert(h->uid > 0); /* We never should be UID 0 */ + + /* Let's kill everything matching the specified UID */ + r = safe_fork("(sd-killer)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_WAIT|FORK_LOG, NULL); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + if (setresgid(h->uid, h->uid, h->uid) < 0) { + log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + if (setgroups(0, NULL) < 0) { + log_error_errno(errno, "Failed to reset auxiliary groups list: %m"); + _exit(EXIT_FAILURE); + } + + if (setresuid(h->uid, h->uid, h->uid) < 0) { + log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + if (kill(-1, SIGKILL) < 0) { + log_error_errno(errno, "Failed to kill all processes of UID " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + /* Let's also kill everything in the user's slice */ + if (asprintf(&unit, "user-" UID_FMT ".slice", h->uid) < 0) + return log_oom(); + + r = sd_bus_call_method( + h->manager->bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "KillUnit", + &error, + NULL, + "ssi", unit, "all", SIGKILL); + if (r < 0) + log_full_errno(sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT) ? LOG_DEBUG : LOG_WARNING, + r, "Failed to kill login processes of user, ignoring: %s", bus_error_message(&error, r)); + + return 1; +} + +static int home_get_disk_status_luks( + Home *h, + HomeState state, + uint64_t *ret_disk_size, + uint64_t *ret_disk_usage, + uint64_t *ret_disk_free, + uint64_t *ret_disk_ceiling, + uint64_t *ret_disk_floor) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, + disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX, + stat_used = UINT64_MAX, fs_size = UINT64_MAX, header_size = 0; + + struct statfs sfs; + const char *hd; + int r; + + assert(h); + assert(ret_disk_size); + assert(ret_disk_usage); + assert(ret_disk_free); + assert(ret_disk_ceiling); + + if (state != HOME_ABSENT) { + const char *ip; + + ip = user_record_image_path(h->record); + if (ip) { + struct stat st; + + if (stat(ip, &st) < 0) + log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", ip); + else if (S_ISREG(st.st_mode)) { + _cleanup_free_ char *parent = NULL; + + disk_size = st.st_size; + stat_used = st.st_blocks * 512; + + parent = dirname_malloc(ip); + if (!parent) + return log_oom(); + + if (statfs(parent, &sfs) < 0) + log_debug_errno(r, "Failed to statfs() %s, ignoring: %m", parent); + else + disk_ceiling = stat_used + sfs.f_bsize * sfs.f_bavail; + + } else if (S_ISBLK(st.st_mode)) { + _cleanup_free_ char *szbuf = NULL; + char p[SYS_BLOCK_PATH_MAX("/size")]; + + /* Let's read the size off sysfs, so that we don't have to open the device */ + xsprintf_sys_block_path(p, "/size", st.st_rdev); + r = read_one_line_file(p, &szbuf); + if (r < 0) + log_debug_errno(r, "Failed to read %s, ignoring: %m", p); + else { + uint64_t sz; + + r = safe_atou64(szbuf, &sz); + if (r < 0) + log_debug_errno(r, "Failed to parse %s, ignoring: %s", p, szbuf); + else + disk_size = sz * 512; + } + } else + log_debug("Image path is not a block device or regular file, not able to acquire size."); + } + } + + if (!HOME_STATE_IS_ACTIVE(state)) + goto finish; + + hd = user_record_home_directory(h->record); + if (!hd) + goto finish; + + if (statfs(hd, &sfs) < 0) { + log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", hd); + goto finish; + } + + disk_free = sfs.f_bsize * sfs.f_bavail; + fs_size = sfs.f_bsize * sfs.f_blocks; + if (disk_size != UINT64_MAX && disk_size > fs_size) + header_size = disk_size - fs_size; + + /* We take a perspective from the user here (as opposed to from the host): the used disk space is the + * difference from the limit and what's free. This makes a difference if sparse mode is not used: in + * that case the image is pre-allocated and thus appears all used from the host PoV but is not used + * up at all yet from the user's PoV. + * + * That said, we use use the stat() reported loopback file size as upper boundary: our footprint can + * never be larger than what we take up on the lowest layers. */ + + if (disk_size != UINT64_MAX && disk_size > disk_free) { + disk_usage = disk_size - disk_free; + + if (stat_used != UINT64_MAX && disk_usage > stat_used) + disk_usage = stat_used; + } else + disk_usage = stat_used; + + /* If we have the magic, determine floor preferably by magic */ + disk_floor = minimal_size_by_fs_magic(sfs.f_type) + header_size; + +finish: + /* If we don't know the magic, go by file system name */ + if (disk_floor == UINT64_MAX) + disk_floor = minimal_size_by_fs_name(user_record_file_system_type(h->record)); + + *ret_disk_size = disk_size; + *ret_disk_usage = disk_usage; + *ret_disk_free = disk_free; + *ret_disk_ceiling = disk_ceiling; + *ret_disk_floor = disk_floor; + + return 0; +} + +static int home_get_disk_status_directory( + Home *h, + HomeState state, + uint64_t *ret_disk_size, + uint64_t *ret_disk_usage, + uint64_t *ret_disk_free, + uint64_t *ret_disk_ceiling, + uint64_t *ret_disk_floor) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, + disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX; + _cleanup_free_ char *devnode = NULL; + struct statfs sfs; + struct dqblk req; + const char *path = NULL; + dev_t devno; + int r; + + assert(ret_disk_size); + assert(ret_disk_usage); + assert(ret_disk_free); + assert(ret_disk_ceiling); + assert(ret_disk_floor); + + if (HOME_STATE_IS_ACTIVE(state)) + path = user_record_home_directory(h->record); + + if (!path) { + if (state == HOME_ABSENT) + goto finish; + + path = user_record_image_path(h->record); + } + + if (!path) + goto finish; + + if (statfs(path, &sfs) < 0) + log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", path); + else { + disk_free = sfs.f_bsize * sfs.f_bavail; + disk_size = sfs.f_bsize * sfs.f_blocks; + + /* We don't initialize disk_usage from statfs() data here, since the device is likely not used + * by us alone, and disk_usage should only reflect our own use. */ + } + + if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME)) { + + r = btrfs_is_subvol(path); + if (r < 0) + log_debug_errno(r, "Failed to determine whether %s is a btrfs subvolume: %m", path); + else if (r > 0) { + BtrfsQuotaInfo qi; + + r = btrfs_subvol_get_subtree_quota(path, 0, &qi); + if (r < 0) + log_debug_errno(r, "Failed to query btrfs subtree quota, ignoring: %m"); + else { + disk_usage = qi.referenced; + + if (disk_free != UINT64_MAX) { + disk_ceiling = qi.referenced + disk_free; + + if (disk_size != UINT64_MAX && disk_ceiling > disk_size) + disk_ceiling = disk_size; + } + + if (qi.referenced_max != UINT64_MAX) { + if (disk_size != UINT64_MAX) + disk_size = MIN(qi.referenced_max, disk_size); + else + disk_size = qi.referenced_max; + } + + if (disk_size != UINT64_MAX) { + if (disk_size > disk_usage) + disk_free = disk_size - disk_usage; + else + disk_free = 0; + } + } + + goto finish; + } + } + + if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_FSCRYPT)) { + + r = get_block_device(path, &devno); + if (r < 0) { + log_debug_errno(r, "Failed to determine block device of %s, ignoring: %m", path); + goto finish; + } + if (devno == 0) { + log_debug("File system %s not backed by a block device, can't get quota, ignoring.", path); + goto finish; + } + r = device_path_make_major_minor(S_IFBLK, devno, &devnode); + if (r < 0) { + log_debug_errno(r, "Failed to derive block device path for file system %s, ignoring: %m", path); + goto finish; + } + + if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) { + + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_debug_errno(errno, "No UID quota support on %s.", path); + goto finish; + } + + if (errno != ESRCH) { + log_debug_errno(errno, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); + goto finish; + } + + disk_usage = 0; /* No record of this user? then nothing was used */ + } else { + if (FLAGS_SET(req.dqb_valid, QIF_SPACE) && disk_free != UINT64_MAX) { + disk_ceiling = req.dqb_curspace + disk_free; + + if (disk_size != UINT64_MAX && disk_ceiling > disk_size) + disk_ceiling = disk_size; + } + + if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS)) { + uint64_t q; + + /* Take the minimum of the quota and the available disk space here */ + q = req.dqb_bhardlimit * QIF_DQBLKSIZE; + if (disk_size != UINT64_MAX) + disk_size = MIN(disk_size, q); + else + disk_size = q; + } + if (FLAGS_SET(req.dqb_valid, QIF_SPACE)) { + disk_usage = req.dqb_curspace; + + if (disk_size != UINT64_MAX) { + if (disk_size > disk_usage) + disk_free = disk_size - disk_usage; + else + disk_free = 0; + } + } + } + } + +finish: + *ret_disk_size = disk_size; + *ret_disk_usage = disk_usage; + *ret_disk_free = disk_free; + *ret_disk_ceiling = disk_ceiling; + *ret_disk_floor = disk_floor; + + return 0; +} + +int home_augment_status( + Home *h, + UserRecordLoadFlags flags, + UserRecord **ret) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX; + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL, *v = NULL, *m = NULL, *status = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + char ids[SD_ID128_STRING_MAX]; + HomeState state; + sd_id128_t id; + int r; + + assert(h); + assert(ret); + + /* We are supposed to add this, this can't be on hence. */ + assert(!FLAGS_SET(flags, USER_RECORD_STRIP_STATUS)); + + r = sd_id128_get_machine(&id); + if (r < 0) + return r; + + state = home_get_state(h); + + switch (h->record->storage) { + + case USER_LUKS: + r = home_get_disk_status_luks(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor); + if (r < 0) + return r; + + break; + + case USER_CLASSIC: + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + case USER_CIFS: + r = home_get_disk_status_directory(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor); + if (r < 0) + return r; + + break; + + default: + ; /* unset */ + } + + if (disk_floor == UINT64_MAX || (disk_usage != UINT64_MAX && disk_floor < disk_usage)) + disk_floor = disk_usage; + if (disk_floor == UINT64_MAX || disk_floor < USER_DISK_SIZE_MIN) + disk_floor = USER_DISK_SIZE_MIN; + if (disk_ceiling == UINT64_MAX || disk_ceiling > USER_DISK_SIZE_MAX) + disk_ceiling = USER_DISK_SIZE_MAX; + + r = json_build(&status, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))), + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")), + JSON_BUILD_PAIR_CONDITION(disk_size != UINT64_MAX, "diskSize", JSON_BUILD_UNSIGNED(disk_size)), + JSON_BUILD_PAIR_CONDITION(disk_usage != UINT64_MAX, "diskUsage", JSON_BUILD_UNSIGNED(disk_usage)), + JSON_BUILD_PAIR_CONDITION(disk_free != UINT64_MAX, "diskFree", JSON_BUILD_UNSIGNED(disk_free)), + JSON_BUILD_PAIR_CONDITION(disk_ceiling != UINT64_MAX, "diskCeiling", JSON_BUILD_UNSIGNED(disk_ceiling)), + JSON_BUILD_PAIR_CONDITION(disk_floor != UINT64_MAX, "diskFloor", JSON_BUILD_UNSIGNED(disk_floor)), + JSON_BUILD_PAIR_CONDITION(h->signed_locally >= 0, "signedLocally", JSON_BUILD_BOOLEAN(h->signed_locally)) + )); + if (r < 0) + return r; + + j = json_variant_ref(h->record->json); + v = json_variant_ref(json_variant_by_key(j, "status")); + m = json_variant_ref(json_variant_by_key(v, sd_id128_to_string(id, ids))); + + r = json_variant_filter(&m, STRV_MAKE("diskSize", "diskUsage", "diskFree", "diskCeiling", "diskFloor", "signedLocally")); + if (r < 0) + return r; + + r = json_variant_merge(&m, status); + if (r < 0) + return r; + + r = json_variant_set_field(&v, ids, m); + if (r < 0) + return r; + + r = json_variant_set_field(&j, "status", v); + if (r < 0) + return r; + + ur = user_record_new(); + if (!ur) + return -ENOMEM; + + r = user_record_load(ur, j, flags); + if (r < 0) + return r; + + ur->incomplete = + FLAGS_SET(h->record->mask, USER_RECORD_PRIVILEGED) && + !FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED); + + *ret = TAKE_PTR(ur); + return 0; +} + +static int on_home_ref_eof(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(operation_unrefp) Operation *o = NULL; + Home *h = userdata; + + assert(s); + assert(h); + + if (h->ref_event_source_please_suspend == s) + h->ref_event_source_please_suspend = sd_event_source_disable_unref(h->ref_event_source_please_suspend); + + if (h->ref_event_source_dont_suspend == s) + h->ref_event_source_dont_suspend = sd_event_source_disable_unref(h->ref_event_source_dont_suspend); + + if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + return 0; + + log_info("Got notification that all sessions of user %s ended, deactivating automatically.", h->user_name); + + o = operation_new(OPERATION_PIPE_EOF, NULL); + if (!o) { + log_oom(); + return 0; + } + + home_schedule_operation(h, o, NULL); + return 0; +} + +int home_create_fifo(Home *h, bool please_suspend) { + _cleanup_close_ int ret_fd = -1; + sd_event_source **ss; + const char *fn, *suffix; + int r; + + assert(h); + + if (please_suspend) { + suffix = ".please-suspend"; + ss = &h->ref_event_source_please_suspend; + } else { + suffix = ".dont-suspend"; + ss = &h->ref_event_source_dont_suspend; + } + + fn = strjoina("/run/systemd/home/", h->user_name, suffix); + + if (!*ss) { + _cleanup_close_ int ref_fd = -1; + + (void) mkdir("/run/systemd/home/", 0755); + if (mkfifo(fn, 0600) < 0 && errno != EEXIST) + return log_error_errno(errno, "Failed to create FIFO %s: %m", fn); + + ref_fd = open(fn, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (ref_fd < 0) + return log_error_errno(errno, "Failed to open FIFO %s for reading: %m", fn); + + r = sd_event_add_io(h->manager->event, ss, ref_fd, 0, on_home_ref_eof, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate reference FIFO event source: %m"); + + (void) sd_event_source_set_description(*ss, "acquire-ref"); + + r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_IDLE-1); + if (r < 0) + return r; + + r = sd_event_source_set_io_fd_own(*ss, true); + if (r < 0) + return log_error_errno(r, "Failed to pass ownership of FIFO event fd to event source: %m"); + + TAKE_FD(ref_fd); + } + + ret_fd = open(fn, O_WRONLY|O_CLOEXEC|O_NONBLOCK); + if (ret_fd < 0) + return log_error_errno(errno, "Failed to open FIFO %s for writing: %m", fn); + + return TAKE_FD(ret_fd); +} + +static int home_dispatch_acquire(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int (*call)(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) = NULL; + HomeState for_state; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_ACQUIRE); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + for_state = HOME_FIXATING_FOR_ACQUIRE; + call = home_fixate_internal; + break; + + case HOME_ABSENT: + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + break; + + case HOME_INACTIVE: + for_state = HOME_ACTIVATING_FOR_ACQUIRE; + call = home_activate_internal; + break; + + case HOME_ACTIVE: + for_state = HOME_AUTHENTICATING_FOR_ACQUIRE; + call = home_authenticate_internal; + break; + + case HOME_LOCKED: + for_state = HOME_UNLOCKING_FOR_ACQUIRE; + call = home_unlock_internal; + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + + assert(!h->current_operation); + + if (call) { + r = home_ratelimit(h, &error); + if (r >= 0) + r = call(h, o->secret, for_state, &error); + } + + if (r != 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_release(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_RELEASE); + + if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + /* If there's now a reference again, then let's abort the release attempt */ + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_BUSY, "Home %s is currently referenced.", h->user_name); + else { + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + r = 0; /* done */ + break; + + case HOME_LOCKED: + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + break; + + case HOME_ACTIVE: + r = home_deactivate_internal(h, false, &error); + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + } + + assert(!h->current_operation); + + if (r <= 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_lock_all(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_LOCK_ALL); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_info("Home %s is not active, no locking necessary.", h->user_name); + r = 0; /* done */ + break; + + case HOME_LOCKED: + log_info("Home %s is already locked.", h->user_name); + r = 0; /* done */ + break; + + case HOME_ACTIVE: + log_info("Locking home %s.", h->user_name); + r = home_lock(h, &error); + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + + assert(!h->current_operation); + + if (r != 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_pipe_eof(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_PIPE_EOF); + + if (h->ref_event_source_please_suspend || h->ref_event_source_dont_suspend) + return 1; /* Hmm, there's a reference again, let's cancel this */ + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name); + break; + + case HOME_DEACTIVATING: + log_info("Home %s is already being deactivated, automatic deactivated unnecessary.", h->user_name); + break; + + case HOME_ACTIVE: + r = home_deactivate_internal(h, false, &error); + if (r < 0) + log_warning_errno(r, "Failed to deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r)); + break; + + case HOME_LOCKED: + default: + /* If the device is locked or any operation is being executed, let's leave this pending */ + return 0; + } + + /* Note that we don't call operation_fail() or operation_success() here, because this kind of + * operation has no message associated with it, and thus there's no need to propagate success. */ + + assert(!o->message); + return 1; +} + +static int home_dispatch_deactivate_force(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_DEACTIVATE_FORCE); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name); + break; + + case HOME_DEACTIVATING: + log_debug("Home %s is already being deactivated, forced deactivation due to unplug unnecessary.", h->user_name); + break; + + case HOME_ACTIVE: + case HOME_LOCKED: + r = home_deactivate_internal(h, true, &error); + if (r < 0) + log_warning_errno(r, "Failed to forcibly deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r)); + break; + + default: + /* If any operation is being executed, let's leave this pending */ + return 0; + } + + /* Note that we don't call operation_fail() or operation_success() here, because this kind of + * operation has no message associated with it, and thus there's no need to propagate success. */ + + assert(!o->message); + return 1; +} + +static int on_pending(sd_event_source *s, void *userdata) { + Home *h = userdata; + Operation *o; + int r; + + assert(s); + assert(h); + + o = ordered_set_first(h->pending_operations); + if (o) { + static int (* const operation_table[_OPERATION_MAX])(Home *h, Operation *o) = { + [OPERATION_ACQUIRE] = home_dispatch_acquire, + [OPERATION_RELEASE] = home_dispatch_release, + [OPERATION_LOCK_ALL] = home_dispatch_lock_all, + [OPERATION_PIPE_EOF] = home_dispatch_pipe_eof, + [OPERATION_DEACTIVATE_FORCE] = home_dispatch_deactivate_force, + }; + + assert(operation_table[o->type]); + r = operation_table[o->type](h, o); + if (r != 0) { + /* The operation completed, let's remove it from the pending list, and exit while + * leaving the event source enabled as it is. */ + assert_se(ordered_set_remove(h->pending_operations, o) == o); + operation_unref(o); + return 0; + } + } + + /* Nothing to do anymore, let's turn off this event source */ + r = sd_event_source_set_enabled(s, SD_EVENT_OFF); + if (r < 0) + return log_error_errno(r, "Failed to disable event source: %m"); + + return 0; +} + +int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error) { + int r; + + assert(h); + + if (o) { + if (ordered_set_size(h->pending_operations) >= PENDING_OPERATIONS_MAX) + return sd_bus_error_setf(error, BUS_ERROR_TOO_MANY_OPERATIONS, "Too many client operations requested"); + + r = ordered_set_ensure_allocated(&h->pending_operations, &operation_hash_ops); + if (r < 0) + return r; + + r = ordered_set_put(h->pending_operations, o); + if (r < 0) + return r; + + operation_ref(o); + } + + if (!h->pending_event_source) { + r = sd_event_add_defer(h->manager->event, &h->pending_event_source, on_pending, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate pending defer event source: %m"); + + (void) sd_event_source_set_description(h->pending_event_source, "pending"); + + r = sd_event_source_set_priority(h->pending_event_source, SD_EVENT_PRIORITY_IDLE); + if (r < 0) + return r; + } + + r = sd_event_source_set_enabled(h->pending_event_source, SD_EVENT_ON); + if (r < 0) + return log_error_errno(r, "Failed to trigger pending event source: %m"); + + return 0; +} + +static int home_get_image_path_seat(Home *h, char **ret) { + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + _cleanup_free_ char *c = NULL; + const char *ip, *seat; + struct stat st; + int r; + + assert(h); + + if (user_record_storage(h->record) != USER_LUKS) + return -ENXIO; + + ip = user_record_image_path(h->record); + if (!ip) + return -ENXIO; + + if (!path_startswith(ip, "/dev/")) + return -ENXIO; + + if (stat(ip, &st) < 0) + return -errno; + + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + r = sd_device_new_from_devnum(&d, 'b', st.st_rdev); + if (r < 0) + return r; + + r = sd_device_get_property_value(d, "ID_SEAT", &seat); + if (r == -ENOENT) /* no property means seat0 */ + seat = "seat0"; + else if (r < 0) + return r; + + c = strdup(seat); + if (!c) + return -ENOMEM; + + *ret = TAKE_PTR(c); + return 0; +} + +int home_auto_login(Home *h, char ***ret_seats) { + _cleanup_free_ char *seat = NULL, *seat2 = NULL; + + assert(h); + assert(ret_seats); + + (void) home_get_image_path_seat(h, &seat); + + if (h->record->auto_login > 0 && !streq_ptr(seat, "seat0")) { + /* For now, when the auto-login boolean is set for a user, let's make it mean + * "seat0". Eventually we can extend the concept and allow configuration of any kind of seat, + * but let's keep simple initially, most likely the feature is interesting on single-user + * systems anyway, only. + * + * We filter out users marked for auto-login in we know for sure their home directory is + * absent. */ + + if (user_record_test_image_path(h->record) != USER_TEST_ABSENT) { + seat2 = strdup("seat0"); + if (!seat2) + return -ENOMEM; + } + } + + if (seat || seat2) { + _cleanup_strv_free_ char **list = NULL; + size_t i = 0; + + list = new(char*, 3); + if (!list) + return -ENOMEM; + + if (seat) + list[i++] = TAKE_PTR(seat); + if (seat2) + list[i++] = TAKE_PTR(seat2); + + list[i] = NULL; + *ret_seats = TAKE_PTR(list); + return 1; + } + + *ret_seats = NULL; + return 0; +} + +int home_set_current_message(Home *h, sd_bus_message *m) { + assert(h); + + if (!m) + return 0; + + if (h->current_operation) + return -EBUSY; + + h->current_operation = operation_new(OPERATION_IMMEDIATE, m); + if (!h->current_operation) + return -ENOMEM; + + return 1; +} + +static const char* const home_state_table[_HOME_STATE_MAX] = { + [HOME_UNFIXATED] = "unfixated", + [HOME_ABSENT] = "absent", + [HOME_INACTIVE] = "inactive", + [HOME_FIXATING] = "fixating", + [HOME_FIXATING_FOR_ACTIVATION] = "fixating-for-activation", + [HOME_FIXATING_FOR_ACQUIRE] = "fixating-for-acquire", + [HOME_ACTIVATING] = "activating", + [HOME_ACTIVATING_FOR_ACQUIRE] = "activating-for-acquire", + [HOME_DEACTIVATING] = "deactivating", + [HOME_ACTIVE] = "active", + [HOME_LOCKING] = "locking", + [HOME_LOCKED] = "locked", + [HOME_UNLOCKING] = "unlocking", + [HOME_UNLOCKING_FOR_ACQUIRE] = "unlocking-for-acquire", + [HOME_CREATING] = "creating", + [HOME_REMOVING] = "removing", + [HOME_UPDATING] = "updating", + [HOME_UPDATING_WHILE_ACTIVE] = "updating-while-active", + [HOME_RESIZING] = "resizing", + [HOME_RESIZING_WHILE_ACTIVE] = "resizing-while-active", + [HOME_PASSWD] = "passwd", + [HOME_PASSWD_WHILE_ACTIVE] = "passwd-while-active", + [HOME_AUTHENTICATING] = "authenticating", + [HOME_AUTHENTICATING_WHILE_ACTIVE] = "authenticating-while-active", + [HOME_AUTHENTICATING_FOR_ACQUIRE] = "authenticating-for-acquire", +}; + +DEFINE_STRING_TABLE_LOOKUP(home_state, HomeState); diff --git a/src/home/homed-home.h b/src/home/homed-home.h new file mode 100644 index 0000000000000..c75b06722c625 --- /dev/null +++ b/src/home/homed-home.h @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +typedef struct Home Home; + +#include "homed-manager.h" +#include "homed-operation.h" +#include "list.h" +#include "ordered-set.h" +#include "user-record.h" + +typedef enum HomeState { + HOME_UNFIXATED, /* home exists, but local record does not */ + HOME_ABSENT, /* local record exists, but home does not */ + HOME_INACTIVE, /* record and home exist, but is not logged in */ + HOME_FIXATING, /* generating local record from home */ + HOME_FIXATING_FOR_ACTIVATION, /* fixating in order to activate soon */ + HOME_FIXATING_FOR_ACQUIRE, /* fixating because Acquire() was called */ + HOME_ACTIVATING, + HOME_ACTIVATING_FOR_ACQUIRE, /* activating because Acquire() was called */ + HOME_DEACTIVATING, + HOME_ACTIVE, /* logged in right now */ + HOME_LOCKING, + HOME_LOCKED, + HOME_UNLOCKING, + HOME_UNLOCKING_FOR_ACQUIRE, /* unlocking because Acquire() was called */ + HOME_CREATING, + HOME_REMOVING, + HOME_UPDATING, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE, /* authenticating because Acquire() was called */ + _HOME_STATE_MAX, + _HOME_STATE_INVALID = -1 +} HomeState; + +static inline bool HOME_STATE_IS_ACTIVE(HomeState state) { + return IN_SET(state, + HOME_ACTIVE, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE); +} + +static inline bool HOME_STATE_IS_EXECUTING_OPERATION(HomeState state) { + return IN_SET(state, + HOME_FIXATING, + HOME_FIXATING_FOR_ACTIVATION, + HOME_FIXATING_FOR_ACQUIRE, + HOME_ACTIVATING, + HOME_ACTIVATING_FOR_ACQUIRE, + HOME_DEACTIVATING, + HOME_LOCKING, + HOME_UNLOCKING, + HOME_UNLOCKING_FOR_ACQUIRE, + HOME_CREATING, + HOME_REMOVING, + HOME_UPDATING, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE); +} + +struct Home { + Manager *manager; + char *user_name; + uid_t uid; + + char *sysfs; /* When found via plugged in device, the sysfs path to it */ + + /* Note that the 'state' field is only set to a state while we are doing something (i.e. activating, + * deactivating, creating, removing, and such), or when the home is an "unfixated" one. When we are + * done with an operation we invalidate the state. This is hint for home_get_state() to check the + * state on request as needed from the mount table and similar.*/ + HomeState state; + int signed_locally; /* signed only by us */ + + UserRecord *record; + + pid_t worker_pid; + int worker_stdout_fd; + sd_event_source *worker_event_source; + int worker_error_code; + + /* The message we are currently processing, and thus need to reply to on completion */ + Operation *current_operation; + + /* Stores the raw, plaintext passwords, but only for short periods of time */ + UserRecord *secret; + + /* When we create a home and that fails, we should possibly unregister the record altogether + * again, which is remembered in this boolean. */ + bool unregister_on_failure; + + /* The reading side of a FIFO stored in /run/systemd/home/, the writing side being used for reference + * counting. The references dropped to zero as soon as we see EOF. This concept exists twice: once + * for clients that are fine if we suspend the home directory on system suspend, and once for cliets + * that are not ok with that. This allows us to determine for each home whether there are any clients + * that support unsuspend. */ + sd_event_source *ref_event_source_please_suspend; + sd_event_source *ref_event_source_dont_suspend; + + /* Any pending operations we still need to execute. These are for operations we want to queue if we + * can't execute them right-away. */ + OrderedSet *pending_operations; + + /* A defer event source that processes pending acquire/release/eof events. We have a common + * dispatcher that processes all three kinds of events. */ + sd_event_source *pending_event_source; + + /* Did we send out a D-Bus notification about this entry? */ + bool announced; + + /* Used to coalesce bus PropertiesChanged events */ + sd_event_source *deferred_change_event_source; +}; + +int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret); +Home *home_free(Home *h); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Home*, home_free); + +int home_set_record(Home *h, UserRecord *hr); +int home_save_record(Home *h); +int home_unlink_record(Home *h); + +int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_activate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_deactivate(Home *h, bool force, sd_bus_error *error); +int home_create(Home *h, UserRecord *secret, sd_bus_error *error); +int home_remove(Home *h, sd_bus_error *error); +int home_update(Home *h, UserRecord *new_record, sd_bus_error *error); +int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error); +int home_passwd(Home *h, UserRecord *new_secret, UserRecord *old_secret, sd_bus_error *error); +int home_unregister(Home *h, sd_bus_error *error); +int home_lock(Home *h, sd_bus_error *error); +int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error); + +HomeState home_get_state(Home *h); + +void home_process_notify(Home *h, char **l); + +int home_killall(Home *h); + +int home_augment_status(Home *h, UserRecordLoadFlags flags, UserRecord **ret); + +int home_create_fifo(Home *h, bool please_suspend); +int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error); + +int home_auto_login(Home *h, char ***ret_seats); + +int home_set_current_message(Home *h, sd_bus_message *m); + +const char *home_state_to_string(HomeState state); +HomeState home_state_from_string(const char *s); diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c new file mode 100644 index 0000000000000..38fa6f230573a --- /dev/null +++ b/src/home/homed-manager-bus.c @@ -0,0 +1,690 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-util.h" +#include "format-util.h" +#include "homed-bus.h" +#include "homed-home-bus.h" +#include "homed-manager-bus.h" +#include "homed-manager.h" +#include "strv.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-util.h" + +static int property_get_auto_login( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(bus); + assert(reply); + assert(m); + + r = sd_bus_message_open_container(reply, 'a', "(sso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + _cleanup_(strv_freep) char **seats = NULL; + _cleanup_free_ char *home_path = NULL; + char **s; + + r = home_auto_login(h, &seats); + if (r < 0) { + log_debug_errno(r, "Failed to determine whether home '%s' is candidate for auto-login, ignoring: %m", h->user_name); + continue; + } + if (!r) + continue; + + r = bus_home_path(h, &home_path); + if (r < 0) + return log_error_errno(r, "Failed to generate home bus path: %m"); + + STRV_FOREACH(s, seats) { + r = sd_bus_message_append(reply, "(sso)", h->user_name, *s, home_path); + if (r < 0) + return r; + } + } + + return sd_bus_message_close_container(reply); +} + +static int method_get_home_by_name( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *path = NULL; + const char *user_name; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + return sd_bus_reply_method_return( + message, "usussso", + (uint32_t) h->uid, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); +} + +static int method_get_home_by_uid( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *path = NULL; + Manager *m = userdata; + uint32_t uid; + int r; + Home *h; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "u", &uid); + if (r < 0) + return r; + if (!uid_is_valid(uid)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid); + + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid); + + /* Note that we don't use bus_home_path() here, but build the path manually, since if we are queried + * for a UID we should also generate the bus path with a UID, and bus_home_path() uses our more + * typical bus path by name. */ + if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0) + return -ENOMEM; + + return sd_bus_reply_method_return( + message, "ssussso", + h->user_name, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); +} + +static int method_list_homes( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(susussso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(h, m->homes_by_uid, i) { + _cleanup_free_ char *path = NULL; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + r = sd_bus_message_append( + reply, "(susussso)", + h->user_name, + (uint32_t) h->uid, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_get_user_record_by_name( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL, *path = NULL; + Manager *m = userdata; + const char *user_name; + bool incomplete; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + r = bus_home_get_record_json(h, message, &json, &incomplete); + if (r < 0) + return r; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + return sd_bus_reply_method_return( + message, "sbo", + json, + incomplete, + path); +} + +static int method_get_user_record_by_uid( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL, *path = NULL; + Manager *m = userdata; + bool incomplete; + uint32_t uid; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "u", &uid); + if (r < 0) + return r; + if (!uid_is_valid(uid)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid); + + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid); + + r = bus_home_get_record_json(h, message, &json, &incomplete); + if (r < 0) + return r; + + if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0) + return -ENOMEM; + + return sd_bus_reply_method_return( + message, "sbo", + json, + incomplete, + path); +} + +static int generic_home_method( + Manager *m, + sd_bus_message *message, + sd_bus_message_handler_t handler, + sd_bus_error *error) { + + const char *user_name; + Home *h; + int r; + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + return handler(message, h, error); +} + +static int method_activate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_activate, error); +} + +static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_deactivate, error); +} + +static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL; + struct passwd *pw; + struct group *gr; + bool signed_locally; + Home *other; + int r; + + assert(m); + assert(hr); + assert(ret); + + r = user_record_is_supported(hr, error); + if (r < 0) + return r; + + other = hashmap_get(m->homes_by_name, hr->user_name); + if (other) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists already, refusing.", hr->user_name); + + pw = getpwnam(hr->user_name); + if (pw) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists in the NSS user database, refusing.", hr->user_name); + + gr = getgrnam(hr->user_name); + if (gr) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s conflicts with an NSS group by the same name, refusing.", hr->user_name); + + r = manager_verify_user_record(m, hr); + switch (r) { + + case USER_RECORD_UNSIGNED: + /* If the record is unsigned, then let's sign it with our own key */ + r = manager_sign_user_record(m, hr, &signed_hr, error); + if (r < 0) + return r; + + hr = signed_hr; + _fallthrough_; + + case USER_RECORD_SIGNED_EXCLUSIVE: + signed_locally = true; + break; + + case USER_RECORD_SIGNED: + case USER_RECORD_FOREIGN: + signed_locally = false; + break; + + case -ENOKEY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_SIGNATURE, "Specified user record for %s is signed by a key we don't recognize, refusing.", hr->user_name); + + default: + return sd_bus_error_set_errnof(error, r, "Failed to validate signature for '%s': %m", hr->user_name); + } + + if (uid_is_valid(hr->uid)) { + other = hashmap_get(m->homes_by_uid, UID_TO_PTR(hr->uid)); + if (other) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by home %s, refusing.", hr->uid, other->user_name); + + pw = getpwuid(hr->uid); + if (pw) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by NSS user %s, refusing.", hr->uid, pw->pw_name); + + gr = getgrgid(hr->uid); + if (gr) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use as GID by NSS group %s, refusing.", hr->uid, gr->gr_name); + } else { + r = manager_augment_record_with_uid(m, hr); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to acquire UID for '%s': %m", hr->user_name); + } + + r = home_new(m, hr, NULL, ret); + if (r < 0) + return r; + + (*ret)->signed_locally = signed_locally; + return r; +} + +static int method_register_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_LOAD_EMBEDDED, &hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = validate_and_allocate_home(m, hr, &h, error); + if (r < 0) + return r; + + r = home_save_record(h); + if (r < 0) { + home_free(h); + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_unregister, error); +} + +static int method_create_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = validate_and_allocate_home(m, hr, &h, error); + if (r < 0) + return r; + + r = home_create(h, hr, error); + if (r < 0) + goto fail; + + assert(r == 0); + h->unregister_on_failure = true; + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; + +fail: + (void) home_unlink_record(h); + h = home_free(h); + return r; +} + +static int method_realize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_realize, error); +} + +static int method_remove_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_remove, error); +} + +static int method_fixate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_fixate, error); +} + +static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_authenticate, error); +} + +static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + assert(hr->user_name); + + h = hashmap_get(m->homes_by_name, hr->user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", hr->user_name); + + return bus_home_method_update_record(h, message, hr, error); +} + +static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_resize, error); +} + +static int method_change_password_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_change_password, error); +} + +static int method_lock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_lock, error); +} + +static int method_unlock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_unlock, error); +} + +static int method_acquire_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_acquire, error); +} + +static int method_ref_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_ref, error); +} + +static int method_release_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_release, error); +} + +static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(operation_unrefp) Operation *o = NULL; + bool waiting = false; + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(m); + + /* This is called from logind when we are preparing for system suspend. We enqueue a lock operation + * for every suitable home we have and only when all of them completed we send a reply indicating + * completion. */ + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + /* Automatically suspend all homes that have at least one client referencing it that asked + * for "please suspend", and no client that asked for "please do not suspend". */ + if (h->ref_event_source_dont_suspend || + !h->ref_event_source_please_suspend) + continue; + + if (!o) { + o = operation_new(OPERATION_LOCK_ALL, message); + if (!o) + return -ENOMEM; + } + + log_info("Automatically locking of home of user %s.", h->user_name); + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + waiting = true; + } + + if (waiting) /* At least one lock operation was enqeued, let's leave here without a reply: it will + * be sent as soon as the last of the lock operations completed. */ + return 1; + + return sd_bus_reply_method_return(message, NULL); +} + +const sd_bus_vtable manager_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("AutoLogin", "a(sso)", property_get_auto_login, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_METHOD("GetHomeByName", "s", "usussso", method_get_home_by_name, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("GetHomeByUID", "u", "ssussso", method_get_home_by_uid, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("GetUserRecordByName", "s", "sbo", method_get_user_record_by_name, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("GetUserRecordByUID", "u", "sbo", method_get_user_record_by_uid, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ListHomes", NULL, "a(susussso)", method_list_homes, SD_BUS_VTABLE_UNPRIVILEGED), + + /* The following methods directly execute an operation on a home, without ref-counting, queing or + * anything, and are accessible through homectl. */ + SD_BUS_METHOD("ActivateHome", "ss", NULL, method_activate_home, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("DeactivateHome", "s", NULL, method_deactivate_home, 0), + SD_BUS_METHOD("RegisterHome", "s", NULL, method_register_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Add JSON record to homed, but don't create actual $HOME */ + SD_BUS_METHOD("UnregisterHome", "s", NULL, method_unregister_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record from homed, but don't remove actual $HOME */ + SD_BUS_METHOD("CreateHome", "s", NULL, method_create_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Add JSON record, and create $HOME for it */ + SD_BUS_METHOD("RealizeHome", "ss", NULL, method_realize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Create $HOME for already registered JSON entry */ + SD_BUS_METHOD("RemoveHome", "s", NULL, method_remove_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record and remove $HOME */ + SD_BUS_METHOD("FixateHome", "ss", NULL, method_fixate_home, SD_BUS_VTABLE_SENSITIVE), /* Investigate $HOME and propagate contained JSON record into our database */ + SD_BUS_METHOD("AuthenticateHome", "ss", NULL, method_authenticate_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Just check credentials */ + SD_BUS_METHOD("UpdateHome", "s", NULL, method_update_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Update JSON record of existing user */ + SD_BUS_METHOD("ResizeHome", "sts", NULL, method_resize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ChangePasswordHome", "sss", NULL, method_change_password_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("LockHome", "s", NULL, method_lock_home, 0), /* Prepare active home for system suspend: flush out passwords, suspend access */ + SD_BUS_METHOD("UnlockHome", "ss", NULL, method_unlock_home, SD_BUS_VTABLE_SENSITIVE), /* Make $HOME usable after system resume again */ + + /* The following methods implement ref-counted activation, and are what the PAM module calls (and + * what "homectl with" runs). In contrast to the methods above which fail if an operation is already + * being executed on a home directory, these ones will queue the request, and are thus more + * reliable. Moreover, they are a bit smarter: AcquireHome() will fixate, activate, unlock, or + * authenticate depending on the state of the home, so that the end result is always the same + * (i.e. the home directory is accessible), and we always validate the specified passwords. RefHome() + * will not authenticate, and thus only works if home is already active. */ + SD_BUS_METHOD("AcquireHome", "ssb", "h", method_acquire_home, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("RefHome", "sb", "h", method_ref_home, 0), + SD_BUS_METHOD("ReleaseHome", "s", NULL, method_release_home, 0), + + /* An operation that acts on all homes that allow it */ + SD_BUS_METHOD("LockAllHomes", NULL, NULL, method_lock_all_homes, 0), + + SD_BUS_VTABLE_END +}; + +static int on_deferred_auto_login(sd_event_source *s, void *userdata) { + Manager *m = userdata; + int r; + + assert(m); + + m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source); + + r = sd_bus_emit_properties_changed( + m->bus, + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AutoLogin", NULL); + if (r < 0) + log_warning_errno(r, "Failed to send AutoLogin property change event, ignoring: %m"); + + return 0; +} + +int bus_manager_emit_auto_login_changed(Manager *m) { + int r; + assert(m); + + if (m->deferred_auto_login_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_auto_login_event_source, on_deferred_auto_login, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate auto login event source: %m"); + + r = sd_event_source_set_priority(m->deferred_auto_login_event_source, SD_EVENT_PRIORITY_IDLE+10); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(m->deferred_auto_login_event_source, "deferred-auto-login"); + return 1; +} diff --git a/src/home/homed-manager-bus.h b/src/home/homed-manager-bus.h new file mode 100644 index 0000000000000..40e1cc3d86d60 --- /dev/null +++ b/src/home/homed-manager-bus.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +extern const sd_bus_vtable manager_vtable[]; diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c new file mode 100644 index 0000000000000..0c952c8b67dfd --- /dev/null +++ b/src/home/homed-manager.c @@ -0,0 +1,1676 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "btrfs-util.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-util.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-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 "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_func, 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 = new0(Manager, 1); + if (!m) + return -ENOMEM; + + 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); + + 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; + unsigned line, column; + int r, is_signed; + Home *h; + + assert(m); + assert(name); + assert(fname); + + 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); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + 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 distuingish 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; +} + +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/") { + _cleanup_free_ char *devnode = NULL; + 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 = device_path_make_major_minor(S_IFBLK, st.st_dev, &devnode); + if (r < 0) + return log_error_errno(r, "Failed to derive block device path for %s: %m", where); + + if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, uid, (caddr_t) &req) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) + log_debug_errno(errno, "No UID quota support on %s, ignoring.", where); + else + log_warning_errno(errno, "Failed to query quota on %s, ignoring.", 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 a multiple images for a 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, 0, + 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 don'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 = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/home1", "org.freedesktop.home1.Manager", manager_vtable, m); + if (r < 0) + return log_error_errno(r, "Failed to add manager object vtable: %m"); + + r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/home1/home", "org.freedesktop.home1.Home", home_vtable, bus_home_object_find, m); + if (r < 0) + return log_error_errno(r, "Failed to add image object vtable: %m"); + + r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/home1/home", bus_home_node_enumerator, m); + if (r < 0) + return log_error_errno(r, "Failed to add image enumerator: %m"); + + r = sd_bus_add_object_manager(m->bus, NULL, "/org/freedesktop/home1/home"); + if (r < 0) + return log_error_errno(r, "Failed to add object manager: %m"); + + 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) { + union { + struct cmsghdr cmsghdr; + uint8_t buf[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(fd, &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC); + if (m < 0) + return -errno; + + 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("Recieved 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; + 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"); + + r = sockaddr_un_set_path(&sa.un, "/run/systemd/home/notify"); + if (r < 0) + return log_error_errno(r, "Failed to set AF_UNIX socket path: %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 ot 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_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 ot 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_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 privtate key file into place: %m"); + } + temp_private = mfree(temp_private); + + 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 focussed */ + + 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; +} diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h new file mode 100644 index 0000000000000..00298a3d2dcbc --- /dev/null +++ b/src/home/homed-manager.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" +#include "sd-device.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "homed-home.h" +#include "varlink.h" + +#define HOME_UID_MIN 60001 +#define HOME_UID_MAX 60513 + +struct Manager { + sd_event *event; + sd_bus *bus; + + Hashmap *polkit_registry; + + Hashmap *homes_by_uid; + Hashmap *homes_by_name; + Hashmap *homes_by_worker_pid; + Hashmap *homes_by_sysfs; + + bool scan_slash_home; + + sd_event_source *inotify_event_source; + + /* An even source we receieve sd_notify() messages from our worker from */ + sd_event_source *notify_socket_event_source; + + sd_device_monitor *device_monitor; + + sd_event_source *deferred_rescan_event_source; + sd_event_source *deferred_gc_event_source; + sd_event_source *deferred_auto_login_event_source; + + Home *gc_focus; + + VarlinkServer *varlink_server; + + EVP_PKEY *private_key; /* actually a pair of private and public key */ + Hashmap *public_keys; /* key name [char*] → publick key [EVP_PKEY*] */ +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); + +int manager_augment_record_with_uid(Manager *m, UserRecord *hr); + +int manager_enqueue_rescan(Manager *m); +int manager_enqueue_gc(Manager *m, Home *focus); + +int manager_verify_user_record(Manager *m, UserRecord *hr); + +int manager_acquire_key_pair(Manager *m); +int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error); + +int bus_manager_emit_auto_login_changed(Manager *m); diff --git a/src/home/homed-operation.c b/src/home/homed-operation.c new file mode 100644 index 0000000000000..80dc555cd0e66 --- /dev/null +++ b/src/home/homed-operation.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "fd-util.h" +#include "homed-operation.h" + +Operation *operation_new(OperationType type, sd_bus_message *m) { + Operation *o; + + assert(type >= 0); + assert(type < _OPERATION_MAX); + + o = new(Operation, 1); + if (!o) + return NULL; + + *o = (Operation) { + .type = type, + .n_ref = 1, + .message = sd_bus_message_ref(m), + .send_fd = -1, + .result = -1, + }; + + return o; +} + +static Operation *operation_free(Operation *o) { + int r; + + if (!o) + return NULL; + + if (o->message && o->result >= 0) { + + if (o->result) { + /* Propagate success */ + if (o->send_fd < 0) + r = sd_bus_reply_method_return(o->message, NULL); + else + r = sd_bus_reply_method_return(o->message, "h", o->send_fd); + + } else { + /* Propagate failure */ + if (sd_bus_error_is_set(&o->error)) + r = sd_bus_reply_method_error(o->message, &o->error); + else + r = sd_bus_reply_method_errnof(o->message, o->ret, "Failed to execute operation: %m"); + } + if (r < 0) + log_warning_errno(r, "Failed ot reply to %s method call, ignoring: %m", sd_bus_message_get_member(o->message)); + } + + sd_bus_message_unref(o->message); + user_record_unref(o->secret); + safe_close(o->send_fd); + sd_bus_error_free(&o->error); + + return mfree(o); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(Operation, operation, operation_free); + +void operation_result(Operation *o, int ret, const sd_bus_error *error) { + assert(o); + + if (ret >= 0) + o->result = true; + else { + o->ret = ret; + + sd_bus_error_free(&o->error); + sd_bus_error_copy(&o->error, error); + + o->result = false; + } +} diff --git a/src/home/homed-operation.h b/src/home/homed-operation.h new file mode 100644 index 0000000000000..224de9185253e --- /dev/null +++ b/src/home/homed-operation.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "user-record.h" + +typedef enum OperationType { + OPERATION_ACQUIRE, /* enqueued on AcquireHome() */ + OPERATION_RELEASE, /* enqueued on ReleaseHome() */ + OPERATION_LOCK_ALL, /* enqueued on LockAllHomes() */ + OPERATION_PIPE_EOF, /* enqueued when we see EOF on the per-home reference pipes */ + OPERATION_DEACTIVATE_FORCE, /* enqueued on hard $HOME unplug */ + OPERATION_IMMEDIATE, /* this is never enqueued, it's just a marker we immediately started executing an operation without enqueuing anything first. */ + _OPERATION_MAX, + _OPERATION_INVALID = -1, +} OperationType; + +/* Encapsulates an operation on one or more home directories. This has two uses: + * + * 1) For queuing an operation when we need to execute one for some reason but there's already one being + * executed. + * + * 2) When executing an operation without enqueuing it first (OPERATION_IMMEDIATE) + * + * Note that a single operation object can encapsulate operations on multiple home directories. This is used + * for the LockAllHomes() operation, which is one operation but applies to all homes at once. In case the + * operation applies to multiple homes the reference counter is increased once for each, and thus the + * operation is fully completed only after it reached zero again. + * + * The object (optionally) contains a reference of the D-Bus message triggering the operation, which is + * replied to when the operation is fully completed, i.e. when n_ref reaches zero. + */ + +typedef struct Operation { + unsigned n_ref; + OperationType type; + sd_bus_message *message; + + UserRecord *secret; + int send_fd; /* pipe fd for AcquireHome() which is taken already when we start the operation */ + + int result; /* < 0 if not completed yet, == 0 on failure, > 0 on success */ + sd_bus_error error; + int ret; +} Operation; + +Operation *operation_new(OperationType type, sd_bus_message *m); +Operation *operation_ref(Operation *operation); +Operation *operation_unref(Operation *operation); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Operation*, operation_unref); + +void operation_result(Operation *o, int ret, const sd_bus_error *error); + +static inline Operation* operation_result_unref(Operation *o, int ret, const sd_bus_error *error) { + if (!o) + return NULL; + + operation_result(o, ret, error); + return operation_unref(o); +} diff --git a/src/home/homed-varlink.c b/src/home/homed-varlink.c new file mode 100644 index 0000000000000..4ef0c716171d5 --- /dev/null +++ b/src/home/homed-varlink.c @@ -0,0 +1,368 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "group-record.h" +#include "homed-varlink.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "format-util.h" + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static bool client_is_trusted(Varlink *link, Home *h) { + uid_t peer_uid; + int r; + + assert(link); + assert(h); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) { + log_debug_errno(r, "Unable to query peer UID, ignoring: %m"); + return false; + } + + return peer_uid == 0 || peer_uid == h->uid; +} + +static int build_user_json(Home *h, bool trusted, JsonVariant **ret) { + _cleanup_(user_record_unrefp) UserRecord *augmented = NULL; + UserRecordLoadFlags flags; + int r; + + assert(h); + assert(ret); + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = home_augment_status(h, flags, &augmented); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(augmented->json)), + JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(augmented->incomplete)))); +} + +static bool home_user_match_lookup_parameters(LookupParameters *p, Home *h) { + assert(p); + assert(h); + + if (p->user_name && !streq(p->user_name, h->user_name)) + return false; + + if (uid_is_valid(p->uid) && h->uid != p->uid) + return false; + + return true; +} + +int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + Manager *m = userdata; + bool trusted; + Home *h; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + trusted = client_is_trusted(link, h); + + if (uid_is_valid(p.uid)) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(p.uid)); + else if (p.user_name) + h = hashmap_get(m->homes_by_name, p.user_name); + else { + Iterator i; + + /* If neither UID nor name was specified, then dump all homes. Do so with varlink_notify() + * for all entries but the last, so that clients can stream the results, and easily process + * them piecemeal. */ + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!home_user_match_lookup_parameters(&p, h)) + continue; + + if (v) { + /* An entry set from the previous iteration? Then send it now */ + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_user_json(h, trusted, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (!home_user_match_lookup_parameters(&p, h)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_user_json(h, trusted, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int build_group_json(Home *h, JsonVariant **ret) { + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + int r; + + assert(h); + assert(ret); + + g = group_record_new(); + if (!g) + return -ENOMEM; + + r = group_record_synthesize(g, h->record); + if (r < 0) + return r; + + assert(!FLAGS_SET(g->mask, USER_RECORD_SECRET)); + assert(!FLAGS_SET(g->mask, USER_RECORD_PRIVILEGED)); + + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(g->json)))); +} + +static bool home_group_match_lookup_parameters(LookupParameters *p, Home *h) { + assert(p); + assert(h); + + if (p->group_name && !streq(h->user_name, p->group_name)) + return false; + + if (gid_is_valid(p->gid) && h->uid != (uid_t) p->gid) + return false; + + return true; +} + +int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + Manager *m = userdata; + Home *h; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (gid_is_valid(p.gid)) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR((uid_t) p.gid)); + else if (p.group_name) + h = hashmap_get(m->homes_by_name, p.group_name); + else { + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!home_group_match_lookup_parameters(&p, h)) + continue; + + if (v) { + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_group_json(h, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (!home_group_match_lookup_parameters(&p, h)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(h, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + Manager *m = userdata; + LookupParameters p = {}; + Home *h; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (p.user_name) { + const char *last = NULL; + char **i; + + h = hashmap_get(m->homes_by_name, p.user_name); + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (p.group_name) { + if (!strv_contains(h->record->member_of, p.group_name)) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + } + + STRV_FOREACH(i, h->record->member_of) { + if (last) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last)))); + if (r < 0) + return r; + } + + last = *i; + } + + if (last) + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last)))); + + } else if (p.group_name) { + const char *last = NULL; + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!strv_contains(h->record->member_of, p.group_name)) + continue; + + if (last) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + if (r < 0) + return r; + } + + last = h->user_name; + } + + if (last) + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + } else { + const char *last_user_name = NULL, *last_group_name = NULL; + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + char **j; + + STRV_FOREACH(j, h->record->member_of) { + + if (last_user_name) { + assert(last_group_name); + + r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + + if (r < 0) + return r; + } + + last_user_name = h->user_name; + last_group_name = *j; + } + } + + if (last_user_name) { + assert(last_group_name); + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + } + } + + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} diff --git a/src/home/homed-varlink.h b/src/home/homed-varlink.h new file mode 100644 index 0000000000000..4454d2344221f --- /dev/null +++ b/src/home/homed-varlink.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "homed-manager.h" + +int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); +int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); +int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); diff --git a/src/home/homed.c b/src/home/homed.c new file mode 100644 index 0000000000000..cd445354a1e13 --- /dev/null +++ b/src/home/homed.c @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "daemon-util.h" +#include "homed-manager.h" +#include "log.h" +#include "main-func.h" +#include "signal-util.h" + +static int run(int argc, char *argv[]) { + _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup_service(); + + // FIXME + log_set_max_level(LOG_DEBUG); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.Home", 1) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %m"); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, -1) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/homework.c b/src/home/homework.c new file mode 100644 index 0000000000000..95b5d442002a1 --- /dev/null +++ b/src/home/homework.c @@ -0,0 +1,5017 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "blkid-util.h" +#include "blockdev-util.h" +#include "btrfs-util.h" +#include "chattr-util.h" +#include "chown-recursive.h" +#include "copy.h" +#include "crypt-util.h" +#include "dirent-util.h" +#include "dm-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "fsck-util.h" +#include "hexdecoct.h" +#include "home-util.h" +#include "id128-util.h" +#include "io-util.h" +#include "json.h" +#include "log.h" +#include "loop-util.h" +#include "main-func.h" +#include "memory-util.h" +#include "missing_magic.h" +#include "missing_syscall.h" +#include "mkdir.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "random-util.h" +#include "resize-fs.h" +#include "rm-rf.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "umask-util.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "virt.h" +#include "xattr-util.h" + +/* Round down to the nearest 1K size. Note that Linux generally handles block devices with 512 blocks only, + * but actually doesn't accept uneven numbers in many cases. To avoid any confusion around this we'll + * strictly round disk sizes down to the next 1K boundary.*/ +#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(1023)) + +/* Make sure a bad password always results in a 3s delay, no matter what */ +#define BAD_PASSWORD_DELAY_USEC (3 * USEC_PER_SEC) + +static bool supported_fstype(const char *fstype) { + /* Limit the set of supported file systems a bit, as protection against little tested kernel file + * systems. Also, we only support the resize ioctls for these file systems. */ + return STR_IN_SET(fstype, "ext4", "btrfs", "xfs"); +} + +static const char *mount_options_for_fstype(const char *fstype) { + if (streq(fstype, "ext4")) + return "noquota,user_xattr"; + if (streq(fstype, "xfs")) + return "noquota"; + if (streq(fstype, "btrfs")) + return "noacl"; + return NULL; +} + +static int probe_file_system_by_fd( + int fd, + char **ret_fstype, + sd_id128_t *ret_uuid) { + + _cleanup_(blkid_free_probep) blkid_probe b = NULL; + _cleanup_free_ char *s = NULL; + const char *fstype = NULL, *uuid = NULL; + sd_id128_t id; + int r; + + assert(fd >= 0); + assert(ret_fstype); + assert(ret_uuid); + + b = blkid_new_probe(); + if (!b) + return -ENOMEM; + + errno = 0; + r = blkid_probe_set_device(b, fd, 0, 0); + if (r != 0) + return errno > 0 ? -errno : -ENOMEM; + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_UUID); + + errno = 0; + r = blkid_do_safeprobe(b); + if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */ + return -ENOPKG; + if (r != 0) + return errno > 0 ? -errno : -EIO; + + (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); + if (!fstype) + return -ENOPKG; + + (void) blkid_probe_lookup_value(b, "UUID", &uuid, NULL); + if (!uuid) + return -ENOPKG; + + r = sd_id128_from_string(uuid, &id); + if (r < 0) + return r; + + s = strdup(fstype); + if (!s) + return -ENOMEM; + + *ret_fstype = TAKE_PTR(s); + *ret_uuid = id; + + return 0; +} + +static int probe_file_system_by_path(const char *path, char **ret_fstype, sd_id128_t *ret_uuid) { + _cleanup_close_ int fd = -1; + + fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return -errno; + + return probe_file_system_by_fd(fd, ret_fstype, ret_uuid); +} + +static int block_get_size_by_fd(int fd, uint64_t *ret) { + struct stat st; + + assert(fd >= 0); + assert(ret); + + if (fstat(fd, &st) < 0) + return -errno; + + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + if (ioctl(fd, BLKGETSIZE64, ret) < 0) + return -errno; + + return 0; +} + +static int block_get_size_by_path(const char *path, uint64_t *ret) { + _cleanup_close_ int fd = -1; + + fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return -errno; + + return block_get_size_by_fd(fd, ret); +} + +static int run_fsck(const char *node, const char *fstype) { + int r, exit_status; + pid_t fsck_pid; + + assert(node); + assert(fstype); + + r = fsck_exists(fstype); + if (r < 0) + return log_error_errno(r, "Failed to check if fsck for file system %s exists: %m", fstype); + if (r == 0) { + log_warning("No fsck for file system %s installed, ignoring.", fstype); + return 0; + } + + r = safe_fork("(fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execl("/sbin/fsck", "/sbin/fsck", "-aTl", node, NULL); + log_error_errno(errno, "Failed to execute fsck: %m"); + _exit(FSCK_OPERATIONAL_ERROR); + } + + exit_status = wait_for_terminate_and_check("fsck", fsck_pid, WAIT_LOG_ABNORMAL); + if (exit_status < 0) + return exit_status; + if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) { + log_warning("fsck failed with exit status %i.", exit_status); + + if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing."); + + log_warning("Ignoring fsck error."); + } + + log_info("File system check completed."); + + return 1; +} + +static int luks_setup( + const char *node, + const char *dm_name, + sd_id128_t uuid, + const char *cipher, + const char *cipher_mode, + uint64_t volume_key_size, + char **passwords, + bool discard, + struct crypt_device **ret, + sd_id128_t *ret_found_uuid, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *vk = NULL; + sd_id128_t p; + size_t ks; + char **pp; + int r; + + assert(node); + assert(dm_name); + assert(ret); + + r = crypt_init(&cd, node); + if (r < 0) + return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_load(cd, CRYPT_LUKS2, NULL); + if (r < 0) + return log_error_errno(r, "Failed to load LUKS superblock: %m"); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + ks = (size_t) r; + + if (!sd_id128_is_null(uuid) || ret_found_uuid) { + const char *s; + + s = crypt_get_uuid(cd); + if (!s) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID."); + + r = sd_id128_from_string(s, &p); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID."); + + /* Check that the UUID matches, if specified */ + if (!sd_id128_is_null(uuid) && + !sd_id128_equal(uuid, p)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has wrong UUID."); + } + + if (cipher && !streq_ptr(cipher, crypt_get_cipher(cd))) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher."); + + if (cipher_mode && !streq_ptr(cipher_mode, crypt_get_cipher_mode(cd))) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher mode."); + + if (volume_key_size != UINT64_MAX && ks != volume_key_size) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong volume key size."); + + vk = malloc(ks); + if (!vk) + return log_oom(); + + STRV_FOREACH(pp, passwords) { + size_t vks = ks; + + r = crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + vk, + &vks, + *pp, + strlen(*pp)); + if (r < 0) { + log_debug_errno(r, "Password %zu didn't work: %m", (size_t) (pp - passwords)); + continue; + } + + r = crypt_activate_by_volume_key( + cd, + dm_name, + vk, vks, + discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0); + if (r < 0) + return log_error_errno(r, "Failed to unlock LUKS superblock: %m"); + + log_info("Setting up LUKS device /dev/mapper/%s completed.", dm_name); + + *ret = TAKE_PTR(cd); + + if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */ + *ret_found_uuid = p; + if (ret_volume_key) + *ret_volume_key = TAKE_PTR(vk); + if (ret_volume_key_size) + *ret_volume_key_size = vks; + + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock."); +} + +static int luks_open( + const char *dm_name, + char **passwords, + struct crypt_device **ret, + sd_id128_t *ret_found_uuid, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *vk = NULL; + sd_id128_t p; + size_t ks; + char **pp; + int r; + + assert(dm_name); + assert(ret); + + /* Opens a LUKS device that is already set up. Re-validates the password while doing so (which also + * provides us with the volume key, which we want). */ + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_load(cd, CRYPT_LUKS2, NULL); + if (r < 0) + return log_error_errno(r, "Failed to load LUKS superblock: %m"); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + ks = (size_t) r; + + if (ret_found_uuid) { + const char *s; + + s = crypt_get_uuid(cd); + if (!s) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID."); + + r = sd_id128_from_string(s, &p); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID."); + } + + vk = malloc(ks); + if (!vk) + return log_oom(); + + STRV_FOREACH(pp, passwords) { + size_t vks = ks; + + r = crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + vk, + &vks, + *pp, + strlen(*pp)); + if (r < 0) { + log_debug_errno(r, "Password %zu didn't work: %m", (size_t) (pp - passwords)); + continue; + } + + log_info("Discovered used LUKS device /dev/mapper/%s, and validated password.", dm_name); + + /* This is needed so that crypt_resize() can operate correctly for pre-existing LUKS + * devices. We need to tell libcryptsetup the volume key explicitly, so that it is in the + * kernel keyring. */ + r = crypt_activate_by_volume_key(cd, NULL, vk, vks, CRYPT_ACTIVATE_KEYRING_KEY); + if (r < 0) + return log_error_errno(r, "Failed to upload volume key again: %m"); + + log_info("Successfully re-activated LUKS device."); + + *ret = TAKE_PTR(cd); + + if (ret_found_uuid) + *ret_found_uuid = p; + if (ret_volume_key) + *ret_volume_key = TAKE_PTR(vk); + if (ret_volume_key_size) + *ret_volume_key_size = ks; + + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock."); +} + +static int fs_validate( + const char *dm_node, + sd_id128_t uuid, + char **ret_fstype, + sd_id128_t *ret_found_uuid) { + + _cleanup_free_ char *fstype = NULL; + sd_id128_t u; + int r; + + assert(dm_node); + assert(ret_fstype); + + r = probe_file_system_by_path(dm_node, &fstype, &u); + if (r < 0) + return log_error_errno(r, "Failed to probe file system: %m"); + + /* Limit the set of supported file systems a bit, as protection against little tested kernel file + * systems. Also, we only support the resize ioctls for these file systems. */ + if (!supported_fstype(fstype)) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Image contains unsupported file system: %s", strna(fstype)); + + if (!sd_id128_is_null(uuid) && + !sd_id128_equal(uuid, u)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "File system has wrong UUID."); + + log_info("Probing file system completed (found %s).", fstype); + + *ret_fstype = TAKE_PTR(fstype); + + if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */ + *ret_found_uuid = u; + + return 0; +} + +static int mount_node(const char *node, const char *fstype, bool discard) { + _cleanup_free_ char *joined = NULL; + const char *options, *discard_option; + int r; + + options = mount_options_for_fstype(fstype); + + discard_option = discard ? "discard" : "nodiscard"; + + if (options) { + joined = strjoin(options, ",", discard_option); + if (!joined) + return log_oom(); + + options = joined; + } else + options = discard_option; + + r = mount_verbose(LOG_ERR, node, "/run/systemd/user-home-mount", fstype, MS_NODEV|MS_NOSUID|MS_RELATIME, strempty(options)); + if (r < 0) + return r; + + log_info("Mounting file system completed."); + return 0; +} + +static int unshare_and_mount(const char *node, const char *fstype, bool discard) { + int r; + + if (unshare(CLONE_NEWNS) < 0) + return log_error_errno(errno, "Couldn't unshare file system namespace: %m"); + + r = mount_verbose(LOG_ERR, "/run", "/run", NULL, MS_SLAVE|MS_REC, NULL); /* Mark /run as MS_SLAVE in our new namespace */ + if (r < 0) + return r; + + (void) mkdir_p("/run/systemd/user-home-mount", 0700); + + if (node) + return mount_node(node, fstype, discard); + + return 0; +} + +typedef struct HomeSetup { + char *dm_name; + char *dm_node; + + LoopDevice *loop; + struct crypt_device *crypt_device; + int root_fd; + sd_id128_t found_partition_uuid; + sd_id128_t found_luks_uuid; + sd_id128_t found_fs_uuid; + + void *found_fscrypt_salt; + size_t found_fscrypt_salt_size; + + void *volume_key; + size_t volume_key_size; + + bool undo_dm; + bool undo_mount; + + uint64_t partition_offset; + uint64_t partition_size; +} HomeSetup; + +#define HOME_SETUP_INIT \ + { \ + .root_fd = -1, \ + .partition_offset = UINT64_MAX, \ + .partition_size = UINT64_MAX, \ + } + +static int home_setup_undo(HomeSetup *setup) { + int r = 0, q; + + assert(setup); + + setup->root_fd = safe_close(setup->root_fd); + + if (setup->undo_mount) { + q = umount_verbose("/run/systemd/user-home-mount"); + if (q < 0) + r = q; + } + + if (setup->undo_dm && setup->crypt_device && setup->dm_name) { + q = crypt_deactivate(setup->crypt_device, setup->dm_name); + if (q < 0) + r = q; + } + + setup->undo_mount = false; + setup->undo_dm = false; + + setup->dm_name = mfree(setup->dm_name); + setup->dm_node = mfree(setup->dm_node); + + setup->loop = loop_device_unref(setup->loop); + crypt_free(setup->crypt_device); + setup->crypt_device = NULL; + + explicit_bzero_safe(setup->volume_key, setup->volume_key_size); + setup->volume_key = mfree(setup->volume_key); + setup->volume_key_size = 0; + + setup->found_fscrypt_salt = mfree(setup->found_fscrypt_salt); + setup->found_fscrypt_salt_size = 0; + + return r; +} + +static int make_dm_names(const char *user_name, char **ret_dm_name, char **ret_dm_node) { + _cleanup_free_ char *name = NULL, *node = NULL; + + assert(user_name); + assert(ret_dm_name); + assert(ret_dm_node); + + name = strjoin("home-", user_name); + if (!name) + return log_oom(); + + node = path_join("/dev/mapper/", name); + if (!node) + return log_oom(); + + *ret_dm_name = TAKE_PTR(name); + *ret_dm_node = TAKE_PTR(node); + return 0; +} + +static int luks_validate( + int fd, + const char *label, + sd_id128_t partition_uuid, + sd_id128_t *ret_partition_uuid, + uint64_t *ret_offset, + uint64_t *ret_size) { + + _cleanup_(blkid_free_probep) blkid_probe b = NULL; + sd_id128_t found_partition_uuid = SD_ID128_NULL; + const char *fstype = NULL, *pttype = NULL; + blkid_loff_t offset = 0, size = 0; + blkid_partlist pl; + bool found = false; + int r, i, n; + + assert(fd >= 0); + assert(label); + assert(ret_offset); + assert(ret_size); + + b = blkid_new_probe(); + if (!b) + return -ENOMEM; + + errno = 0; + r = blkid_probe_set_device(b, fd, 0, 0); + if (r != 0) + return errno > 0 ? -errno : -ENOMEM; + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE); + (void) blkid_probe_enable_partitions(b, 1); + (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS); + + errno = 0; + r = blkid_do_safeprobe(b); + if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */ + return -ENOPKG; + if (r != 0) + return errno > 0 ? -errno : -EIO; + + (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); + if (streq_ptr(fstype, "crypto_LUKS")) { + /* Directly a LUKS image */ + *ret_offset = 0; + *ret_size = UINT64_MAX; /* full disk */ + *ret_partition_uuid = SD_ID128_NULL; + return 0; + } else if (fstype) + return -ENOPKG; + + (void) blkid_probe_lookup_value(b, "PTTYPE", &pttype, NULL); + if (!streq_ptr(pttype, "gpt")) + return -ENOPKG; + + errno = 0; + pl = blkid_probe_get_partitions(b); + if (!pl) + return errno > 0 ? -errno : -ENOMEM; + + errno = 0; + n = blkid_partlist_numof_partitions(pl); + if (n < 0) + return errno > 0 ? -errno : -EIO; + + for (i = 0; i < n; i++) { + blkid_partition pp; + sd_id128_t id; + const char *sid; + + errno = 0; + pp = blkid_partlist_get_partition(pl, i); + if (!pp) + return errno > 0 ? -errno : -EIO; + + if (!streq_ptr(blkid_partition_get_type_string(pp), "773f91ef-66d4-49b5-bd83-d683bf40ad16")) + continue; + + if (!streq_ptr(blkid_partition_get_name(pp), label)) + continue; + + sid = blkid_partition_get_uuid(pp); + if (sid) { + r = sd_id128_from_string(sid, &id); + if (r < 0) + log_debug_errno(r, "Couldn't parse partition UUID %s, weird: %m", sid); + + if (!sd_id128_is_null(partition_uuid) && !sd_id128_equal(id, partition_uuid)) + continue; + } + + if (found) + return -ENOPKG; + + offset = blkid_partition_get_start(pp); + size = blkid_partition_get_size(pp); + found_partition_uuid = id; + + found = true; + } + + if (!found) + return -ENOPKG; + + if (offset < 0) + return -EINVAL; + if ((uint64_t) offset > UINT64_MAX / 512U) + return -EINVAL; + if (size <= 0) + return -EINVAL; + if ((uint64_t) size > UINT64_MAX / 512U) + return -EINVAL; + + *ret_offset = offset * 512U; + *ret_size = size * 512U; + *ret_partition_uuid = found_partition_uuid; + + return 0; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_CIPHER_CTX*, EVP_CIPHER_CTX_free); + +static int crypt_device_to_evp_cipher(struct crypt_device *cd, const EVP_CIPHER **ret) { + _cleanup_free_ char *cipher_name = NULL; + const char *cipher, *cipher_mode, *e; + size_t key_size, key_bits; + const EVP_CIPHER *cc; + int r; + + assert(cd); + + /* Let's find the right OpenSSL EVP_CIPHER object that matches the encryption settings of the LUKS + * device */ + + cipher = crypt_get_cipher(cd); + if (!cipher) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher from LUKS device."); + + cipher_mode = crypt_get_cipher_mode(cd); + if (!cipher_mode) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher mode from LUKS device."); + + e = strchr(cipher_mode, '-'); + if (e) + cipher_mode = strndupa(cipher_mode, e - cipher_mode); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Cannot get volume key size from LUKS device."); + + key_size = r; + key_bits = key_size * 8; + if (streq(cipher_mode, "xts")) + key_bits /= 2; + + if (asprintf(&cipher_name, "%s-%zu-%s", cipher, key_bits, cipher_mode) < 0) + return log_oom(); + + cc = EVP_get_cipherbyname(cipher_name); + if (!cc) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Selected cipher mode '%s' not supported, can't encrypt JSON record.", cipher_name); + + /* Verify that our key length calculations match what OpenSSL thinks */ + r = EVP_CIPHER_key_length(cc); + if (r < 0 || (uint64_t) r != key_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key size of selected cipher doesn't meet out expectations."); + + *ret = cc; + return 0; +} + +static int luks_validate_home_record( + struct crypt_device *cd, + UserRecord *h, + const void *volume_key, + UserRecord **ret_luks_home_record) { + + int r, token; + + assert(cd); + assert(h); + + for (token = 0;; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *rr = NULL; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(user_record_unrefp) UserRecord *lhr = NULL; + _cleanup_free_ void *encrypted = NULL, *iv = NULL; + size_t decrypted_size, encrypted_size, iv_size; + int decrypted_size_out1, decrypted_size_out2; + _cleanup_free_ char *decrypted = NULL; + const char *text, *type; + crypt_token_info state; + JsonVariant *jr, *jiv; + unsigned line, column; + const EVP_CIPHER *cc; + + state = crypt_token_status(cd, token, &type); + if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, give up */ + break; + if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL)) + continue; + if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state); + + if (!streq(type, "systemd-homed")) + continue; + + r = crypt_token_json_get(cd, token, &text); + if (r < 0) + return log_error_errno(r, "Failed to read LUKS token %i: %m", token); + + r = json_parse(text, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse LUKS token JSON data %u:%u: %m", line, column); + + jr = json_variant_by_key(v, "record"); + if (!jr) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'record' field."); + jiv = json_variant_by_key(v, "iv"); + if (!jiv) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'iv' field."); + + r = json_variant_unbase64(jr, &encrypted, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to base64 decode record: %m"); + + r = json_variant_unbase64(jiv, &iv, &iv_size); + if (r < 0) + return log_error_errno(r, "Failed to base64 decode IV: %m"); + + r = crypt_device_to_evp_cipher(cd, &cc); + if (r < 0) + return r; + if (iv_size > INT_MAX || EVP_CIPHER_iv_length(cc) != (int) iv_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "IV size doesn't match."); + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + if (EVP_DecryptInit_ex(context, cc, NULL, volume_key, iv) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context."); + + decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2; + decrypted = new(char, decrypted_size); + if (!decrypted) + return log_oom(); + + if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt JSON record."); + + assert((size_t) decrypted_size_out1 <= decrypted_size); + + if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted + decrypted_size_out1, &decrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryptio of JSON record."); + + assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size); + decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2; + + if (memchr(decrypted, 0, decrypted_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Inner NUL byte in JSON record, refusing."); + + decrypted[decrypted_size] = 0; + + r = json_parse(decrypted, JSON_PARSE_SENSITIVE, &rr, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse decrypted JSON record, refusing."); + + lhr = user_record_new(); + if (!lhr) + return log_oom(); + + r = user_record_load(lhr, rr, USER_RECORD_LOAD_EMBEDDED); + if (r < 0) + return log_error_errno(r, "Failed to parse user record: %m"); + + if (!user_record_compatible(h, lhr)) + return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "LUKS home record not compatible with host record, refusing."); + + r = user_record_test_secret(lhr, h); + if (r == -ENXIO) + return log_error_errno(r, "Embedded LUKS user record contains no hashed password, invalid."); + if (r == -ENOKEY) + return log_error_errno(r, "Supplied password does not match password of embedded LUKS user record."); + if (r < 0) + return log_error_errno(r, "Failed to validate password of embedded LUKS user record: %m"); + + log_info("Provided password unlocks embedded LUKS user record."); + + *ret_luks_home_record = TAKE_PTR(lhr); + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find home record in LUKS2 header, refusing."); +} + +static int format_luks_token_text( + struct crypt_device *cd, + UserRecord *hr, + const void *volume_key, + char **ret) { + + int r, encrypted_size_out1 = 0, encrypted_size_out2 = 0, iv_size, key_size; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ void *iv = NULL, *encrypted = NULL; + size_t text_length, encrypted_size; + _cleanup_free_ char *text = NULL; + const EVP_CIPHER *cc; + + assert(cd); + assert(hr); + assert(volume_key); + assert(ret); + + r = crypt_device_to_evp_cipher(cd, &cc); + if (r < 0) + return r; + + key_size = EVP_CIPHER_key_length(cc); + iv_size = EVP_CIPHER_iv_length(cc); + + if (iv_size > 0) { + iv = malloc(iv_size); + if (!iv) + return log_oom(); + + r = genuine_random_bytes(iv, iv_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate IV: %m"); + } + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + if (EVP_EncryptInit_ex(context, cc, NULL, volume_key, iv) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context."); + + r = json_variant_format(hr->json, 0, &text); + if (r < 0) + return log_error_errno(r,"Failed to format user record for LUKS: %m"); + + text_length = strlen(text); + encrypted_size = text_length + 2*key_size - 1; + + encrypted = malloc(encrypted_size); + if (!encrypted) + return log_oom(); + + if (EVP_EncryptUpdate(context, encrypted, &encrypted_size_out1, (uint8_t*) text, text_length) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt JSON record."); + + assert((size_t) encrypted_size_out1 <= encrypted_size); + + if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted + encrypted_size_out1, &encrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of JSON record. "); + + assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 <= encrypted_size); + + r = json_build(&v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-homed")), + JSON_BUILD_PAIR("keyslots", JSON_BUILD_EMPTY_ARRAY), + JSON_BUILD_PAIR("record", JSON_BUILD_BASE64(encrypted, encrypted_size_out1 + encrypted_size_out2)), + JSON_BUILD_PAIR("iv", JSON_BUILD_BASE64(iv, iv_size)))); + if (r < 0) + return log_error_errno(r, "Failed to prepare LUKS JSON token object: %m"); + + r = json_variant_format(v, 0, ret); + if (r < 0) + return log_error_errno(r, "Failed to format encrypted user record for LUKS: %m"); + + return 0; +} + +static int luks_store_header_identity( + UserRecord *h, + struct crypt_device *cd, + const void *volume_key, + UserRecord *old_home) { + + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL; + _cleanup_free_ char *text = NULL; + int token = 0, r; + + assert(h); + + if (!cd) + return 0; + + assert(volume_key); + + /* Let's store the user's identity record in the LUKS2 "token" header data fields, in an encrypted + * fashion. Why that? If we'd rely on the record being embedded in the payload file system itself we + * would have to mount the file system before we can validate the JSON record, its signatures and + * whether it matches what we are looking for. However, kernel file system implementations are + * generally not ready to be used on untrusted media. Hence let's store the record independently of + * the file system, so that we can validate it first, and only then mount the file system. To keep + * things simple we use the same encryption settings for this record as for the file system itself. */ + + r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &header_home); + if (r < 0) + return log_error_errno(r, "Failed to determine new header record: %m"); + + if (old_home && user_record_equal(old_home, header_home)) { + log_debug("Not updating header home record."); + return 0; + } + + r = format_luks_token_text(cd, header_home, volume_key, &text); + if (r < 0) + return r; + + for (;; token++) { + crypt_token_info state; + const char *type; + + state = crypt_token_status(cd, token, &type); + if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, we are done */ + break; + if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL)) + continue; /* Not ours */ + if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state); + + if (!streq(type, "systemd-homed")) + continue; + + r = crypt_token_json_set(cd, token, text); + if (r < 0) + return log_error_errno(r, "Failed to set JSON token for slot %i: %m", token); + + /* Now, let's free the text so that for all further matching tokens we all crypt_json_token_set() + * with a NULL text in order to invalidate the tokens. */ + text = mfree(text); + token++; + } + + if (text) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Didn't find any record token to update."); + + log_info("Wrote LUKS header user record."); + + return 1; +} + +static int run_fitrim(int root_fd) { + char buf[FORMAT_BYTES_MAX]; + struct fstrim_range range = { + .len = UINT64_MAX, + }; + + /* If discarding is on, discard everything right after mounting, so that the discard setting takes + * effect on activation. */ + + assert(root_fd >= 0); + + if (ioctl(root_fd, FITRIM, &range) < 0) { + if (IN_SET(errno, ENOTTY, EOPNOTSUPP, EBADF)) { + log_debug_errno(errno, "File system does not support FITRIM, not trimming."); + return 0; + } + + return log_warning_errno(errno, "Failed to invoke FITRIM, ignoring: %m"); + } + + log_info("Discarded unused %s.", + format_bytes(buf, sizeof(buf), range.len)); + return 1; +} + +static int run_fallocate(int backing_fd, const struct stat *st) { + char buf[FORMAT_BYTES_MAX]; + + assert(backing_fd >= 0); + assert(st); + + /* If discarding is off, let's allocate the whole image before mounting, so that the setting takes + * effect on activation */ + + if (!S_ISREG(st->st_mode)) + return 0; + + if (st->st_blocks >= DIV_ROUND_UP(st->st_size, 512)) { + log_info("Backing file is fully allocated already."); + return 0; + } + + if (fallocate(backing_fd, FALLOC_FL_KEEP_SIZE, 0, st->st_size) < 0) { + + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_debug_errno(errno, "fallocate() not supported on file system, ignoring."); + return 0; + } + + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to fully allocate home."); + return -ENOSPC; /* make recognizable */ + } + + return log_error_errno(errno, "Failed to allocate backing file blocks: %m"); + } + + log_info("Allocated additional %s.", + format_bytes(buf, sizeof(buf), (DIV_ROUND_UP(st->st_size, 512) - st->st_blocks) * 512)); + return 1; +} + +static int home_prepare_luks( + UserRecord *h, + bool already_activated, + const char *force_image_path, + HomeSetup *setup, + UserRecord **ret_luks_home) { + + sd_id128_t found_partition_uuid, found_luks_uuid, found_fs_uuid; + _cleanup_(user_record_unrefp) UserRecord *luks_home = NULL; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + bool dm_activated = false, mounted = false; + _cleanup_close_ int root_fd = -1; + size_t volume_key_size = 0; + uint64_t offset, size; + int r; + + assert(h); + assert(setup); + assert(setup->dm_name); + assert(setup->dm_node); + + assert(user_record_storage(h) == USER_LUKS); + + if (already_activated) { + struct loop_info64 info; + const char *n; + + r = luks_open(setup->dm_name, h->password, &cd, &found_luks_uuid, &volume_key, &volume_key_size); + if (r < 0) + return r; + + r = luks_validate_home_record(cd, h, volume_key, &luks_home); + if (r < 0) + return r; + + n = crypt_get_device_name(cd); + if (!n) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine backing device for DM %s.", setup->dm_name); + + r = loop_device_open(n, O_RDWR, &loop); + if (r < 0) + return log_error_errno(r, "Failed to open loopback device %s: %m", n); + + if (ioctl(loop->fd, LOOP_GET_STATUS64, &info) < 0) { + _cleanup_free_ char *sysfs = NULL; + struct stat st; + + if (!IN_SET(errno, ENOTTY, EINVAL)) + return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n); + + if (ioctl(loop->fd, BLKGETSIZE64, &size) < 0) + return log_error_errno(r, "Failed to read block device size of %s: %m", n); + + if (fstat(loop->fd, &st) < 0) + return log_error_errno(r, "Failed to stat block device %s: %m", n); + assert(S_ISBLK(st.st_mode)); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + + if (access(sysfs, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", sysfs); + + offset = 0; + } else { + _cleanup_free_ char *startfn = NULL, *buffer = NULL; + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + + r = read_one_line_file(sysfs, &buffer); + if (r < 0) + return log_error_errno(r, "Failed to read partition start offset: %m"); + + r = safe_atou64(buffer, &offset); + if (r < 0) + return log_error_errno(r, "Failed to parse partition start offset: %m"); + + if (offset > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Offset too large for 64 byte range, refusing."); + + offset *= 512U; + } + } else { + offset = info.lo_offset; + size = info.lo_sizelimit; + } + + found_partition_uuid = found_fs_uuid = SD_ID128_NULL; + + log_info("Discovered used loopback device %s.", loop->node); + + root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(r, "Failed to open home directory: %m"); + goto fail; + } + } else { + _cleanup_free_ char *fstype = NULL, *subdir = NULL; + _cleanup_close_ int fd = -1; + const char *ip; + struct stat st; + + ip = force_image_path ?: user_record_image_path(h); + + subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h)); + if (!subdir) + return log_oom(); + + fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return log_error_errno(errno, "Failed to open image file %s: %m", ip); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to fstat() image file: %m"); + if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode)) + return log_error_errno(errno, "Image file %s is not a regular file or block device: %m", ip); + + r = luks_validate(fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size); + if (r < 0) + return log_error_errno(r, "Failed to validate disk label: %m"); + + if (!user_record_luks_discard(h)) { + r = run_fallocate(fd, &st); + if (r < 0) + return r; + } + + r = loop_device_make_full(fd, O_RDWR, offset, size, 0, &loop); + if (r == -ENOENT) { + log_error_errno(r, "Loopback block device support is not available on this system."); + return -ENOLINK; /* make recognizable */ + } + if (r < 0) + return log_error_errno(r, "Failed to allocate loopback context: %m"); + + log_info("Setting up loopback device %s completed.", loop->node ?: ip); + + r = luks_setup(loop->node ?: ip, + setup->dm_name, + h->luks_uuid, + h->luks_cipher, + h->luks_cipher_mode, + h->luks_volume_key_size, + h->password, + user_record_luks_discard(h), + &cd, + &found_luks_uuid, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + dm_activated = true; + + r = luks_validate_home_record(cd, h, volume_key, &luks_home); + if (r < 0) + goto fail; + + r = fs_validate(setup->dm_node, h->file_system_uuid, &fstype, &found_fs_uuid); + if (r < 0) + goto fail; + + r = run_fsck(setup->dm_node, fstype); + if (r < 0) + goto fail; + + r = unshare_and_mount(setup->dm_node, fstype, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + mounted = true; + + root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(r, "Failed to open home directory: %m"); + goto fail; + } + + if (user_record_luks_discard(h)) + (void) run_fitrim(root_fd); + } + + setup->loop = TAKE_PTR(loop); + setup->crypt_device = TAKE_PTR(cd); + setup->root_fd = TAKE_FD(root_fd); + setup->found_partition_uuid = found_partition_uuid; + setup->found_luks_uuid = found_luks_uuid; + setup->found_fs_uuid = found_fs_uuid; + setup->partition_offset = offset; + setup->partition_size = size; + setup->volume_key = TAKE_PTR(volume_key); + setup->volume_key_size = volume_key_size; + + setup->undo_mount = mounted; + setup->undo_dm = dm_activated; + + if (ret_luks_home) + *ret_luks_home = TAKE_PTR(luks_home); + + return 0; + +fail: + if (mounted) + (void) umount_verbose("/run/systemd/user-home-mount"); + + if (dm_activated) + (void) crypt_deactivate(cd, setup->dm_name); + + return r; +} + +static int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup) { + assert(h); + assert(setup); + + setup->root_fd = open(user_record_image_path(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + return 0; +} + +static int home_upload_fscrypt_key( + UserRecord *h, + const char *password, + const void *salt, + size_t salt_size, + const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + uint8_t ret_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE]) { + + uint8_t master[512 / 8] = {}, hashed[512 / 8] = {}, hashed2[512 / 8] = {}; + _cleanup_free_ char *hex = NULL; + const char *description; + struct fscrypt_key key; + key_serial_t serial; + int r; + + assert(h); + + /* Derive a master key from the password using PBKDF2-SHA512-HMAC */ + if (PKCS5_PBKDF2_HMAC(password, strlen(password), + salt, salt_size, + 0xFFFF, EVP_sha512(), sizeof(master), master) != 1) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed"); + goto finish; + } + + /* Derive the key descriptor from the master key, also in order to be compatible with e4crypt */ + assert_se(SHA512(master, sizeof(master), hashed) == hashed); + assert_se(SHA512(hashed, sizeof(hashed), hashed2) == hashed2); + + if (match_key_descriptor) { + assert_cc(FS_KEY_DESCRIPTOR_SIZE <= sizeof(hashed2)); + if (memcmp(match_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE) != 0) + return -ENOANO; /* don't log here */ + } + + hex = hexmem(hashed2, 8); + if (!hex) { + r = log_oom(); + goto finish; + } + + description = strjoina("fscrypt:", hex); + + key = (struct fscrypt_key) { + .mode = 0, + .size = sizeof(master), + }; + + assert_cc(sizeof(master) <= sizeof(key.raw)); + memcpy(key.raw, master, sizeof(master)); + + /* Upload to the kernel */ + serial = add_key("logon", description, &key, sizeof(key), KEY_SPEC_THREAD_KEYRING); + if (serial < 0) { + r = log_error_errno(errno, "Failed to install master key in keyring: %m"); + goto finish; + } + + log_info("Configured encryption key."); + + if (ret_key_descriptor) { + assert_cc(FS_KEY_DESCRIPTOR_SIZE <= sizeof(hashed2)); + memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE); + } + + r = 0; + +finish: + explicit_bzero_safe(master, sizeof(master)); + explicit_bzero_safe(&key, sizeof(key)); + return r; +} + +static int home_prepare_fscrypt(UserRecord *h, bool already_activated, HomeSetup *setup) { + _cleanup_free_ char *salt_text = NULL; + struct fscrypt_policy policy; + bool uploaded = false; + const char *ip; + const void *salt = NULL; + _cleanup_free_ void *salt_buffer = NULL; + size_t salt_size = 0; + char **i; + int r; + + assert(h); + assert(setup); + assert(user_record_storage(h) == USER_FSCRYPT); + + assert_se(ip = user_record_image_path(h)); + + setup->root_fd = open(ip, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + r = fgetxattr_malloc(setup->root_fd, "trusted.fscrypt_salt", &salt_text); + if (r == -ENODATA) { + salt = h->fscrypt_salt; + salt_size = h->fscrypt_salt_size; + } else if (r < 0) + return log_error_errno(r, "Faile read fscrypt salt extended attribute of %s: %m", ip); + else if (r >= 0) { + r = unbase64mem_full(salt_text, r, true, &salt_buffer, &salt_size); + if (r < 0) + return log_error_errno(r, "fscrypt salt extended attribute on %s is not valid: %m", ip); + + if (h->fscrypt_salt_size > 0 && + memcmp_nn(h->fscrypt_salt, h->fscrypt_salt_size, salt_buffer, salt_size) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "fscrypt salt doesn't match record."); + + free_and_replace(setup->found_fscrypt_salt, salt_buffer); + setup->found_fscrypt_salt_size = salt_size; + salt = setup->found_fscrypt_salt; + } + + if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (errno == ENODATA) + return log_error_errno(errno, "Home directory %s is not encrypted.", ip); + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + return log_error_errno(errno, "Failed to acquire encryption policy of %s: %m", ip); + } + + STRV_FOREACH(i, h->password) { + r = home_upload_fscrypt_key(h, *i, salt, salt_size, policy.master_key_descriptor, NULL); + if (r >= 0) { + uploaded = true; /* This is the key we are looking for and we uploaded it into the + * kernel */ + break; + } + if (r != -ENOANO) + return r; + + /* Try another password if we got ENOANO, i.e. when the password we supplied doesn't + * match the policy of the home directory. */ + } + + if (!uploaded) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords."); + + return 0; +} + +static int home_prepare_cifs( + UserRecord *h, + bool already_activated, + HomeSetup *setup) { + + char **pw; + int r; + + assert(h); + assert(setup); + assert(user_record_storage(h) == USER_CIFS); + + if (already_activated) + setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + else { + bool mounted = false; + + r = unshare_and_mount(NULL, NULL, false); + if (r < 0) + return r; + + STRV_FOREACH(pw, h->password) { + _cleanup_(unlink_and_freep) char *p = NULL; + _cleanup_free_ char *options = NULL; + _cleanup_(fclosep) FILE *f = NULL; + pid_t mount_pid; + int exit_status; + + r = fopen_temporary(NULL, &f, &p); + if (r < 0) + return log_error_errno(r, "Failed to create temporary credentials file: %m"); + + fprintf(f, + "username=%s\n" + "password=%s\n", + user_record_cifs_user_name(h), + *pw); + + if (h->cifs_domain) + fprintf(f, "domain=%s\n", h->cifs_domain); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write temporary credentials file: %m"); + + f = safe_fclose(f); + + if (asprintf(&options, "credentials=%s,uid=" UID_FMT ",forceuid,gid=" UID_FMT ",forcegid,file_mode=0%3o,dir_mode=0%3o", + p, h->uid, h->uid, h->access_mode, h->access_mode) < 0) + return log_oom(); + + r = safe_fork("(mount)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &mount_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execl("/bin/mount", "/bin/mount", "-n", "-t", "cifs", + h->cifs_service, "/run/systemd/user-home-mount", + "-o", options, NULL); + + log_error_errno(errno, "Failed to execute fsck: %m"); + _exit(EXIT_FAILURE); + } + + exit_status = wait_for_terminate_and_check("mount", mount_pid, WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS); + if (exit_status < 0) + return exit_status; + if (exit_status != EXIT_SUCCESS) + return -EPROTO; + + mounted = true; + break; + } + + if (!mounted) + return log_error_errno(ENOKEY, "Failed to mount home directory with supplied password."); + + setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + } + if (setup->root_fd < 0) + return log_error_errno(r, "Failed to open home directory: %m"); + + return 0; +} + +static int home_prepare( + UserRecord *h, + bool already_activated, + HomeSetup *setup, + UserRecord **ret_header_home) { + + int r; + + assert(h); + assert(setup); + assert(!setup->loop); + assert(!setup->crypt_device); + assert(setup->root_fd < 0); + assert(!setup->undo_dm); + assert(!setup->undo_mount); + + /* Makes a home directory accessible (through the root_fd file descriptor, not by path!). */ + + switch (user_record_storage(h)) { + + case USER_LUKS: + return home_prepare_luks(h, already_activated, NULL, setup, ret_header_home); + + case USER_SUBVOLUME: + case USER_DIRECTORY: + r = home_prepare_directory(h, already_activated, setup); + break; + + case USER_FSCRYPT: + r = home_prepare_fscrypt(h, already_activated, setup); + break; + + case USER_CIFS: + r = home_prepare_cifs(h, already_activated, setup); + break; + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } + + if (r < 0) + return r; + + if (ret_header_home) + *ret_header_home = NULL; + + return r; +} + +static int sync_and_statfs(int root_fd, struct statfs *ret) { + assert(root_fd >= 0); + + /* Let's sync this to disk, so that the disk space reported by fstatfs() below is accurate (for file + * systems such as btrfs where this is determined lazily). */ + + if (syncfs(root_fd) < 0) + return log_error_errno(errno, "Failed to synchronize file system: %m"); + + if (ret) + if (fstatfs(root_fd, ret) < 0) + return log_error_errno(errno, "Failed to statfs() file system: %m"); + + log_info("Synchronized disk."); + + return 0; +} + +static void print_size_summary(uint64_t host_size, uint64_t encrypted_size, struct statfs *sfs) { + char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], buffer4[FORMAT_BYTES_MAX]; + + assert(sfs); + + log_info("Image size is %s, file system size is %s, file system payload size is %s, file system free is %s.", + format_bytes(buffer1, sizeof(buffer1), host_size), + format_bytes(buffer2, sizeof(buffer2), encrypted_size), + format_bytes(buffer3, sizeof(buffer3), (uint64_t) sfs->f_blocks * (uint64_t) sfs->f_frsize), + format_bytes(buffer4, sizeof(buffer4), (uint64_t) sfs->f_bfree * (uint64_t) sfs->f_frsize)); +} + +static int read_identity_file(int root_fd, JsonVariant **ret) { + _cleanup_(fclosep) FILE *identity_file = NULL; + _cleanup_close_ int identity_fd = -1; + unsigned line, column; + int r; + + assert(root_fd >= 0); + assert(ret); + + identity_fd = openat(root_fd, ".identity", O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW|O_NONBLOCK); + if (identity_fd < 0) + return log_error_errno(errno, "Failed to open .identity file in home directory: %m"); + + r = fd_verify_regular(identity_fd); + if (r < 0) + return log_error_errno(r, "Embedded identity file is not a regular file, refusing: %m"); + + identity_file = fdopen(identity_fd, "r"); + if (!identity_file) + return log_oom(); + + identity_fd = -1; + + r = json_parse_file(identity_file, ".identity", JSON_PARSE_SENSITIVE, ret, &line, &column); + if (r < 0) + return log_error_errno(r, "[.identity:%u:%u] Failed to parse JSON data: %m", line, column); + + log_info("Read embedded .identity file."); + + return 0; +} + +static int write_identity_file(int root_fd, JsonVariant *v, uid_t uid) { + _cleanup_(json_variant_unrefp) JsonVariant *normalized = NULL; + _cleanup_(fclosep) FILE *identity_file = NULL; + _cleanup_close_ int identity_fd = -1; + _cleanup_free_ char *fn = NULL; + int r; + + assert(root_fd >= 0); + assert(v); + + normalized = json_variant_ref(v); + + r = json_variant_normalize(&normalized); + if (r < 0) + log_warning_errno(r, "Failed to normalize user record, ignoring: %m"); + + r = tempfn_random(".identity", NULL, &fn); + if (r < 0) + return r; + + identity_fd = openat(root_fd, fn, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); + if (identity_fd < 0) + return log_error_errno(errno, "Failed to create .identity file in home directory: %m"); + + identity_file = fdopen(identity_fd, "w"); + if (!identity_file) { + r = log_oom(); + goto fail; + } + + identity_fd = -1; + + json_variant_dump(normalized, JSON_FORMAT_PRETTY, identity_file, NULL); + + r = fflush_and_check(identity_file); + if (r < 0) { + log_error_errno(r, "Failed to write .identity file: %m"); + goto fail; + } + + if (fchown(fileno(identity_file), uid, uid) < 0) { + log_error_errno(r, "Failed to change ownership of identity file: %m"); + goto fail; + } + + if (renameat(root_fd, fn, root_fd, ".identity") < 0) { + r = log_error_errno(errno, "Failed to move identity file into place: %m"); + goto fail; + } + + log_info("Wrote embedded .identity file."); + + return 0; + +fail: + (void) unlinkat(root_fd, fn, 0); + return r; +} + +static int load_embedded_identity( + UserRecord *h, + int root_fd, + UserRecord *header_home, + UserReconcileMode mode, + UserRecord **ret_embedded_home, + UserRecord **ret_new_home) { + + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *intermediate_home = NULL, *new_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(h); + assert(root_fd >= 0); + + r = read_identity_file(root_fd, &v); + if (r < 0) + return r; + + embedded_home = user_record_new(); + if (!embedded_home) + return log_oom(); + + r = user_record_load(embedded_home, v, USER_RECORD_LOAD_EMBEDDED); + if (r < 0) + return r; + + /* Insist that credentials the user supplies alos unlocks any embedded records. */ + r = user_record_test_secret(embedded_home, h); + if (r == -ENXIO) + return log_error_errno(r, "Embedded home user record contains no hashed password, invalid."); + if (r == -ENOKEY) + return log_error_errno(r, "Supplied password does not match password of embedded home user record."); + if (r < 0) + return log_error_errno(r, "Failed to validate password of embedded home user record: %m"); + + /* At this point we have three records to deal with: + * + * · The record we got passed from the host + * · The record included in the LUKS header (only if LUKS is used) + * · The record in the home directory itself (~.identity) + * + * Now we have to reconcile all three, and let the newest one win. */ + + if (header_home) { + /* Note we relax the requirements here. Instead of insisting that the host record is strictly + * newer, let's also be OK if its equally new. If it is, we'll however insist that the + * embedded record must be newer, so that we update at least one of the two. */ + + r = user_record_reconcile(h, header_home, mode == USER_RECONCILE_REQUIRE_NEWER ? USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL : mode, &intermediate_home); + if (r == -EREMCHG) /* this was supposed to be checked earlier already, but let's check this again */ + return log_error_errno(r, "Identity stored on host and in header don't match, refusing."); + if (r == -ESTALE) + return log_error_errno(r, "Embedded identity record is newer than supplied record, refusing."); + if (r < 0) + return log_error_errno(r, "Failed to reconcile host and header identities: %m"); + if (r == USER_RECONCILE_EMBEDDED_WON) + log_info("Reconciling header user identity completed (header version was newer)."); + else if (r == USER_RECONCILE_HOST_WON) { + log_info("Reconciling header user identity completed (host version was newer)."); + + if (mode == USER_RECONCILE_REQUIRE_NEWER) /* Host version is newer than the header + * version, hence we'll update + * something. This means we can relax the + * requirements on the embedded + * identity. */ + mode = USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL; + } else { + assert(r == USER_RECONCILE_IDENTICAL); + log_info("Reconciling user identities completed (host and header version were identical)."); + } + + h = intermediate_home; + } + + r = user_record_reconcile(h, embedded_home, mode, &new_home); + if (r == -EREMCHG) + return log_error_errno(r, "Identity stored on host and in home don't match, refusing."); + if (r == -ESTALE) + return log_error_errno(r, "Embedded identity record is equally new or newer than supplied record, refusing."); + if (r < 0) + return log_error_errno(r, "Failed to reconcile host and embedded identities: %m"); + if (r == USER_RECONCILE_EMBEDDED_WON) + log_info("Reconciling embedded user identity completed (embedded version was newer)."); + else if (r == USER_RECONCILE_HOST_WON) + log_info("Reconciling embedded user identity completed (host version was newer)."); + else { + assert(r == USER_RECONCILE_IDENTICAL); + log_info("Reconciling embedded user identity completed (host and embedded version were identical)."); + } + + if (ret_embedded_home) + *ret_embedded_home = TAKE_PTR(embedded_home); + + if (ret_new_home) + *ret_new_home = TAKE_PTR(new_home); + + return 0; +} + +static int store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home) { + _cleanup_(user_record_unrefp) UserRecord *embedded = NULL; + int r; + + assert(h); + assert(root_fd >= 0); + assert(uid_is_valid(uid)); + + r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &embedded); + if (r < 0) + return log_error_errno(r, "Failed to determine new embedded record: %m"); + + if (old_home && user_record_equal(old_home, embedded)) { + log_debug("Not updating embedded home record."); + return 0; + } + + /* The identity has changed, let's update it in the image */ + r = write_identity_file(root_fd, embedded->json, h->uid); + if (r < 0) + return r; + + return 1; +} + +static uint64_t luks_volume_key_size_convert(struct crypt_device *cd) { + int k; + + assert(cd); + + k = crypt_get_volume_key_size(cd); + if (k <= 0) + return UINT64_MAX; + + return (uint64_t) k; +} + +static const char *file_system_type_fd(int fd) { + struct statfs sfs; + + assert(fd >= 0); + + if (fstatfs(fd, &sfs) < 0) { + log_debug_errno(errno, "Failed to statfs(): %m"); + return NULL; + } + + if (is_fs_type(&sfs, XFS_SB_MAGIC)) + return "xfs"; + if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) + return "ext4"; + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) + return "btrfs"; + + return 0; +} + +static int extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup) { + int r; + + assert(h); + assert(used); + assert(setup); + + r = user_record_add_binding( + h, + user_record_storage(used), + user_record_image_path(used), + setup->found_partition_uuid, + setup->found_luks_uuid, + setup->found_fs_uuid, + setup->crypt_device ? crypt_get_cipher(setup->crypt_device) : NULL, + setup->crypt_device ? crypt_get_cipher_mode(setup->crypt_device) : NULL, + setup->crypt_device ? luks_volume_key_size_convert(setup->crypt_device) : UINT64_MAX, + file_system_type_fd(setup->root_fd), + setup->found_fscrypt_salt, + setup->found_fscrypt_salt_size, + user_record_home_directory(used), + used->uid, + (gid_t) used->uid); + if (r < 0) + return log_error_errno(r, "Failed to update binding in record: %m"); + + return 0; +} + +static int chown_recursive_directory(int root_fd, uid_t uid) { + int r; + + assert(root_fd >= 0); + assert(uid_is_valid(uid)); + + r = fd_chown_recursive(root_fd, uid, (gid_t) uid, 0777); + if (r < 0) + return log_error_errno(r, "Failed to change ownership of files and directories: %m"); + if (r == 0) + log_info("Recursive changing of ownership not necessary, skipped."); + else + log_info("Recursive changing of ownership completed."); + + return 0; +} + +static int move_mount(const char *user_name_and_realm, const char *target) { + _cleanup_free_ char *subdir = NULL; + const char *d; + int r; + + assert(user_name_and_realm); + assert(target); + + if (user_name_and_realm) { + subdir = path_join("/run/systemd/user-home-mount/", user_name_and_realm); + if (!subdir) + return log_oom(); + + d = subdir; + } else + d = "/run/systemd/user-home-mount/"; + + (void) mkdir_p(target, 0700); + + r = mount_verbose(LOG_ERR, d, target, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + return r; + + log_info("Moving to final mount point %s completed.", target); + return 0; +} + +static int home_freshen( + UserRecord *h, + HomeSetup *setup, + UserRecord *header_home, + struct statfs *ret_statfs, + UserRecord **ret_new_home) { + + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; + int r; + + assert(h); + assert(setup); + assert(ret_new_home); + + /* When activating a home directory, does the identity work: loads the identity from the $HOME + * directory, reconciles it with our idea, chown()s everything. */ + + r = load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, &embedded_home, &new_home); + if (r < 0) + return r; + + r = luks_store_header_identity(new_home, setup->crypt_device, setup->volume_key, header_home); + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = chown_recursive_directory(setup->root_fd, h->uid); + if (r < 0) + return r; + + r = sync_and_statfs(setup->root_fd, ret_statfs); + if (r < 0) + return r; + + *ret_new_home = TAKE_PTR(new_home); + return 0; +} + +static int home_activate_luks(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *luks_home_record = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_free_ char *fstype = NULL; + uint64_t host_size, encrypted_size; + const char *hdo, *hd; + struct statfs sfs; + int r; + + assert(h); + assert(user_record_storage(h) == USER_LUKS); + assert(ret_home); + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */ + + r = make_dm_names(h->user_name, &setup.dm_name, &setup.dm_node); + if (r < 0) + return r; + + r = access(setup.dm_node, F_OK); + if (r < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", setup.dm_node); + } else + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", setup.dm_node); + + r = home_prepare_luks(h, false, NULL, &setup, &luks_home_record); + if (r < 0) + return r; + + r = block_get_size_by_fd(setup.loop->fd, &host_size); + if (r < 0) + return log_error_errno(r, "Failed to get loopback block device size: %m"); + + r = block_get_size_by_path(setup.dm_node, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to get LUKS block device size: %m"); + + r = home_freshen(h, &setup, luks_home_record, &sfs, &new_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + r = move_mount(user_record_user_name_and_realm(h), hd); + if (r < 0) + return r; + + setup.undo_mount = false; + + loop_device_relinquish(setup.loop); + + r = dm_deferred_remove(setup.dm_name); + if (r < 0) + log_warning_errno(r, "Failed to relinquish dm device, ignoring: %m"); + + setup.undo_dm = false; + + log_info("Everything completed."); + + print_size_summary(host_size, encrypted_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_activate_directory(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *embedded_json = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + const char *hdo, *hd, *ipo, *ip; + int r; + + assert(h); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)); + assert(ret_home); + + assert_se(ipo = user_record_image_path(h)); + ip = strdupa(ipo); /* copy out, since reconciliation might cause changing of the field */ + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); + + r = home_prepare(h, false, &setup, &header_home); + if (r < 0) + return r; + + r = home_freshen(h, &setup, header_home, NULL, &new_home); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + /* Create mount point to mount over if necessary */ + if (!path_equal(ip, hd)) + (void) mkdir_p(hd, 0700); + + /* Create a mount point (even if the directory is already placed correctly), as a way to indicate + * this mount point is now "activated". Moreover, we want to set per-user + * MS_NOSUID/MS_NOEXEC/MS_NODEV. */ + r = mount_verbose(LOG_ERR, ip, hd, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + r = mount_verbose(LOG_ERR, NULL, hd, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL); + if (r < 0) { + (void) umount_verbose(hd); + return r; + } + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_activate_cifs(UserRecord *h, UserRecord **ret_home) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_free_ char *fstype = NULL; + const char *hdo, *hd; + int r; + + assert(h); + assert(user_record_storage(h) == USER_CIFS); + assert(ret_home); + + if (!h->cifs_service) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing."); + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */ + + r = home_prepare_cifs(h, false, &setup); + if (r < 0) + return r; + + r = home_freshen(h, &setup, NULL, NULL, &new_home); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + r = move_mount(NULL, hd); + if (r < 0) + return r; + + setup.undo_mount = false; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int user_record_validate_hashed_password(UserRecord *h) { + int r; + + assert(h); + + if (strv_isempty(h->password)) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No password provided."); + + r = user_record_test_secret(h, h); + if (r == -ENXIO) { + log_info("Supplied user record contains no hashed passwords, not validating password against it."); + return 0; + } + if (r == -ENOKEY) + return log_error_errno(r, "Passwords not correct or not sufficient to unlock record."); + if (r < 0) + return log_error_errno(r, "Failed to validate password of record: %m"); + + log_info("Provided password unlocks user record."); + + return 0; +} + +static int home_activate(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Activating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "Home directory %s is already mounted, refusing.", user_record_home_directory(h)); + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s is missing, refusing.", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_LUKS: + r = home_activate_luks(h, &new_home); + if (r < 0) + return r; + + break; + + case USER_SUBVOLUME: + case USER_DIRECTORY: + case USER_FSCRYPT: + r = home_activate_directory(h, &new_home); + if (r < 0) + return r; + + break; + + case USER_CIFS: + r = home_activate_cifs(h, &new_home); + if (r < 0) + return r; + + break; + + default: + assert_not_reached("unexpected type"); + } + + /* Note that the returned object might either be a reference to an updated version of the existing + * home object, or a reference to a newly allocated home object. The caller has to be able to deal + * with both, and consider the old object out-of-date. */ + if (user_record_equal(h, new_home)) { + *ret_home = NULL; + return 0; /* no identity change */ + } + + *ret_home = TAKE_PTR(new_home); + return 1; /* identity updated */ +} + +static int home_deactivate(UserRecord *h, bool force) { + bool done = false; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Deactivating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) { + if (umount2(user_record_home_directory(h), UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0)) < 0) + return log_error_errno(errno, "Failed to unmount %s: %m", user_record_home_directory(h)); + + log_info("Unmounting completed."); + done = true; + } else + log_info("Directory %s is already unmounted.", user_record_home_directory(h)); + + if (user_record_storage(h) == USER_LUKS) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + + /* Note that the DM device and loopback device are set to auto-detach, hence strictly + * speaking we don't have to explicitly have to detach them. However, we do that nonetheless + * (in case of the DM device), to avoid races: by explicitly detaching them we know when the + * detaching is complete. We don't bother about the loopback device because unlike the DM + * device it doesn't have a fixed name. */ + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) + log_debug_errno(r, "LUKS device %s is already detached.", dm_name); + else if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + else { + log_info("Discovered used LUKS device %s.", dm_node); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_deactivate(cd, dm_name); + if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) + log_debug_errno(r, "LUKS device %s is already detached.", dm_node); + else if (r < 0) + return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node); + + log_info("LUKS device detaching completed."); + done = true; + } + } + + if (!done) + return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home is not active."); + + log_info("Everything completed."); + return 0; +} + +static int run_mkfs( + const char *node, + const char *fstype, + const char *label, + sd_id128_t uuid, + bool discard) { + + int r; + + assert(node); + assert(fstype); + assert(label); + + r = mkfs_exists(fstype); + if (r < 0) + return log_error_errno(r, "Failed to check if mkfs for file system %s exists: %m", fstype); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Nt mkfs for file system %s installed.", fstype); + + r = safe_fork("(mkfs)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, NULL); + if (r < 0) + return r; + if (r == 0) { + const char *mkfs; + char suuid[37]; + + /* Child */ + + mkfs = strjoina("mkfs.", fstype); + id128_to_uuid_string(uuid, suuid); + + if (streq(fstype, "ext4")) + execlp(mkfs, mkfs, + "-L", label, + "-U", suuid, + "-I", "256", + "-O", "has_journal", + "-m", "0", + "-E", discard ? "lazy_itable_init=1,discard" : "lazy_itable_init=1,nodiscard", + node, NULL); + else if (streq(fstype, "btrfs")) { + if (discard) + execlp(mkfs, mkfs, "-L", label, "-U", suuid, node, NULL); + else + execlp(mkfs, mkfs, "-L", label, "-U", suuid, "--nodiscard", node, NULL); + } else if (streq(fstype, "xfs")) { + const char *j; + + j = strjoina("uuid=", suuid); + if (discard) + execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", node, NULL); + else + execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", "-K", node, NULL); + } else { + log_error("Cannot make file system: %s", fstype); + _exit(EXIT_FAILURE); + } + + log_error_errno(errno, "Failed to execute %s: %m", mkfs); + _exit(EXIT_FAILURE); + } + + return 0; +} + +static int luks_format( + const char *node, + const char *dm_name, + sd_id128_t uuid, + const char *label, + char **passwords, + char **hashed_passwords, + bool discard, + UserRecord *hr, + struct crypt_device **ret) { + + _cleanup_(user_record_unrefp) UserRecord *reduced = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + _cleanup_free_ char *text = NULL; + size_t volume_key_size; + char suuid[37], **pp; + bool added = false; + int slot = 0, r; + + assert(node); + assert(dm_name); + assert(hr); + assert(ret); + + r = crypt_init(&cd, node); + if (r < 0) + return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + /* Normally we'd, just leave volume key generation to libcryptsetup. However, we can't, since we + * can't extract the volume key from the library again, but we need it in order to encrypt the JSON + * record. Hence, let's generate it on our own, so that we can keep track of it. */ + + volume_key_size = user_record_luks_volume_key_size(hr); + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + r = genuine_random_bytes(volume_key, volume_key_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate volume key: %m"); + + /* Increase the metadata space to 4M, the largest LUKS2 supports */ + r = crypt_set_metadata_size(cd, 4096U*1024U, 0); + if (r < 0) + return log_error_errno(r, "Failed to change LUKS2 metadata size: %m"); + + r = crypt_format(cd, + CRYPT_LUKS2, + user_record_luks_cipher(hr), + user_record_luks_cipher_mode(hr), + id128_to_uuid_string(uuid, suuid), + volume_key, + volume_key_size, + &(struct crypt_params_luks2) { + .label = label, + .subsystem = "systemd-home", + .sector_size = 512U, + .pbkdf = &(struct crypt_pbkdf_type) { + .hash = user_record_luks_pbkdf_hash_algorithm(hr), + .type = user_record_luks_pbkdf_type(hr), + .time_ms = user_record_luks_pbkdf_time_cost_usec(hr) / USEC_PER_MSEC, + .max_memory_kb = user_record_luks_pbkdf_memory_cost(hr) / 1024, + .parallel_threads = user_record_luks_pbkdf_parallel_threads(hr), + }, + }); + if (r < 0) + return log_error_errno(r, "Failed to format LUKS image: %m"); + + log_info("LUKS formatting completed."); + + STRV_FOREACH(pp, passwords) { + r = test_password(hashed_passwords, *pp); + if (r < 0) + return log_error_errno(r, "Failed to verify whether password is valid: %m"); + if (r == 0) /* Ignore passwords that are not available as hashed too, it's not supposed to be + * used for disk encryption (might be for smartcard?) */ + continue; + + r = crypt_keyslot_add_by_volume_key( + cd, + slot, + volume_key, + volume_key_size, + *pp, + strlen(*pp)); + if (r < 0) + return log_error_errno(r, "Failed to set up LUKS password for slot %i: %m", slot); + + log_info("Writing password to LUKS keyslot %i completed.", slot); + added = true; + } + + if (!added) + return log_error_errno(r, "No password configured on LUKS, refusing."); + + r = crypt_activate_by_volume_key( + cd, + dm_name, + volume_key, + volume_key_size, + discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0); + if (r < 0) + return log_error_errno(r, "Failed to activate LUKS superblock: %m"); + + log_info("LUKS activation by volume key succeeded."); + + r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &reduced); + if (r < 0) + return log_error_errno(r, "Failed to prepare home record for LUKS: %m"); + + r = format_luks_token_text(cd, reduced, volume_key, &text); + if (r < 0) + return r; + + r = crypt_token_json_set(cd, CRYPT_ANY_TOKEN, text); + if (r < 0) + return log_error_errno(r, "Failed to set LUKS JSON token: %m"); + + log_info("Writing user record as LUKS token completed."); + + if (ret) + *ret = TAKE_PTR(cd); + + return 0; +} + +static int copy_skel(int root_fd, const char *skel) { + int r; + + assert(root_fd >= 0); + + r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", UID_INVALID, GID_INVALID, COPY_MERGE|COPY_REPLACE); + if (r == -ENOENT) { + log_info("Skeleton directory %s missing, ignoring.", skel); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to copy in %s: %m", skel); + + log_info("Copying in %s completed.", skel); + return 0; +} + +static int change_access_mode(int root_fd, mode_t m) { + assert(root_fd >= 0); + + if (fchmod(root_fd, m) < 0) + return log_error_errno(errno, "Failed to change access mode of top-level directory: %m"); + + log_info("Changed top-level directory access mode to 0%o.", m); + return 0; +} + +static int home_populate(UserRecord *h, int dir_fd) { + int r; + + assert(h); + assert(dir_fd >= 0); + + r = copy_skel(dir_fd, user_record_skeleton_directory(h)); + if (r < 0) + return r; + + r = store_embedded_identity(h, dir_fd, h->uid, NULL); + if (r < 0) + return r; + + r = chown_recursive_directory(dir_fd, h->uid); + if (r < 0) + return r; + + r = change_access_mode(dir_fd, user_record_access_mode(h)); + if (r < 0) + return r; + + return 0; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_context*, fdisk_unref_context); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_partition*, fdisk_unref_partition); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_parttype*, fdisk_unref_parttype); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_table*, fdisk_unref_table); + +static int make_partition_table( + int fd, + const char *label, + sd_id128_t uuid, + uint64_t *ret_offset, + uint64_t *ret_size, + sd_id128_t *ret_disk_uuid) { + + _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *p = NULL, *q = NULL; + _cleanup_free_ char *path = NULL, *joined = NULL, *disk_uuid_as_string = NULL; + _cleanup_(fdisk_unref_parttypep) struct fdisk_parttype *t = NULL; + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + uint64_t offset, size; + sd_id128_t disk_uuid; + char uuids[37]; + int r; + + assert(fd >= 0); + assert(label); + assert(ret_offset); + assert(ret_size); + + t = fdisk_new_parttype(); + if (!t) + return log_oom(); + + r = fdisk_parttype_set_typestr(t, "773f91ef-66d4-49b5-bd83-d683bf40ad16"); + if (r < 0) + return log_error_errno(r, "Failed to initialize partition type: %m"); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + r = fdisk_create_disklabel(c, "gpt"); + if (r < 0) + return log_error_errno(r, "Failed to create gpt disk label: %m"); + + p = fdisk_new_partition(); + if (!p) + return log_oom(); + + r = fdisk_partition_set_type(p, t); + if (r < 0) + return log_error_errno(r, "Failed to set partition type: %m"); + + r = fdisk_partition_start_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to place partition at beginning of space: %m"); + + r = fdisk_partition_partno_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to place partition at first free partition index: %m"); + + r = fdisk_partition_end_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to make partition cover all free space: %m"); + + r = fdisk_partition_set_name(p, label); + if (r < 0) + return log_error_errno(r, "Failed to set partition name: %m"); + + r = fdisk_partition_set_uuid(p, id128_to_uuid_string(uuid, uuids)); + if (r < 0) + return log_error_errno(r, "Failed to set partition UUID: %m"); + + r = fdisk_add_partition(c, p, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add partition: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write disk label: %m"); + + r = fdisk_get_disklabel_id(c, &disk_uuid_as_string); + if (r < 0) + return log_error_errno(r, "Failed to determine disk label UUID: %m"); + + r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid); + if (r < 0) + return log_error_errno(r, "Failed to parse disk label UUID: %m"); + + r = fdisk_get_partition(c, 0, &q); + if (r < 0) + return log_error_errno(r, "Failed to read created partition metadata: %m"); + + assert(fdisk_partition_has_start(q)); + offset = fdisk_partition_get_start(q); + if (offset > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition offset too large."); + + assert(fdisk_partition_has_size(q)); + size = fdisk_partition_get_size(q); + if (size > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition size too large."); + + *ret_offset = offset * 512U; + *ret_size = size * 512U; + *ret_disk_uuid = disk_uuid; + + return 0; +} + +static bool supported_fs_size(const char *fstype, uint64_t host_size) { + uint64_t m; + + m = minimal_size_by_fs_name(fstype); + if (m == UINT64_MAX) + return false; + + return host_size >= m; +} + +static int wait_for_devlink(const char *path) { + _cleanup_close_ int inotify_fd = -1; + usec_t until; + int r; + + /* let's wait for a device link to show up in /dev, with a time-out. This is good to do since we + * return a /dev/disk/by-uuid/… link to our callers and they likely want to access it right-away, + * hence let's wait until udev has caught up with our changes, and wait for the symlink to be + * created. */ + + until = usec_add(now(CLOCK_MONOTONIC), 45 * USEC_PER_SEC); + + for (;;) { + _cleanup_free_ char *dn = NULL; + usec_t w; + + if (laccess(path, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", path); + } else + return 0; /* Found it */ + + if (inotify_fd < 0) { + /* We need to wait for the device symlink to show up, let's create an inotify watch for it */ + inotify_fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC); + if (inotify_fd < 0) + return log_error_errno(errno, "Failed to allocate inotify fd: %m"); + } + + dn = dirname_malloc(path); + for (;;) { + if (!dn) + return log_oom(); + + log_info("Watching %s", dn); + + if (inotify_add_watch(inotify_fd, dn, IN_CREATE|IN_MOVED_TO|IN_ONLYDIR|IN_DELETE_SELF|IN_MOVE_SELF) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to add watch on %s: %m", dn); + } else + break; + + if (empty_or_root(dn)) + break; + + dn = dirname_malloc(dn); + } + + w = now(CLOCK_MONOTONIC); + if (w >= until) + return log_error_errno(SYNTHETIC_ERRNO(ETIMEDOUT), "Device link %s still hasn't shown up, giving up.", path); + + r = fd_wait_for_event(inotify_fd, POLLIN, usec_sub_unsigned(until, w)); + if (r < 0) + return log_error_errno(r, "Failed to watch inotify: %m"); + + (void) flush_fd(inotify_fd); + } +} + +static int calculate_disk_size(UserRecord *h, const char *parent_dir, uint64_t *ret) { + char buf[FORMAT_BYTES_MAX]; + struct statfs sfs; + uint64_t m; + + assert(h); + assert(parent_dir); + assert(ret); + + if (h->disk_size != UINT64_MAX) { + *ret = DISK_SIZE_ROUND_DOWN(h->disk_size); + return 0; + } + + if (statfs(parent_dir, &sfs) < 0) + return log_error_errno(errno, "statfs() on %s failed: %m", parent_dir); + + m = sfs.f_bsize * sfs.f_bavail; + + if (h->disk_size_relative == UINT64_MAX) { + + if (m > UINT64_MAX / USER_DISK_SIZE_DEFAULT_PERCENT) + return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Disk size too large."); + + *ret = DISK_SIZE_ROUND_DOWN(m * USER_DISK_SIZE_DEFAULT_PERCENT / 100); + + log_info("Sizing home to %u%% of available disk space, which is %s.", + USER_DISK_SIZE_DEFAULT_PERCENT, + format_bytes(buf, sizeof(buf), *ret)); + } else { + *ret = DISK_SIZE_ROUND_DOWN((uint64_t) ((double) m * (double) h->disk_size_relative / (double) UINT32_MAX)); + + log_info("Sizing home to %" PRIu64 ".%01" PRIu64 "%% of available disk space, which is %s.", + (h->disk_size_relative * 100) / UINT32_MAX, + ((h->disk_size_relative * 1000) / UINT32_MAX) % 10, + format_bytes(buf, sizeof(buf), *ret)); + } + + if (*ret < USER_DISK_SIZE_MIN) + *ret = USER_DISK_SIZE_MIN; + + return 0; +} + +static int home_create_luks(UserRecord *h, UserRecord **ret_home) { + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL, *subdir = NULL, *disk_uuid_path = NULL, *temporary_image_path = NULL; + uint64_t host_size, encrypted_size, partition_offset, partition_size; + bool image_created = false, dm_activated = false, mounted = false; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + sd_id128_t partition_uuid, fs_uuid, luks_uuid, disk_uuid; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_close_ int image_fd = -1, root_fd = -1; + const char *fstype, *ip; + struct statfs sfs; + int r; + + assert(h); + assert(h->storage < 0 || h->storage == USER_LUKS); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + fstype = user_record_file_system_type(h); + if (!supported_fstype(fstype)) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Unsupported file system type: %s", h->file_system_type); + + if (sd_id128_is_null(h->partition_uuid)) { + r = sd_id128_randomize(&partition_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition UUID: %m"); + } else + partition_uuid = h->partition_uuid; + + if (sd_id128_is_null(h->luks_uuid)) { + r = sd_id128_randomize(&luks_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire LUKS UUID: %m"); + } else + luks_uuid = h->luks_uuid; + + if (sd_id128_is_null(h->file_system_uuid)) { + r = sd_id128_randomize(&fs_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire file system UUID: %m"); + } else + fs_uuid = h->file_system_uuid; + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = access(dm_node, F_OK); + if (r < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node); + } else + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", dm_node); + + if (path_startswith(ip, "/dev/")) { + _cleanup_free_ char *sysfs = NULL; + uint64_t block_device_size; + struct stat st; + + /* Let's place the home directory on a real device, i.e. an USB stick or such */ + + image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open device %s: %m", ip); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat device %s: %m", ip); + if (!S_ISBLK(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Device is not a block device, refusing."); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + if (access(sysfs, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check whether %s exists: %m", sysfs); + } else + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Operating on partitions is currently not supported, sorry. Please specify a top-level block device."); + + if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */ + return log_error_errno(errno, "Failed to lock block device %s: %m", ip); + + if (ioctl(image_fd, BLKGETSIZE64, &block_device_size) < 0) + return log_error_errno(errno, "Failed to read block device size: %m"); + + if (h->disk_size == UINT64_MAX) { + + /* If a relative disk size is requested, apply it relative to the block device size */ + if (h->disk_size_relative < UINT32_MAX) + host_size = CLAMP(DISK_SIZE_ROUND_DOWN(block_device_size * h->disk_size_relative / UINT32_MAX), + USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX); + else + host_size = block_device_size; /* Otherwise, take the full device */ + + } else if (host_size > block_device_size) + return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Selected disk size larger than backing block device, refusing."); + else + host_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + + if (!supported_fs_size(fstype, host_size)) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type); + + /* After creation we should reference this partition by its UUID instead of the block + * device. That's preferable since the user might have specified a device node such as + * /dev/sdb to us, which might look very different when replugged. */ + if (asprintf(&disk_uuid_path, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(luks_uuid)) < 0) + return log_oom(); + + if (user_record_luks_discard(h)) { + if (ioctl(image_fd, BLKDISCARD, (uint64_t[]) { 0, block_device_size }) < 0) + log_full_errno(errno == EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to issue full-device BLKDISCARD on device, ignoring: %m"); + else + log_info("Full device discard completed."); + } + } else { + _cleanup_free_ char *parent = NULL; + + parent = dirname_malloc(ip); + if (!parent) + return log_oom(); + + r = mkdir_p(parent, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create parent directory %s: %m", parent); + + r = calculate_disk_size(h, parent, &host_size); + if (r < 0) + return r; + + if (!supported_fs_size(fstype, host_size)) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type); + + r = tempfn_random(ip, "homework", &temporary_image_path); + if (r < 0) + return log_error_errno(r, "Failed to derive temporary file name for %s: %m", ip); + + image_fd = open(temporary_image_path, O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); + if (image_fd < 0) + return log_error_errno(errno, "Failed to create home image %s: %m", temporary_image_path); + + image_created = true; + + r = chattr_fd(image_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL); + if (r < 0) + log_warning_errno(r, "Failed to set file attributes on %s, ignoring: %m", temporary_image_path); + + if (user_record_luks_discard(h)) + r = ftruncate(image_fd, host_size); + else + r = fallocate(image_fd, 0, 0, host_size); + if (r < 0) { + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to allocate home."); + r = -ENOSPC; /* make recognizable */ + goto fail; + } + + r = log_error_errno(errno, "Failed to truncate home image %s: %m", temporary_image_path); + goto fail; + } + + log_info("Allocating image file completed."); + } + + r = make_partition_table( + image_fd, + user_record_user_name_and_realm(h), + partition_uuid, + &partition_offset, + &partition_size, + &disk_uuid); + if (r < 0) + goto fail; + + log_info("Writing of partition table completed."); + + r = loop_device_make_full(image_fd, O_RDWR, partition_offset, partition_size, 0, &loop); + if (r < 0) { + if (r == -ENOENT) { /* this means /dev/loop-control doesn't exist, i.e. we are in a container + * or similar and loopback bock devices are not available, return a + * recognizable error in this case. */ + log_error_errno(r, "Loopback block device support is not available on this system."); + r = -ENOLINK; + goto fail; + } + + log_error_errno(r, "Failed to set up loopback device for %s: %m", temporary_image_path); + goto fail; + } + + r = loop_device_flock(loop, LOCK_EX); /* make sure udev won't read before we are done */ + if (r < 0) { + log_error_errno(r, "Failed to take lock on loop device: %m"); + goto fail; + } + + log_info("Setting up loopback device %s completed.", loop->node ?: ip); + + r = luks_format(loop->node, dm_name, luks_uuid, user_record_user_name_and_realm(h), h->password, h->hashed_password, user_record_luks_discard(h), h, &cd); + if (r < 0) + goto fail; + + dm_activated = true; + + r = block_get_size_by_path(dm_node, &encrypted_size); + if (r < 0) { + log_error_errno(r, "Failed to get encrypted block device size: %m"); + goto fail; + } + + log_info("Setting up LUKS device %s completed.", dm_node); + + r = run_mkfs(dm_node, fstype, user_record_user_name_and_realm(h), fs_uuid, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + log_info("Formatting file system completed."); + + r = unshare_and_mount(dm_node, fstype, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + mounted = true; + + subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h)); + if (!subdir) { + r = log_oom(); + goto fail; + } + + if (mkdir(subdir, 0700) < 0) { + r = log_error_errno(errno, "Failed to create user directory in mounted image file: %m"); + goto fail; + } + + root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(errno, "Failed to open user directory in mounted image file: %m"); + goto fail; + } + + r = home_populate(h, root_fd); + if (r < 0) + goto fail; + + r = sync_and_statfs(root_fd, &sfs); + if (r < 0) + goto fail; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG, &new_home); + if (r < 0) { + log_error_errno(r, "Failed to clone record: %m"); + goto fail; + } + + r = user_record_add_binding( + new_home, + USER_LUKS, + disk_uuid_path ?: ip, + partition_uuid, + luks_uuid, + fs_uuid, + crypt_get_cipher(cd), + crypt_get_cipher_mode(cd), + luks_volume_key_size_convert(cd), + fstype, + NULL, 0, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) { + log_error_errno(r, "Failed to add binding to record: %m"); + goto fail; + } + + root_fd = safe_close(root_fd); + + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + goto fail; + + mounted = false; + + r = crypt_deactivate(cd, dm_name); + if (r < 0) { + log_error_errno(r, "Failed to deactivate LUKS device: %m"); + goto fail; + } + + dm_activated = false; + + loop = loop_device_unref(loop); + + if (disk_uuid_path) + (void) ioctl(image_fd, BLKRRPART, 0); + + /* Let's close the image fd now. If we are operating on a real block device this will release the BSD + * lock that ensures udev doesn't interfere with what we are doing */ + image_fd = safe_close(image_fd); + + if (temporary_image_path) { + if (rename(temporary_image_path, ip) < 0) { + log_error_errno(errno, "Failed to rename image file: %m"); + goto fail; + } + + log_info("Moved image file into place."); + } + + if (disk_uuid_path) + (void) wait_for_devlink(disk_uuid_path); + + log_info("Everything completed."); + + print_size_summary(host_size, encrypted_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 0; + +fail: + /* Let's close all files before we unmount the file system, to avoid EBUSY */ + root_fd = safe_close(root_fd); + + if (mounted) + (void) umount_verbose("/run/systemd/user-home-mount"); + + if (dm_activated) + (void) crypt_deactivate(cd, dm_name); + + loop = loop_device_unref(loop); + + if (image_created) + (void) unlink(temporary_image_path); + + return r; +} + +static int home_update_quota_btrfs(UserRecord *h, const char *path) { + int r; + + assert(h); + assert(path); + + if (h->disk_size == UINT64_MAX) + return 0; + + /* If the user wants quota, enable it */ + r = btrfs_quota_enable(path, true); + if (r == -ENOTTY) + return log_error_errno(r, "No btrfs quota support on subvolume %s.", path); + if (r < 0) + return log_error_errno(r, "Failed to enable btrfs quota support on %s.", path); + + r = btrfs_qgroup_set_limit(path, 0, h->disk_size); + if (r < 0) + return log_error_errno(r, "Faled to set disk quota on subvolume %s: %m", path); + + log_info("Set btrfs quota."); + + return 0; +} + +static int home_update_quota_classic(UserRecord *h, const char *path) { + _cleanup_free_ char *devnode = NULL; + struct dqblk req; + dev_t devno; + int r; + + assert(h); + assert(uid_is_valid(h->uid)); + assert(path); + + if (h->disk_size == UINT64_MAX) + return 0; + + r = get_block_device(path, &devno); + if (r < 0) + return log_error_errno(r, "Failed to determine block device of %s: %m", path); + if (devno == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "File system %s not backed by a block device.", path); + + r = device_path_make_major_minor(S_IFBLK, devno, &devnode); + if (r < 0) + return log_error_errno(r, "Failed to derive block device path for file system %s: %m", path); + + if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) + return log_error_errno(errno, "No UID quota support on %s.", path); + + if (errno != ESRCH) + return log_error_errno(errno, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); + + zero(req); + } else { + /* Shortcut things if everything is set up properly already */ + if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && h->disk_size / QIF_DQBLKSIZE == req.dqb_bhardlimit) { + log_info("Configured quota already matches the intended setting, not updating quota."); + return 0; + } + } + + req.dqb_valid = QIF_BLIMITS; + req.dqb_bsoftlimit = req.dqb_bhardlimit = h->disk_size / QIF_DQBLKSIZE; + + if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) { + if (errno == ESRCH) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "UID quota not available on %s.", path); + + return log_error_errno(errno, "Failed to set disk quota for UID " UID_FMT ": %m", h->uid); + } + + log_info("Updated per-UID quota."); + + return 0; +} + +static int home_update_quota_auto(UserRecord *h, const char *path) { + struct statfs sfs; + int r; + + assert(h); + + if (h->disk_size == UINT64_MAX) + return 0; + + if (!path) { + path = user_record_image_path(h); + if (!path) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home record lacks image path."); + } + + if (statfs(path, &sfs) < 0) + return log_error_errno(errno, "Failed to statfs() file system: %m"); + + if (is_fs_type(&sfs, XFS_SB_MAGIC) || + is_fs_type(&sfs, EXT4_SUPER_MAGIC)) + return home_update_quota_classic(h, path); + + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + + r = btrfs_is_subvol(path); + if (r < 0) + return log_error_errno(errno, "Failed to test if %s is a subvolume: %m", path); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Directory %s is not a subvolume, cannot apply quota.", path); + + return home_update_quota_btrfs(h, path); + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Type of directory %s not known, cannot apply quota.", path); +} + +static int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { + _cleanup_(rm_rf_subvolume_and_freep) char *temporary = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_close_ int root_fd = -1; + _cleanup_free_ char *d = NULL; + const char *ip; + int r; + + assert(h); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME)); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + r = tempfn_random(ip, "homework", &d); + if (r < 0) + return log_error_errno(r, "Failed to allocate temporary directory: %m"); + + (void) mkdir_parents(d, 0755); + + switch (user_record_storage(h)) { + + case USER_SUBVOLUME: + RUN_WITH_UMASK(0077) + r = btrfs_subvol_make(d); + + if (r >= 0) { + log_info("Subvolume created."); + + if (h->disk_size != UINT64_MAX) { + + /* Enable quota for the subvolume we just created. Note we don't check for + * errors here and only log about debug level about this. */ + r = btrfs_quota_enable(d, true); + if (r < 0) + log_debug_errno(r, "Failed to enable quota on %s, ignoring: %m", d); + + r = btrfs_subvol_auto_qgroup(d, 0, false); + if (r < 0) + log_debug_errno(r, "Failed to set up automatic quota group on %s, ignoring: %m", d); + + /* Actually configure the quota. We also ignore errors here, but we do log + * about them loudly, to keep things discoverable even though we don't + * consider lacking quota support in kernel fatal. */ + (void) home_update_quota_btrfs(h, d); + } + + break; + } + if (r != -ENOTTY) + return log_error_errno(r, "Failed to create temporary home directory subvolume %s: %m", d); + + log_info("Creating subvolume %s is not supported, as file system does not support subvolumes. Falling back to regular directory.", d); + _fallthrough_; + + case USER_DIRECTORY: + + if (mkdir(d, 0700) < 0) + return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d); + + (void) home_update_quota_classic(h, d); + break; + + default: + assert_not_reached("unexpected storage"); + } + + temporary = TAKE_PTR(d); /* Needs to be destroyed now */ + + root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + r = home_populate(h, root_fd); + if (r < 0) + return r; + + r = sync_and_statfs(root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + user_record_storage(h), + ip, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, 0, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + if (rename(temporary, ip) < 0) + return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); + + temporary = mfree(temporary); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_create_fscrypt(UserRecord *h, UserRecord **ret_home) { + _cleanup_free_ char *d = NULL, *hex = NULL, *salt_xattr = NULL; + _cleanup_(rm_rf_physical_and_freep) char *temporary = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_close_ int root_fd = -1; + struct fscrypt_policy policy; + uint8_t salt_buffer[64]; + const void *salt = NULL; + size_t salt_size = 0; + const char *ip; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + assert(ret_home); + + if (strv_length(h->password) > 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Record has more than one password which is not supported for fscrypt, refusing."); + + assert_se(ip = user_record_image_path(h)); + + r = tempfn_random(ip, "homework", &d); + if (r < 0) + return log_error_errno(r, "Failed to allocate temporary directory: %m"); + + (void) mkdir_parents(d, 0755); + + if (mkdir(d, 0700) < 0) + return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d); + + temporary = TAKE_PTR(d); /* Needs to be destroyed now */ + + root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + if (ioctl(root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + if (errno != ENODATA) + return log_error_errno(errno, "Failed to get fscrypt policy of directory: %m"); + } else + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", d); + + policy = (struct fscrypt_policy) { + .version = 0, + .contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS, + .filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS, + .flags = FS_POLICY_FLAGS_PAD_32, + }; + + if (h->fscrypt_salt_size > 0) { + salt = h->fscrypt_salt; + salt_size = h->fscrypt_salt_size; + } else { + ssize_t n; + + r = genuine_random_bytes(salt_buffer, sizeof(salt_buffer), RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate salt value: %m"); + + n = base64mem(salt_buffer, sizeof(salt_buffer), &salt_xattr); + if (n < 0) + return log_error_errno(n, "Failed to base64 encode salt value: %m"); + + salt = salt_buffer; + salt_size = sizeof(salt_buffer); + } + + r = home_upload_fscrypt_key(h, h->password[0], salt, salt_size, NULL, policy.master_key_descriptor); + if (r < 0) + return r; + + if (ioctl(root_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) + return log_error_errno(errno, "Failed to set fscrypt policy on directory: %m"); + + log_info("Encryption policy set."); + + (void) home_update_quota_classic(h, temporary); + + r = home_populate(h, root_fd); + if (r < 0) + return r; + + if (fsetxattr(root_fd, "trusted.fscrypt_salt", salt_xattr, strlen(salt_xattr), 0) < 0) + return log_error_errno(errno, "Failed to set salt value extended attribute on %s: %m", ip); + + r = sync_and_statfs(root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + USER_FSCRYPT, + ip, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + salt, salt_size, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + if (rename(temporary, ip) < 0) + return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); + + temporary = mfree(temporary); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_create_cifs(UserRecord *h, UserRecord **ret_home) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_(closedirp) DIR *d = NULL; + int r, copy; + + assert(h); + assert(user_record_storage(h) == USER_CIFS); + assert(ret_home); + + if (!h->cifs_service) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing."); + + if (access("/sbin/mount.cifs", F_OK) < 0) { + if (errno == ENOENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "/sbin/mount.cifs is missing."); + + return log_error_errno(errno, "Unable to detect whether /sbin/mount.cifs exists: %m"); + } + + r = home_prepare_cifs(h, false, &setup); + if (r < 0) + return r; + + copy = fcntl(setup.root_fd, F_DUPFD_CLOEXEC, 3); + if (copy < 0) + return -errno; + + d = fdopendir(copy); + if (!d) { + safe_close(copy); + return -errno; + } + + errno = 0; + if (readdir_no_dot(d)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTEMPTY), "Selected CIFS directory not empty, refusing."); + if (errno != 0) + return log_error_errno(errno, "Failed to detect if CIFS directory is empty: %m"); + + r = home_populate(h, setup.root_fd); + if (r < 0) + return r; + + r = sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + USER_CIFS, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, 0, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_create(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r != USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Home directory %s already exists, refusing.", user_record_home_directory(h)); + + /* When the user didn't specify the storage type to use, fix it to be LUKS -- unless we run in a + * container where loopback devices and LUKS/DM are not available. Note that we typically default to + * the assumption of "classic" storage for most operations. However, if we create a new home, then + * let's user LUKS if nothing is specified. */ + if (h->storage < 0) { + UserStorage new_storage; + + r = detect_container(); + if (r < 0) + return log_error_errno(r, "Failed to determine whether we are in a container: %m"); + if (r > 0) { + new_storage = USER_DIRECTORY; + + r = path_is_fs_type("/home", BTRFS_SUPER_MAGIC); + if (r < 0) + log_debug_errno(r, "Failed to determine file system of /home, ignoring: %m"); + + new_storage = r > 0 ? USER_SUBVOLUME : USER_DIRECTORY; + } else + new_storage = USER_LUKS; + + r = user_record_add_binding( + h, + new_storage, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, 0, + NULL, + UID_INVALID, + GID_INVALID); + if (r < 0) + return log_error_errno(r, "Failed to change storage type to LUKS: %m"); + + if (!h->image_path_auto) { + h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), new_storage == USER_LUKS ? ".home" : ".homedir"); + if (!h->image_path_auto) + return log_oom(); + } + } + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (!IN_SET(r, USER_TEST_ABSENT, USER_TEST_UNDEFINED, USER_TEST_MAYBE)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Image path %s already exists, refusing.", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_LUKS: + r = home_create_luks(h, &new_home); + break; + + case USER_DIRECTORY: + case USER_SUBVOLUME: + r = home_create_directory_or_subvolume(h, &new_home); + break; + + case USER_FSCRYPT: + r = home_create_fscrypt(h, &new_home); + break; + + case USER_CIFS: + r = home_create_cifs(h, &new_home); + break; + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), + "Creating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } + if (r < 0) + return r; + + if (user_record_equal(h, new_home)) { + *ret_home = NULL; + return 0; + } + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_remove(UserRecord *h) { + bool deleted = false; + const char *ip, *hd; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Removing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + hd = user_record_home_directory(h); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Directory %s is still mounted, refusing.", hd); + + assert(hd); + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + + ip = user_record_image_path(h); + + switch (user_record_storage(h)) { + + case USER_LUKS: { + struct stat st; + + assert(ip); + + if (stat(ip, &st) < 0) { + if (errno != -ENOENT) + return log_error_errno(errno, "Failed to stat %s: %m", ip); + + } else { + if (S_ISREG(st.st_mode)) { + if (unlink(ip) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to remove %s: %m", ip); + } else + deleted = true; + + } else if (S_ISBLK(st.st_mode)) + log_info("Not removing file system on block device %s.", ip); + else + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Image file %s is neither block device, nor regular, refusing removal.", ip); + } + + break; + } + + case USER_SUBVOLUME: + case USER_DIRECTORY: + case USER_FSCRYPT: + assert(ip); + + r = rm_rf(ip, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); + if (r < 0) { + if (r != -ENOENT) + return log_warning_errno(r, "Failed to remove %s: %m", ip); + } else + deleted = true; + + /* If the image path and the home directory are the same invalidate the home directory, so + * that we don't remove it anymore */ + if (path_equal(ip, hd)) + hd = NULL; + + break; + + case USER_CIFS: + /* Nothing else to do here: we won't remove remote stuff. */ + log_info("Not removing home directory on remote server."); + break; + + default: + assert_not_reached("unknown storage type"); + } + + if (hd) { + if (rmdir(hd) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to remove %s, ignoring: %m", hd); + } else + deleted = true; + } + + if (deleted) + log_info("Everything completed."); + else { + log_notice("Nothing to remove."); + return -EALREADY; + } + + return 0; +} + +static int home_validate_update(UserRecord *h, HomeSetup *setup) { + bool has_mount = false; + int r; + + assert(h); + assert(setup); + assert(!setup->dm_name); + assert(!setup->dm_node); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + + has_mount = r == USER_TEST_MOUNTED; + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s does not exist", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + case USER_CIFS: + break; + + case USER_LUKS: { + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + bool has_dm = false; + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = access(dm_node, F_OK); + if (r < 0 && errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node); + + has_dm = r >= 0; + + if (has_dm != has_mount) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Home mount incompletely set up."); + + setup->dm_name = TAKE_PTR(dm_name); + setup->dm_node = TAKE_PTR(dm_node); + break; + } + + default: + assert_not_reached("unexpected storage type"); + } + + return has_mount; /* return true if the home record is already active */ +} + +static int home_update(UserRecord *h, UserRecord **ret) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *embedded_json = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &embedded_home, &new_home); + if (r < 0) + return r; + + r = luks_store_header_identity(new_home, setup.crypt_device, setup.volume_key, header_home); + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret = TAKE_PTR(new_home); + return 0; +} + +enum { + CAN_RESIZE_ONLINE, + CAN_RESIZE_OFFLINE, +}; + +static int can_resize_fs(int fd, uint64_t old_size, uint64_t new_size) { + struct statfs sfs; + + assert(fd >= 0); + + /* Filter out bogus requests early */ + if (old_size == 0 || old_size == UINT64_MAX || + new_size == 0 || new_size == UINT64_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid resize parameters."); + + if ((old_size & 511) != 0 || (new_size & 511) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Resize parameters not multiple of 512."); + + if (fstatfs(fd, &sfs) < 0) + return log_error_errno(errno, "Failed to fstatfs() file system: %m"); + + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + + if (new_size < BTRFS_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for btrfs (needs to be 256M at least."); + + /* btrfs can grow and shrink online */ + + } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) { + + if (new_size < XFS_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for xfs (needs to be 14M at least)."); + + /* XFS can grow, but not shrink */ + if (new_size < old_size) + return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Shrinking this type of file system is not supported."); + + } else if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) { + + if (new_size < EXT4_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for ext4 (needs to be 1M at least)."); + + /* ext4 can grow online, and shrink offline */ + if (new_size < old_size) + return CAN_RESIZE_OFFLINE; + + } else + return log_error_errno(SYNTHETIC_ERRNO(ESOCKTNOSUPPORT), "Resizing this type of file system is not supported."); + + return CAN_RESIZE_ONLINE; +} + +static int ext4_offline_resize_fs(HomeSetup *setup, uint64_t new_size, bool discard) { + _cleanup_free_ char *size_str = NULL; + bool re_open = false, re_mount = false; + pid_t resize_pid, fsck_pid; + int r, exit_status; + + assert(setup); + assert(setup->dm_node); + + /* First, unmount the file system */ + if (setup->root_fd >= 0) { + setup->root_fd = safe_close(setup->root_fd); + re_open = true; + } + + if (setup->undo_mount) { + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + return r; + + setup->undo_mount = false; + re_mount = true; + } + + log_info("Temporarary unmounting of file system completed."); + + /* resize2fs requires that the file system is force checked first, do so. */ + r = safe_fork("(e2fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL); + log_error_errno(errno, "Failed to execute e2fsck: %m"); + _exit(EXIT_FAILURE); + } + + exit_status = wait_for_terminate_and_check("e2fsck", fsck_pid, WAIT_LOG_ABNORMAL); + if (exit_status < 0) + return exit_status; + if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) { + log_warning("e2fsck failed with exit status %i.", exit_status); + + if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing."); + + log_warning("Ignoring fsck error."); + } + + log_info("Forced file system check completed."); + + /* We use 512 sectors here, because resize2fs doesn't do byte sizes */ + if (asprintf(&size_str, "%" PRIu64 "s", new_size / 512) < 0) + return log_oom(); + + /* Resize the thing */ + r = safe_fork("(e2resize)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, &resize_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL); + log_error_errno(errno, "Failed to execute resize2fs: %m"); + _exit(EXIT_FAILURE); + } + + log_info("Offline file system resize completed."); + + /* Re-establish mounts and reopen the directory */ + if (re_mount) { + r = mount_node(setup->dm_node, "ext4", discard); + if (r < 0) + return r; + + setup->undo_mount = true; + } + + if (re_open) { + setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to reopen file system: %m"); + } + + log_info("File system mounted again."); + + return 0; +} + +static int prepare_resize_partition( + int fd, + uint64_t partition_offset, + uint64_t old_partition_size, + uint64_t new_partition_size, + sd_id128_t *ret_disk_uuid, + struct fdisk_table **ret_table) { + + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; + _cleanup_free_ char *path = NULL, *disk_uuid_as_string = NULL; + size_t n_partitions, i; + sd_id128_t disk_uuid; + bool found = false; + int r; + + assert(fd >= 0); + assert(ret_disk_uuid); + assert(ret_table); + + assert((partition_offset & 511) == 0); + assert((old_partition_size & 511) == 0); + assert((new_partition_size & 511) == 0); + assert(UINT64_MAX - old_partition_size >= partition_offset); + assert(UINT64_MAX - new_partition_size >= partition_offset); + + if (partition_offset == 0) { + /* If the offset is at the beginning we assume no partition table, let's exit early. */ + log_debug("Not rewriting partition table, operating on naked device."); + *ret_disk_uuid = SD_ID128_NULL; + *ret_table = NULL; + return 0; + } + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(ENOMEDIUM), "Disk has no GPT partition table."); + + r = fdisk_get_disklabel_id(c, &disk_uuid_as_string); + if (r < 0) + return log_error_errno(r, "Failed to acquire disk UUID: %m"); + + r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid); + if (r < 0) + return log_error_errno(r, "Failed parse disk UUID: %m"); + + r = fdisk_get_partitions(c, &t); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition table: %m"); + + n_partitions = fdisk_table_get_nents(t); + for (i = 0; i < n_partitions; i++) { + struct fdisk_partition *p; + + p = fdisk_table_get_partition(t, i); + if (r < 0) + return log_error_errno(r, "Failed to read partition metadata: %m"); + + if (fdisk_partition_is_used(p) <= 0) + continue; + if (fdisk_partition_has_start(p) <= 0 || fdisk_partition_has_size(p) <= 0 || fdisk_partition_has_end(p) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found partition without a size."); + + if (fdisk_partition_get_start(p) == partition_offset / 512U && + fdisk_partition_get_size(p) == old_partition_size / 512U) { + + if (found) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Partition found twice, refusing."); + + /* Found our partition, now patch it */ + r = fdisk_partition_size_explicit(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to enable explicit partition size: %m"); + + r = fdisk_partition_set_size(p, new_partition_size / 512U); + if (r < 0) + return log_error_errno(r, "Failed to change partition size: %m"); + + found = true; + continue; + + } else { + if (fdisk_partition_get_start(p) < partition_offset + new_partition_size / 512U && + fdisk_partition_get_end(p) >= partition_offset / 512) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't extend, conflicting partition found."); + } + } + + if (!found) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Failed to find matching partition to resize."); + + *ret_table = TAKE_PTR(t); + *ret_disk_uuid = disk_uuid; + + return 1; +} + +static int ask_cb(struct fdisk_context *c, struct fdisk_ask *ask, void *userdata) { + char *result; + + assert(c); + + switch (fdisk_ask_get_type(ask)) { + + case FDISK_ASKTYPE_STRING: + result = new(char, 37); + if (!result) + return log_oom(); + + fdisk_ask_string_set_result(ask, id128_to_uuid_string(*(sd_id128_t*) userdata, result)); + break; + + default: + log_debug("Unexpected question from libfdisk, ignoring."); + } + + return 0; +} + +static int apply_resize_partition(int fd, sd_id128_t disk_uuids, struct fdisk_table *t) { + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_free_ void *two_zero_lbas = NULL; + _cleanup_free_ char *path = NULL; + ssize_t n; + int r; + + assert(fd >= 0); + + if (!t) /* no partition table to apply, exit early */ + return 0; + + two_zero_lbas = malloc0(1024U); + if (!two_zero_lbas) + return log_oom(); + + /* libfdisk appears to get confused by the existing PMBR. Let's explicitly flush it out. */ + n = pwrite(fd, two_zero_lbas, 1024U, 0); + if (n < 0) + return log_error_errno(r, "Failed to wipe partition table: %m"); + if (n != 1024) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while whiping partition table."); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + r = fdisk_create_disklabel(c, "gpt"); + if (r < 0) + return log_error_errno(r, "Failed to create GPT disk label: %m"); + + r = fdisk_apply_table(c, t); + if (r < 0) + return log_error_errno(r, "Failed to apply partition table: %m"); + + r = fdisk_set_ask(c, ask_cb, &disk_uuids); + if (r < 0) + return log_error_errno(r, "Failed to set libfdisk query function: %m"); + + r = fdisk_set_disklabel_id(c); + if (r < 0) + return log_error_errno(r, "Failed to change disklabel ID: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write disk label: %m"); + + return 1; +} + +static int home_resize_luks(UserRecord *h, bool already_activated, HomeSetup *setup, UserRecord **ret_home) { + char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], + buffer4[FORMAT_BYTES_MAX], buffer5[FORMAT_BYTES_MAX], buffer6[FORMAT_BYTES_MAX]; + uint64_t old_image_size, new_image_size, old_fs_size, new_fs_size, crypto_offset, new_partition_size; + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *table = NULL; + _cleanup_free_ char *whole_disk = NULL; + _cleanup_close_ int image_fd = -1; + sd_id128_t disk_uuid; + const char *ip, *ipo; + struct statfs sfs; + struct stat st; + int r, resize_type; + + assert(h); + assert(user_record_storage(h) == USER_LUKS); + assert(setup); + assert(ret_home); + + assert_se(ipo = user_record_image_path(h)); + ip = strdupa(ipo); /* copy out since original might change later in home record object */ + + image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open image file %s: %m", ip); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat image file %s: %m", ip); + if (S_ISBLK(st.st_mode)) { + dev_t parent; + + r = block_get_whole_disk(st.st_rdev, &parent); + if (r < 0) + return log_error_errno(r, "Failed to acquire whole block device for %s: %m", ip); + if (r > 0) { + /* If we shall resize a file system on a partition device, then let's figure out the + * whole disk device and operate on that instead, since we need to rewrite the + * partition table to resize the partition. */ + + log_info("Operating on partition device %s, using parent device.", ip); + + r = device_path_make_major_minor(st.st_mode, parent, &whole_disk); + if (r < 0) + return log_error_errno(r, "Failed to derive whole disk path for %s: %m", ip); + + safe_close(image_fd); + + image_fd = open(whole_disk, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open whole block device %s: %m", whole_disk); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat whole block device %s: %m", whole_disk); + if (!S_ISBLK(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Whole block device %s is not actually a block device, refusing.", whole_disk); + } else + log_info("Operating on whole block device %s.", ip); + + if (ioctl(image_fd, BLKGETSIZE64, &old_image_size) < 0) + return log_error_errno(errno, "Failed to determine size of original block device: %m"); + + if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */ + return log_error_errno(errno, "Failed to lock block device %s: %m", ip); + + new_image_size = old_image_size; /* we can't resize physical block devices */ + } else { + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Image file %s is not a block device nor regular: %m", ip); + + old_image_size = st.st_size; + + /* Note an asymetry here: when we operate on loopback files the specified disk size we get we + * apply onto the loopback file as a whole. When we operate on block devices we instead apply + * to the partition itself only. */ + + new_image_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + if (new_image_size == old_image_size) { + log_info("Image size already matching, skipping operation."); + return 0; + } + } + + r = home_prepare_luks(h, already_activated, whole_disk, setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home); + if (r < 0) + return r; + + log_info("offset = %" PRIu64 ", size = %" PRIu64 ", image = %" PRIu64, setup->partition_offset, setup->partition_size, old_image_size); + + if ((UINT64_MAX - setup->partition_offset) < setup->partition_size || + setup->partition_offset + setup->partition_size > old_image_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Old partition doesn't fit in backing storage, refusing."); + + if (S_ISREG(st.st_mode)) { + uint64_t partition_table_extra; + + partition_table_extra = old_image_size - setup->partition_size; + if (new_image_size <= partition_table_extra) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than partition table metadata."); + + new_partition_size = new_image_size - partition_table_extra; + } else { + assert(S_ISBLK(st.st_mode)); + + new_partition_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + if (new_partition_size == setup->partition_size) { + log_info("Partition size already matching, skipping operation."); + return 0; + } + } + + if ((UINT64_MAX - setup->partition_offset) < new_partition_size || + setup->partition_offset + new_partition_size > new_image_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New partition doesn't fit into backing storage, refusing."); + + crypto_offset = crypt_get_data_offset(setup->crypt_device); + if (setup->partition_size / 512U <= crypto_offset) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weird, old crypto payload offset doesn't actually fit in partition size?"); + if (new_partition_size / 512U <= crypto_offset) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than crypto payload offset?"); + + old_fs_size = (setup->partition_size / 512U - crypto_offset) * 512U; + new_fs_size = (new_partition_size / 512U - crypto_offset) * 512U; + + /* Before we start doing anything, let's figure out if we actually can */ + resize_type = can_resize_fs(setup->root_fd, old_fs_size, new_fs_size); + if (resize_type < 0) + return resize_type; + if (resize_type == CAN_RESIZE_OFFLINE && already_activated) + return log_error_errno(SYNTHETIC_ERRNO(ETXTBSY), "File systems of this type can only be resized offline, but is currently online."); + + log_info("Ready to resize image size %s → %s, partition size %s → %s, file system size %s → %s.", + format_bytes(buffer1, sizeof(buffer1), old_image_size), + format_bytes(buffer2, sizeof(buffer2), new_image_size), + format_bytes(buffer3, sizeof(buffer3), setup->partition_size), + format_bytes(buffer4, sizeof(buffer4), new_partition_size), + format_bytes(buffer5, sizeof(buffer5), old_fs_size), + format_bytes(buffer6, sizeof(buffer6), new_fs_size)); + + r = prepare_resize_partition( + image_fd, + setup->partition_offset, + setup->partition_size, + new_partition_size, + &disk_uuid, + &table); + if (r < 0) + return r; + + if (new_fs_size > old_fs_size) { + + if (S_ISREG(st.st_mode)) { + /* Grow file size */ + + if (user_record_luks_discard(h)) + r = ftruncate(image_fd, new_image_size); + else + r = fallocate(image_fd, 0, 0, new_image_size); + if (r < 0) { + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to grow home."); + return -ENOSPC; /* make recognizable */ + } + + return log_error_errno(errno, "Failed to grow image file %s: %m", ip); + } + + log_info("Growing of image file completed."); + } + + /* Make sure loopback device sees the new bigger size */ + r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size); + if (r == -ENOTTY) + log_debug_errno(r, "Device is not a loopback device, not refreshing size."); + else if (r < 0) + return log_error_errno(r, "Failed to refresh loopback device size: %m"); + else + log_info("Refreshing loop device size completed."); + + r = apply_resize_partition(image_fd, disk_uuid, table); + if (r < 0) + return r; + if (r > 0) + log_info("Growing of partition completed."); + + if (ioctl(image_fd, BLKRRPART, 0) < 0) + log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m"); + + /* Tell LUKS about the new bigger size too */ + r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512U); + if (r < 0) + return log_error_errno(r, "Failed to grow LUKS device: %m"); + + log_info("LUKS device growing completed."); + } else { + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + if (S_ISREG(st.st_mode)) { + if (user_record_luks_discard(h)) + /* Before we shrink, let's trim the file system, so that we need less space on disk during the shrinking */ + (void) run_fitrim(setup->root_fd); + else { + /* If discard is off, let's ensure all backing blocks are allocated, so that our resize operation doesn't fail half-way */ + r = run_fallocate(image_fd, &st); + if (r < 0) + return r; + } + } + } + + /* Now resize the file system */ + if (resize_type == CAN_RESIZE_ONLINE) + r = resize_fs(setup->root_fd, new_fs_size); + else + r = ext4_offline_resize_fs(setup, new_fs_size, user_record_luks_discard(h)); + if (r < 0) + return log_error_errno(r, "Failed to resize file system: %m"); + + log_info("File system resizing completed."); + + /* Immediately sync afterwards */ + r = sync_and_statfs(setup->root_fd, NULL); + if (r < 0) + return r; + + if (new_fs_size < old_fs_size) { + + /* Shrink the LUKS device now, matching the new file system size */ + r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512); + if (r < 0) + return log_error_errno(r, "Failed to shrink LUKS device: %m"); + + log_info("LUKS device shrinking completed."); + + if (S_ISREG(st.st_mode)) { + /* Shrink the image file */ + if (ftruncate(image_fd, new_image_size) < 0) + return log_error_errno(errno, "Failed to shrink image file %s: %m", ip); + + log_info("Shrinking of image file completed."); + } + + /* Refresh the loop devices size */ + r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size); + if (r == -ENOTTY) + log_debug_errno(r, "Device is not a loopback device, not refreshing size."); + else if (r < 0) + return log_error_errno(r, "Failed to refresh loopback device size: %m"); + else + log_info("Refreshing loop device size completed."); + + r = apply_resize_partition(image_fd, disk_uuid, table); + if (r < 0) + return r; + if (r > 0) + log_info("Shrinking of partition completed."); + + if (ioctl(image_fd, BLKRRPART, 0) < 0) + log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m"); + } else { + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + } + + r = luks_store_header_identity(new_home, setup->crypt_device, setup->volume_key, header_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, setup); + if (r < 0) + return r; + + if (user_record_luks_discard(h)) + (void) run_fitrim(setup->root_fd); + + r = sync_and_statfs(setup->root_fd, &sfs); + if (r < 0) + return r; + + r = home_setup_undo(setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + print_size_summary(new_image_size, new_fs_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_resize_directory(UserRecord *h, bool already_activated, HomeSetup *setup, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; + int r; + + assert(h); + assert(setup); + assert(ret_home); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)); + + r = home_prepare(h, already_activated, setup, NULL); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home); + if (r < 0) + return r; + + r = home_update_quota_auto(h, NULL); + if (ERRNO_IS_NOT_SUPPORTED(r)) + return -ESOCKTNOSUPPORT; /* make recognizable */ + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, setup); + if (r < 0) + return r; + + r = sync_and_statfs(setup->root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_resize(UserRecord *h, UserRecord **ret) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret); + + if (h->disk_size == UINT64_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing."); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + switch (user_record_storage(h)) { + + case USER_LUKS: + return home_resize_luks(h, already_activated, &setup, ret); + + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + return home_resize_directory(h, already_activated, &setup, ret); + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Resizing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } +} + +static int luks_passwd(struct crypt_device *cd, char **passwords, char **hashed_passwords) { + _cleanup_free_ char **add = NULL; /* Note: do not use strv_free() here! */ + size_t volume_key_size, added = 0, i, max_key_slots; + _cleanup_(erase_and_freep) void *volume_key = NULL; + bool volume_key_loaded = false; + const char *type; + char **pp; + int r; + + if (!cd) + return 0; + + type = crypt_get_type(cd); + if (!type) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine crypto device type."); + + r = crypt_keyslot_max(type); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine number of key slots."); + max_key_slots = r; + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine volume key size."); + volume_key_size = r; + + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + add = new(char*, strv_length(passwords)); + if (!add) + return log_oom(); + + STRV_FOREACH(pp, passwords) { + /* Look if this password is among the hashed passwords. If so, it's a new password to set, + * otherwise an old password to use to unlock */ + + r = test_password(hashed_passwords, *pp); + if (r < 0) + return r; + if (r > 0) + /* This is a new key to add */ + add[added++] = *pp; + + if (!volume_key_loaded) { + /* Try to load the volume key with this */ + r = crypt_volume_key_get(cd, CRYPT_ANY_SLOT, volume_key, &volume_key_size, *pp, strlen(*pp)); + if (r == -EPERM) { + log_debug_errno(r, "Password %zu didn't work.", (size_t) (pp - passwords)); + continue; + } + if (r < 0) + return log_error_errno(r, "Failed to unlock LUKS superblock: %m"); + + log_info("Unlocked LUKS superblock with key %zu.", (size_t) (pp - passwords)); + volume_key_loaded = true; + } + } + + if (added == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Found no new key to set, refusing."); + + if (!volume_key_loaded) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with all keys."); + + for (i = 0; i < max_key_slots; i++) { + r = crypt_keyslot_destroy(cd, i); + if (r < 0 && !IN_SET(r, -ENOENT, -EINVAL)) /* Returns EINVAL or ENOENT if there's no key in this slot already */ + return log_error_errno(r, "Failed to destroy LUKS password: %m"); + + if (i >= added) { + if (r >= 0) + log_info("Destroyed LUKS key slot %zu.", i); + } else { + r = crypt_keyslot_add_by_volume_key( + cd, + i, + volume_key, + volume_key_size, + add[i], + strlen(add[i])); + if (r < 0) + return log_error_errno(r, "Failed to set up LUKS password: %m"); + + log_info("Updated LUKS key slot %zu.", i); + } + } + + return 1; +} + +static int home_passwd(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret_home); + + /* Yes that's right, we cannot change passwords for "fscrypt" directories right now, since ext4 + * doesn't support re-encryption. We could add that by not deriving the master key directly from the + * password, but then we'd need some side storage (xattr?) and would be in the crypto business a + * bit too much for my taste (also home directories would not be self-contained anymore + * then). Alternatively we could do reencrypt in userspace, by setting up a new directory and copying + * things over. But that'd be terribly slow. */ + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Changing password of home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home); + if (r < 0) + return r; + + r = luks_passwd(setup.crypt_device, h->password, h->hashed_password); + if (r < 0) + return r; + + r = luks_store_header_identity(new_home, setup.crypt_device, setup.volume_key, header_home); + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_inspect(UserRecord *h, UserRecord **ret_home) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + bool already_activated = false; + int r; + + assert(h); + assert(ret_home); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, NULL, &new_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_lock(UserRecord *h) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + _cleanup_close_ int root_fd = -1; + const char *p; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (user_record_storage(h) != USER_LUKS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Locking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r != USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home directory of %s is not mounted, can't lock.", h->user_name); + + assert_se(p = user_record_home_directory(h)); + root_fd = open(p, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + log_info("Discovered used LUKS device %s.", dm_node); + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + if (syncfs(root_fd) < 0) /* Snake oil, but let's better be safe than sorry */ + return log_error_errno(errno, "Failed to synchronize file system %s: %m", p); + + root_fd = safe_close(root_fd); + + log_info("File system synchronized."); + + /* Note that we don't invoke FIFREEZE here, it appears libcryptsetup/device-mapper already does that on its own for us */ + + r = crypt_suspend(cd, dm_name); + if (r < 0) { + /* (void) ioctl(root_fd, FITHAW, 0); */ + return log_error_errno(r, "Failed to suspend cryptsetup device: %s: %m", dm_node); + } + + log_info("LUKS device suspended."); + + log_info("Everything completed."); + return 1; +} + +static int home_unlock(UserRecord *h) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + bool resumed = false; + char **pp; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (user_record_storage(h) != USER_LUKS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Unlocking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + /* Note that we don't check if $HOME is actually mounted, since we want to avoid disk accesses on that mount until we have resumed the device. */ + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + log_info("Discovered used LUKS device %s.", dm_node); + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + STRV_FOREACH(pp, h->password) { + r = crypt_resume_by_passphrase(cd, dm_name, CRYPT_ANY_SLOT, *pp, strlen(*pp)); + if (r < 0) + log_debug_errno(r, "Password %zu didn't work for resuming device: %m", (size_t) (pp - h->password)); + else { + log_info("Resumed LUKS device %s.", dm_node); + resumed = true; + break; + } + } + + if (!resumed) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock."); + + log_info("LUKS device resumed."); + + log_info("Everything completed."); + return 1; +} + +static int run(int argc, char *argv[]) { + _cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(fclosep) FILE *opened_file = NULL; + unsigned line = 0, column = 0; + const char *json_path = NULL; + FILE *json_file; + usec_t start; + int r; + + start = now(CLOCK_MONOTONIC); + + log_setup_service(); + + umask(0022); + + if (argc < 2 || argc > 3) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes one or two arguments."); + + if (argc > 2) { + json_path = argv[2]; + + opened_file = fopen(json_path, "re"); + if (!opened_file) + return log_error_errno(errno, "Failed to open %s: %m", json_path); + + json_file = opened_file; + } else { + json_path = ""; + json_file = stdin; + } + + r = json_parse_file(json_file, json_path, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column); + + home = user_record_new(); + if (!home) + return log_oom(); + + r = user_record_load(home, v, USER_RECORD_LOAD_FULL|USER_RECORD_LOG); + if (r < 0) + return r; + + /* Well known return values of these operations, that systemd-homed knows and converts to proper D-Bus errors: + * + * EMSGSIZE → file systems of this type cannnot be shrinked + * ETXTBSY → file systems of this type can only be shrinked offline + * ERANGE → file system size too small + * ENOLINK → system does not support selected storage backend + * EPROTONOSUPPORT → system does not support selected file system + * ENOTTY → operation not support on this storage + * ESOCKTNOSUPPORT → operation not support on this file system + * ENOKEY → password incorrect (or not sufficient) + * EBUSY → file system is currently active + * ENOEXEC → file system is currently not active + * ENOSPC → not enough disk space for operation + */ + + if (streq(argv[1], "activate")) + r = home_activate(home, &new_home); + else if (streq(argv[1], "deactivate")) + r = home_deactivate(home, false); + else if (streq(argv[1], "deactivate-force")) + r = home_deactivate(home, true); + else if (streq(argv[1], "create")) + r = home_create(home, &new_home); + else if (streq(argv[1], "remove")) + r = home_remove(home); + else if (streq(argv[1], "update")) + r = home_update(home, &new_home); + else if (streq(argv[1], "resize")) + r = home_resize(home, &new_home); + else if (streq(argv[1], "passwd")) + r = home_passwd(home, &new_home); + else if (streq(argv[1], "inspect")) + r = home_inspect(home, &new_home); + else if (streq(argv[1], "lock")) + r = home_lock(home); + else if (streq(argv[1], "unlock")) + r = home_unlock(home); + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]); + if (r == -ENOKEY) { + usec_t end, n, d; + + /* Make sure bad passwords replies always take at least 3s, and if longer multiples of 3s, so + * that it's not clear how long we actually needed for our calculations. */ + n = now(CLOCK_MONOTONIC); + assert(n >= start); + + d = usec_sub_unsigned(n, start); + if (d > BAD_PASSWORD_DELAY_USEC) + end = start + DIV_ROUND_UP(d, BAD_PASSWORD_DELAY_USEC) * BAD_PASSWORD_DELAY_USEC; + else + end = start + BAD_PASSWORD_DELAY_USEC; + + if (n < end) + (void) usleep(usec_sub_unsigned(end, n)); + } + if (r < 0) + return r; + + /* We always pass the new record back, regardless if it changed or not. This allows our caller to + * prepare a fresh record, send to us, and only if it works use it without having to keep a local + * copy. */ + if (new_home) + json_variant_dump(new_home->json, JSON_FORMAT_NEWLINE, stdout, NULL); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/meson.build b/src/home/meson.build new file mode 100644 index 0000000000000..d6f46716dbb90 --- /dev/null +++ b/src/home/meson.build @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +systemd_homework_sources = files(''' + home-util.c + home-util.h + homework.c + user-record-util.c + user-record-util.h +'''.split()) + +systemd_homed_sources = files(''' + home-util.c + home-util.h + homed-bus.c + homed-bus.h + homed-home-bus.c + homed-home-bus.h + homed-home.c + homed-home.h + homed-manager-bus.c + homed-manager-bus.h + homed-manager.c + homed-manager.h + homed-operation.c + homed-operation.h + homed-varlink.c + homed-varlink.h + homed.c + pwquality-util.c + pwquality-util.h + user-record-sign.c + user-record-sign.h + user-record-util.c + user-record-util.h +'''.split()) + +if conf.get('ENABLE_HOMED') == 1 + install_data('org.freedesktop.home1.conf', + install_dir : dbuspolicydir) + install_data('org.freedesktop.home1.service', + install_dir : dbussystemservicedir) + install_data('org.freedesktop.home1.policy', + install_dir : polkitpolicydir) +endif diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf new file mode 100644 index 0000000000000..d615501054d74 --- /dev/null +++ b/src/home/org.freedesktop.home1.conf @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy new file mode 100644 index 0000000000000..66ef8e0e9d45c --- /dev/null +++ b/src/home/org.freedesktop.home1.policy @@ -0,0 +1,72 @@ + + + + + + + + The systemd Project + http://www.freedesktop.org/wiki/Software/systemd + + + Create a home + Authentication is required for creating a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Remove a home + Authentication is required for removing a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Check credentials of a home + Authentication is required for checking credentials against a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Update a home + Authentication is required for updating a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Resize a home + Authentication is required for resizing a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Change password of a home + Authentication is required for changing the password of a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + diff --git a/src/home/org.freedesktop.home1.service b/src/home/org.freedesktop.home1.service new file mode 100644 index 0000000000000..cff19b38617df --- /dev/null +++ b/src/home/org.freedesktop.home1.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +[D-BUS Service] +Name=org.freedesktop.home1 +Exec=/bin/false +User=root +SystemdService=dbus-org.freedesktop.home1.service diff --git a/src/home/pwquality-util.c b/src/home/pwquality-util.c new file mode 100644 index 0000000000000..e15203e8dc242 --- /dev/null +++ b/src/home/pwquality-util.c @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#if HAVE_LIBPWQUALITY +/* pwquality.h uses size_t but doesn't include sys/types.h on its own */ +#include +#include +#endif + +#include "bus-common-errors.h" +#include "home-util.h" +#include "pwquality-util.h" +#include "strv.h" + +#if HAVE_LIBPWQUALITY +DEFINE_TRIVIAL_CLEANUP_FUNC(pwquality_settings_t*, pwquality_free_settings); + +static void pwquality_maybe_disable_dictionary( + pwquality_settings_t *pwq) { + + char buf[PWQ_MAX_ERROR_MESSAGE_LEN]; + const char *path; + int r; + + r = pwquality_get_str_value(pwq, PWQ_SETTING_DICT_PATH, &path); + if (r < 0) { + log_warning("Failed to read libpwquality dictionary path, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL)); + return; + } + + // REMOVE THIS AS SOON AS https://github.com/libpwquality/libpwquality/pull/21 IS MERGED AND RELEASED + if (isempty(path)) + path = "/usr/share/cracklib/pw_dict.pwd.gz"; + + if (isempty(path)) { + log_warning("Weird, no dictionary file configured, ignoring."); + return; + } + + if (access(path, F_OK) >= 0) + return; + + if (errno != ENOENT) { + log_warning_errno(errno, "Failed to check if dictionary file %s exists, ignoring: %m", path); + return; + } + + r = pwquality_set_int_value(pwq, PWQ_SETTING_DICT_CHECK, 0); + if (r < 0) { + log_warning("Failed to disable libpwquality dictionary check, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL)); + return; + } +} + +int quality_check_password( + UserRecord *hr, + UserRecord *secret, + sd_bus_error *error) { + + _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL; + char buf[PWQ_MAX_ERROR_MESSAGE_LEN], **pp; + void *auxerror; + int r; + + assert(hr); + assert(secret); + + pwq = pwquality_default_settings(); + if (!pwq) + return log_oom(); + + r = pwquality_read_config(pwq, NULL, &auxerror); + if (r < 0) + log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + pwquality_maybe_disable_dictionary(pwq); + + /* This is a bit more complex than one might think at first. pwquality_check() would like to know the + * old password to make security checks. We support arbitrary numbers of passwords however, hence we + * call the function once for each combination of old and new password. */ + + /* Iterate through all new passwords */ + STRV_FOREACH(pp, secret->password) { + bool called = false; + char **old; + + r = test_password(hr->hashed_password, *pp); + if (r < 0) + return r; + if (r == 0) /* This is an old password as it isn't listed in the hashedPassword field, skip it */ + continue; + + /* Check this password against all old passwords */ + STRV_FOREACH(old, secret->password) { + + if (streq(*pp, *old)) + continue; + + r = test_password(hr->hashed_password, *old); + if (r < 0) + return r; + if (r > 0) /* This is a new password, not suitable as old password */ + continue; + + r = pwquality_check(pwq, *pp, *old, hr->user_name, &auxerror); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + called = true; + } + + if (called) + continue; + + /* If there are no old passwords, let's call pwquality_check() without any. */ + r = pwquality_check(pwq, *pp, NULL, hr->user_name, &auxerror); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + } + + return 0; +} +#else +int quality_check_password(UserRecord *hr, sd_bus_error *error) { + assert(hr); + return 0; +} + +#endif diff --git a/src/home/pwquality-util.h b/src/home/pwquality-util.h new file mode 100644 index 0000000000000..b44150b30568e --- /dev/null +++ b/src/home/pwquality-util.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" +#include "user-record.h" + +int quality_check_password(UserRecord *hr, UserRecord *secret, sd_bus_error *error); diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c new file mode 100644 index 0000000000000..d02c4750b37c4 --- /dev/null +++ b/src/home/user-record-sign.c @@ -0,0 +1,174 @@ +#include + +#include "fd-util.h" +#include "user-record-sign.h" +#include "fileio.h" + +static int user_record_signable_json(UserRecord *ur, char **ret) { + _cleanup_(user_record_unrefp) UserRecord *reduced = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL; + int r; + + assert(ur); + assert(ret); + + r = user_record_clone(ur, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_STRIP_SECRET|USER_RECORD_STRIP_BINDING|USER_RECORD_STRIP_STATUS|USER_RECORD_STRIP_SIGNATURE, &reduced); + if (r < 0) + return r; + + j = json_variant_ref(reduced->json); + + r = json_variant_normalize(&j); + if (r < 0) + return r; + + return json_variant_format(j, 0, ret); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_MD_CTX*, EVP_MD_CTX_free); + +int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *encoded = NULL, *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL; + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + _cleanup_free_ char *text = NULL, *key = NULL; + size_t signature_size = 0, key_size = 0; + _cleanup_free_ void *signature = NULL; + _cleanup_fclose_ FILE *mf = NULL; + int r; + + assert(ur); + assert(private_key); + assert(ret); + + r = user_record_signable_json(ur, &text); + if (r < 0) + return r; + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return -ENOMEM; + + if (EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, private_key) <= 0) + return -EIO; + + /* Request signature size */ + if (EVP_DigestSign(md_ctx, NULL, &signature_size, (uint8_t*) text, strlen(text)) <= 0) + return -EIO; + + signature = malloc(signature_size); + if (!signature) + return -ENOMEM; + + if (EVP_DigestSign(md_ctx, signature, &signature_size, (uint8_t*) text, strlen(text)) <= 0) + return -EIO; + + mf = open_memstream_unlocked(&key, &key_size); + if (!mf) + return -ENOMEM; + + if (PEM_write_PUBKEY(mf, private_key) <= 0) + return -EIO; + + r = fflush_and_check(mf); + if (r < 0) + return r; + + r = json_build(&encoded, JSON_BUILD_ARRAY( + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(signature, signature_size)), + JSON_BUILD_PAIR("key", JSON_BUILD_STRING(key))))); + if (r < 0) + return r; + + v = json_variant_ref(ur->json); + + r = json_variant_set_field(&v, "signature", encoded); + if (r < 0) + return r; + + if (DEBUG_LOGGING) + json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + + signed_ur = user_record_new(); + if (!signed_ur) + return log_oom(); + + r = user_record_load(signed_ur, v, USER_RECORD_LOAD_FULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(signed_ur); + return 0; +} + +int user_record_verify(UserRecord *ur, EVP_PKEY *public_key) { + _cleanup_free_ char *text = NULL, *key = NULL; + unsigned n_good = 0, n_bad = 0; + JsonVariant *array, *e; + int r; + + assert(ur); + assert(public_key); + + array = json_variant_by_key(ur->json, "signature"); + if (!array) + return USER_RECORD_UNSIGNED; + + if (!json_variant_is_array(array)) + return -EINVAL; + + if (json_variant_elements(array) == 0) + return USER_RECORD_UNSIGNED; + + r = user_record_signable_json(ur, &text); + if (r < 0) + return r; + + JSON_VARIANT_ARRAY_FOREACH(e, array) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + _cleanup_free_ void *signature = NULL; + size_t signature_size = 0; + JsonVariant *data; + + if (!json_variant_is_object(e)) + return -EINVAL; + + data = json_variant_by_key(e, "data"); + if (!data) + return -EINVAL; + + r = json_variant_unbase64(data, &signature, &signature_size); + if (r < 0) + return r; + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return -ENOMEM; + + if (EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, public_key) <= 0) + return -EIO; + + if (EVP_DigestVerify(md_ctx, signature, signature_size, (uint8_t*) text, strlen(text)) <= 0) { + n_bad ++; + continue; + } + + n_good ++; + } + + return n_good > 0 ? (n_bad == 0 ? USER_RECORD_SIGNED_EXCLUSIVE : USER_RECORD_SIGNED) : + (n_bad == 0 ? USER_RECORD_UNSIGNED : USER_RECORD_FOREIGN); +} + +int user_record_has_signature(UserRecord *ur) { + JsonVariant *array; + + array = json_variant_by_key(ur->json, "signature"); + if (!array) + return false; + + if (!json_variant_is_array(array)) + return -EINVAL; + + return json_variant_elements(array) > 0; +} diff --git a/src/home/user-record-sign.h b/src/home/user-record-sign.h new file mode 100644 index 0000000000000..f045c8837bced --- /dev/null +++ b/src/home/user-record-sign.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "user-record.h" + +int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret); + +enum { + USER_RECORD_UNSIGNED, /* user record has no signature */ + USER_RECORD_SIGNED_EXCLUSIVE, /* user record has only a signature by our own key */ + USER_RECORD_SIGNED, /* user record is signed by us, but by others too */ + USER_RECORD_FOREIGN, /* user record is not signed by us, but by others */ +}; + +int user_record_verify(UserRecord *ur, EVP_PKEY *public_key); + +int user_record_has_signature(UserRecord *ur); diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c new file mode 100644 index 0000000000000..10adee848f6b9 --- /dev/null +++ b/src/home/user-record-util.c @@ -0,0 +1,1007 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_CRYPT_H +#include +#endif + +#include "errno-util.h" +#include "home-util.h" +#include "id128-util.h" +#include "mountpoint-util.h" +#include "path-util.h" +#include "stat-util.h" +#include "user-record-util.h" +#include "user-util.h" + +int user_record_synthesize( + UserRecord *h, + const char *user_name, + const char *realm, + const char *image_path, + UserStorage storage, + uid_t uid, + gid_t gid) { + + _cleanup_free_ char *hd = NULL, *un = NULL, *ip = NULL, *rr = NULL, *user_name_and_realm = NULL; + char smid[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(h); + assert(user_name); + assert(image_path); + assert(IN_SET(storage, USER_LUKS, USER_SUBVOLUME, USER_FSCRYPT, USER_DIRECTORY)); + assert(uid_is_valid(uid)); + assert(gid_is_valid(gid)); + + /* Fill in a home record from just a username and an image path. */ + + if (h->json) + return -EBUSY; + + if (!suitable_user_name(user_name)) + return -EINVAL; + + if (realm) { + r = suitable_realm(realm); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + if (!suitable_image_path(image_path)) + return -EINVAL; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + un = strdup(user_name); + if (!un) + return -ENOMEM; + + if (realm) { + rr = strdup(realm); + if (!rr) + return -ENOMEM; + + user_name_and_realm = strjoin(user_name, "@", realm); + if (!user_name_and_realm) + return -ENOMEM; + } + + ip = strdup(image_path); + if (!ip) + return -ENOMEM; + + hd = path_join("/home/", user_name); + if (!hd) + return -ENOMEM; + + r = json_build(&h->json, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)), + JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(realm)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("regular")), + JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("imagePath", JSON_BUILD_STRING(image_path)), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(hd)), + JSON_BUILD_PAIR("storage", JSON_BUILD_STRING(user_storage_to_string(storage))), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)))))))); + if (r < 0) + return r; + + free_and_replace(h->user_name, un); + free_and_replace(h->realm, rr); + free_and_replace(h->user_name_and_realm_auto, user_name_and_realm); + free_and_replace(h->image_path, ip); + free_and_replace(h->home_directory, hd); + h->storage = storage; + h->uid = uid; + + h->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; + return 0; +} + +int group_record_synthesize(GroupRecord *g, UserRecord *h) { + _cleanup_free_ char *un = NULL, *rr = NULL, *group_name_and_realm = NULL; + char smid[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(g); + assert(h); + + if (g->json) + return -EBUSY; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + un = strdup(h->user_name); + if (!un) + return -ENOMEM; + + if (h->realm) { + rr = strdup(h->realm); + if (!rr) + return -ENOMEM; + + group_name_and_realm = strjoin(un, "@", rr); + if (!group_name_and_realm) + return -ENOMEM; + } + + r = json_build(&g->json, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(un)), + JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(rr)), + JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(user_record_gid(h))))))), + JSON_BUILD_PAIR_CONDITION(h->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(user_record_disposition(h)))), + JSON_BUILD_PAIR("status", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")))))))); + if (r < 0) + return r; + + free_and_replace(g->group_name, un); + free_and_replace(g->realm, rr); + free_and_replace(g->group_name_and_realm_auto, group_name_and_realm); + g->gid = user_record_gid(h); + g->disposition = h->disposition; + + g->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; + return 0; +} + +int user_record_reconcile( + UserRecord *host, + UserRecord *embedded, + UserReconcileMode mode, + UserRecord **ret) { + + int r, result; + + /* Reconciles the identity record stored on the host with the one embedded in a $HOME + * directory. Returns the following error codes: + * + * -EINVAL: one of the records not valid + * -REMCHG: identity records are not about the same user + * -ESTALE: embedded identity record is equally new or newer than supplied record + * + * Return the new record to use, which is either the the embedded record updated with the host + * binding or the host record. In both cases the secret data is stripped. */ + + assert(host); + assert(embedded); + + /* Make sure both records are initialized */ + if (!host->json || !embedded->json) + return -EINVAL; + + /* Ensure these records actually contain user data */ + if (!(embedded->mask & host->mask & USER_RECORD_REGULAR)) + return -EINVAL; + + /* Make sure the user name and realm matches */ + if (!user_record_compatible(host, embedded)) + return -EREMCHG; + + /* Embedded identities may not contain secrets or binding info*/ + if ((embedded->mask & (USER_RECORD_SECRET|USER_RECORD_BINDING)) != 0) + return -EINVAL; + + /* The embedded record checked out, let's now figure out which of the two identities we'll consider + * in effect from now on. We do this by checking the last change timestamp, and in doubt always let + * the embedded data win. */ + if (host->last_change_usec != UINT64_MAX && + (embedded->last_change_usec == UINT64_MAX || host->last_change_usec > embedded->last_change_usec)) + + /* The host version is definitely newer, either because it has a version at all and the + * embedded version doesn't or because it is numerically newer. */ + result = USER_RECONCILE_HOST_WON; + + else if (host->last_change_usec == embedded->last_change_usec) { + + /* The nominal version number of the host and the embedded identity is the same. If so, let's + * verify that, and tell the caller if we are ignoring embedded data. */ + + r = user_record_masked_equal(host, embedded, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE); + if (r < 0) + return r; + if (r > 0) { + if (mode == USER_RECONCILE_REQUIRE_NEWER) + return -ESTALE; + + result = USER_RECONCILE_IDENTICAL; + } else + result = USER_RECONCILE_HOST_WON; + } else { + _cleanup_(json_variant_unrefp) JsonVariant *extended = NULL; + _cleanup_(user_record_unrefp) UserRecord *merged = NULL; + JsonVariant *e; + + /* The embedded version is newer */ + + if (mode == USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL) + return -ESTALE; + + /* Copy in the binding data */ + extended = json_variant_ref(embedded->json); + + e = json_variant_by_key(host->json, "binding"); + if (e) { + r = json_variant_set_field(&extended, "binding", e); + if (r < 0) + return r; + } + + merged = user_record_new(); + if (!merged) + return -ENOMEM; + + r = user_record_load(merged, extended, USER_RECORD_LOAD_MASK_SECRET); + if (r < 0) + return r; + + *ret = TAKE_PTR(merged); + return USER_RECONCILE_EMBEDDED_WON; /* update */ + } + + /* Strip out secrets */ + r = user_record_clone(host, USER_RECORD_LOAD_MASK_SECRET, ret); + if (r < 0) + return r; + + return result; +} + +int user_record_add_binding( + UserRecord *h, + UserStorage storage, + const char *image_path, + sd_id128_t partition_uuid, + sd_id128_t luks_uuid, + sd_id128_t fs_uuid, + const char *luks_cipher, + const char *luks_cipher_mode, + uint64_t luks_volume_key_size, + const char *file_system_type, + const void *fscrypt_salt, + size_t fscrypt_salt_size, + const char *home_directory, + uid_t uid, + gid_t gid) { + + _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL; + char smid[SD_ID128_STRING_MAX], partition_uuids[37], luks_uuids[37], fs_uuids[37]; + _cleanup_free_ char *ip = NULL, *hd = NULL; + _cleanup_free_ void *fs = NULL; + sd_id128_t mid; + int r; + + assert(h); + assert(fscrypt_salt_size == 0 || fscrypt_salt); + + if (!h->json) + return -EUNATCH; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + sd_id128_to_string(mid, smid); + + if (image_path) { + ip = strdup(image_path); + if (!ip) + return -ENOMEM; + } + + if (home_directory) { + hd = strdup(home_directory); + if (!hd) + return -ENOMEM; + } + + if (fscrypt_salt_size > 0) { + fs = memdup(fscrypt_salt, fscrypt_salt_size); + if (!fs) + return -ENOMEM; + } + + r = json_build(&new_binding_entry, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUUID", JSON_BUILD_STRING(id128_to_uuid_string(partition_uuid, partition_uuids))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUUID", JSON_BUILD_STRING(id128_to_uuid_string(luks_uuid, luks_uuids))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUUID", JSON_BUILD_STRING(id128_to_uuid_string(fs_uuid, fs_uuids))), + JSON_BUILD_PAIR_CONDITION(!!luks_cipher, "luksCipher", JSON_BUILD_STRING(luks_cipher)), + JSON_BUILD_PAIR_CONDITION(!!luks_cipher_mode, "luksCipherMode", JSON_BUILD_STRING(luks_cipher_mode)), + JSON_BUILD_PAIR_CONDITION(luks_volume_key_size != UINT64_MAX, "luksVolumeKeySize", JSON_BUILD_UNSIGNED(luks_volume_key_size)), + JSON_BUILD_PAIR_CONDITION(!!file_system_type, "fileSystemType", JSON_BUILD_STRING(file_system_type)), + JSON_BUILD_PAIR_CONDITION(fscrypt_salt_size > 0, "fscryptSalt", JSON_BUILD_BASE64(fscrypt_salt, fscrypt_salt_size)), + JSON_BUILD_PAIR_CONDITION(!!home_directory, "homeDirectory", JSON_BUILD_STRING(home_directory)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR_CONDITION(gid_is_valid(gid), "gid", JSON_BUILD_UNSIGNED(gid)), + JSON_BUILD_PAIR_CONDITION(storage >= 0, "storage", JSON_BUILD_STRING(user_storage_to_string(storage))))); + if (r < 0) + return r; + + binding = json_variant_ref(json_variant_by_key(h->json, "binding")); + if (binding) { + _cleanup_(json_variant_unrefp) JsonVariant *be = NULL; + + /* Merge the new entry with an old one, if that exists */ + be = json_variant_ref(json_variant_by_key(binding, smid)); + if (be) { + r = json_variant_merge(&be, new_binding_entry); + if (r < 0) + return r; + + json_variant_unref(new_binding_entry); + new_binding_entry = TAKE_PTR(be); + } + } + + r = json_variant_set_field(&binding, smid, new_binding_entry); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "binding", binding); + if (r < 0) + return r; + + if (storage >= 0) + h->storage = storage; + + if (ip) + free_and_replace(h->image_path, ip); + + if (!sd_id128_is_null(partition_uuid)) + h->partition_uuid = partition_uuid; + + if (!sd_id128_is_null(luks_uuid)) + h->luks_uuid = luks_uuid; + + if (!sd_id128_is_null(fs_uuid)) + h->file_system_uuid = fs_uuid; + + if (fscrypt_salt_size > 0) { + free_and_replace(h->fscrypt_salt, fs); + h->fscrypt_salt_size = fscrypt_salt_size; + } + + if (hd) + free_and_replace(h->home_directory, hd); + + if (uid_is_valid(uid)) + h->uid = uid; + + h->mask |= USER_RECORD_BINDING; + return 1; +} + +int user_record_test_home_directory(UserRecord *h) { + const char *hd; + int r; + + assert(h); + + /* Returns one of USER_TEST_ABSENT, USER_TEST_MOUNTED, USER_TEST_EXISTS on success */ + + hd = user_record_home_directory(h); + if (!hd) + return -ENXIO; + + r = is_dir(hd, false); + if (r == -ENOENT) + return USER_TEST_ABSENT; + if (r < 0) + return r; + if (r == 0) + return -ENOTDIR; + + r = path_is_mount_point(hd, NULL, 0); + if (r < 0) + return r; + if (r > 0) + return USER_TEST_MOUNTED; + + /* If the image path and the home directory are identical, then it's OK if the directory is + * populated. */ + if (IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) { + const char *ip; + + ip = user_record_image_path(h); + if (ip && path_equal(ip, hd)) + return USER_TEST_EXISTS; + } + + /* Otherwise it's not OK */ + r = dir_is_empty(hd); + if (r < 0) + return r; + if (r == 0) + return -EBUSY; + + return USER_TEST_EXISTS; +} + +int user_record_test_home_directory_and_warn(UserRecord *h) { + int r; + + assert(h); + + r = user_record_test_home_directory(h); + if (r == -ENXIO) + return log_error_errno(r, "User record lacks home directory, refusing."); + if (r == -ENOTDIR) + return log_error_errno(r, "Home directory %s is not a directory, refusing.", user_record_home_directory(h)); + if (r == -EBUSY) + return log_error_errno(r, "Home directory %s exists, is not mounted but populated, refusing.", user_record_home_directory(h)); + if (r < 0) + return log_error_errno(r, "Failed to test whether the home directory %s exists: %m", user_record_home_directory(h)); + + return r; +} + +int user_record_test_image_path(UserRecord *h) { + const char *ip; + struct stat st; + + assert(h); + + if (user_record_storage(h) == USER_CIFS) + return USER_TEST_UNDEFINED; + + ip = user_record_image_path(h); + if (!ip) + return -ENXIO; + + if (stat(ip, &st) < 0) { + if (errno == ENOENT) + return USER_TEST_ABSENT; + + return -errno; + } + + switch (user_record_storage(h)) { + + case USER_LUKS: + if (S_ISREG(st.st_mode)) + return USER_TEST_EXISTS; + if (S_ISBLK(st.st_mode)) { + /* For block devices we can't really be sure if the device referenced actually is the + * fs we look for or some other file system (think: what does /dev/sdb1 refer + * to?). Hence, let's return USER_TEST_MAYBE as an ambigious return value for these + * case, except if the device path used is one of the paths that is based on a + * filesystem or partition UUID or label, because in those cases we can be sure we + * are referring to the right device. */ + + if (PATH_STARTSWITH_SET(ip, + "/dev/disk/by-uuid/", + "/dev/disk/by-partuuid/", + "/dev/disk/by-partlabel/", + "/dev/disk/by-label/")) + return USER_TEST_EXISTS; + + return USER_TEST_MAYBE; + } + + return -EBADFD; + + case USER_CLASSIC: + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + if (S_ISDIR(st.st_mode)) + return USER_TEST_EXISTS; + + return -ENOTDIR; + + default: + assert_not_reached("Unexpected record type"); + } +} + +int user_record_test_image_path_and_warn(UserRecord *h) { + int r; + + assert(h); + + r = user_record_test_image_path(h); + if (r == -ENXIO) + return log_error_errno(r, "User record lacks image path, refusing."); + if (r == -EBADFD) + return log_error_errno(r, "Image path %s is not a regular file or block device, refusing.", user_record_image_path(h)); + if (r == -ENOTDIR) + return log_error_errno(r, "Image path %s is not a directory, refusing.", user_record_image_path(h)); + if (r < 0) + return log_error_errno(r, "Failed to test whether image path %s exists: %m", user_record_image_path(h)); + + return r; +} + +int user_record_test_secret(UserRecord *h, UserRecord *secret) { + char **i; + int r; + + assert(h); + + /* Checks whether any of the specified passwords matches any of the hashed passwords of the entry */ + + if (strv_isempty(h->hashed_password)) + return -ENXIO; + + STRV_FOREACH(i, secret->password) { + r = test_password(h->hashed_password, *i); + if (r < 0) + return r; + if (r > 0) + return 0; + } + + return -ENOKEY; +} + +int user_record_set_disk_size(UserRecord *h, uint64_t disk_size) { + _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL; + _cleanup_free_ JsonVariant **array = NULL; + char smid[SD_ID128_STRING_MAX]; + size_t idx = SIZE_MAX, n; + JsonVariant *per_machine; + sd_id128_t mid; + int r; + + assert(h); + + if (!h->json) + return -EUNATCH; + + if (disk_size < USER_DISK_SIZE_MIN || disk_size > USER_DISK_SIZE_MAX) + return -ERANGE; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + sd_id128_to_string(mid, smid); + + r = json_variant_new_string(&midv, smid); + if (r < 0) + return r; + + r = json_variant_new_array(&midav, (JsonVariant*[]) { midv }, 1); + if (r < 0) + return r; + + per_machine = json_variant_by_key(h->json, "perMachine"); + if (per_machine) { + size_t i; + + if (!json_variant_is_array(per_machine)) + return -EINVAL; + + n = json_variant_elements(per_machine); + + array = new(JsonVariant*, n + 1); + if (!array) + return -ENOMEM; + + for (i = 0; i < n; i++) { + JsonVariant *m; + + array[i] = json_variant_by_index(per_machine, i); + + if (!json_variant_is_object(array[i])) + return -EINVAL; + + m = json_variant_by_key(array[i], "matchMachineId"); + if (!m) { + /* No machineId field? Let's ignore this, but invalidate what we found so far */ + idx = SIZE_MAX; + continue; + } + + if (json_variant_equal(m, midv) || + json_variant_equal(m, midav)) { + /* Matches exactly what we are looking for. Let's use this */ + idx = i; + continue; + } + + r = per_machine_id_match(m, JSON_PERMISSIVE); + if (r < 0) + return r; + if (r > 0) + /* Also matches what we are looking for, but with a broader match. In this + * case let's ignore this entry, and add a new specific one to the end. */ + idx = SIZE_MAX; + } + + if (idx == SIZE_MAX) + idx = n++; /* Nothing suitable found, place new entry at end */ + else + ne = json_variant_ref(array[idx]); + + } else { + array = new(JsonVariant*, 1); + if (!array) + return -ENOMEM; + + idx = 0; + n = 1; + } + + if (!ne) { + r = json_variant_set_field(&ne, "matchMachineId", midav); + if (r < 0) + return r; + } + + r = json_variant_set_field_unsigned(&ne, "diskSize", disk_size); + if (r < 0) + return r; + + assert(idx < n); + array[idx] = ne; + + r = json_variant_new_array(&new_per_machine, array, n); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "perMachine", new_per_machine); + if (r < 0) + return r; + + h->disk_size = disk_size; + h->mask |= USER_RECORD_PER_MACHINE; + return 0; +} + +int user_record_update_last_changed(UserRecord *h, bool with_password) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + usec_t n; + int r; + + assert(h); + + if (!h->json) + return -EUNATCH; + + n = now(CLOCK_REALTIME); + + /* refuse downgrading */ + if (h->last_change_usec != UINT64_MAX && h->last_change_usec >= n) + return -ECHRNG; + if (h->last_password_change_usec != UINT64_MAX && h->last_password_change_usec >= n) + return -ECHRNG; + + v = json_variant_ref(h->json); + + r = json_variant_set_field_unsigned(&v, "lastChangeUSec", n); + if (r < 0) + return r; + + if (with_password) { + r = json_variant_set_field_unsigned(&v, "lastPasswordChangeUSec", n); + if (r < 0) + return r; + + h->last_password_change_usec = n; + } + + h->last_change_usec = n; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->mask |= USER_RECORD_REGULAR; + return 0; +} + +int user_record_make_hashed_password(UserRecord *h, UserRecord *secret) { + _cleanup_(json_variant_unrefp) JsonVariant *new_array = NULL, *priv = NULL; + _cleanup_strv_free_ char **np = NULL; + char **i; + int r; + + assert(h); + assert(secret); + + /* Initializes the hashed password list from the specified plaintext passwords */ + + STRV_FOREACH(i, secret->password) { + _cleanup_free_ char *salt = NULL; + struct crypt_data cd = {}; + char *k; + + r = make_salt(&salt); + if (r < 0) + return r; + + errno = 0; + k = crypt_r(*i, salt, &cd); + if (!k) + return errno_or_else(EINVAL); + + r = strv_extend(&np, k); + if (r < 0) + return r; + } + + r = json_variant_new_array_strv(&new_array, np); + if (r < 0) + return r; + + priv = json_variant_ref(json_variant_by_key(h->json, "privileged")); + r = json_variant_set_field(&priv, "hashedPassword", new_array); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "privileged", priv); + if (r < 0) + return r; + + strv_free(h->hashed_password); + h->hashed_password = TAKE_PTR(np); + + h->mask |= USER_RECORD_PRIVILEGED; + return 0; +} + +int user_record_set_password(UserRecord *h, char **password, bool prepend) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; + _cleanup_(strv_free_erasep) char **e = NULL; + int r; + + assert(h); + + if (prepend) { + e = strv_copy(password); + if (!e) + return -ENOMEM; + + r = strv_extend_strv(&e, h->password, true); + if (r < 0) + return r; + + if (strv_equal(h->password, e)) + return 0; + + } else { + if (strv_equal(h->password, password)) + return 0; + + e = strv_copy(password); + if (!e) + return -ENOMEM; + } + + r = json_variant_new_array_strv(&l, e); + if (r < 0) + return r; + + json_variant_sensitive(l); + + w = json_variant_ref(json_variant_by_key(h->json, "secret")); + r = json_variant_set_field(&w, "password", l); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "secret", w); + if (r < 0) + return r; + + strv_free_and_replace(h->password, e); + + h->mask |= USER_RECORD_SECRET; + return 0; +} + +int user_record_merge_secret(UserRecord *h, UserRecord *secret) { + assert(h); + + /* Merges the secrets from 'secret' into 'h'. */ + + return user_record_set_password(h, secret->password, true); +} + +int user_record_good_authentication(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + uint64_t counter, usec; + sd_id128_t mid; + int r; + + assert(h); + + switch (h->good_authentication_counter) { + case UINT64_MAX: + counter = 1; + break; + case UINT64_MAX-1: + counter = h->good_authentication_counter; /* saturate */ + break; + default: + counter = h->good_authentication_counter + 1; + break; + } + + usec = now(CLOCK_REALTIME); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "goodAuthenticationCounter", counter); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "lastGoodAuthenticationUSec", usec); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->good_authentication_counter = counter; + h->last_good_authentication_usec = usec; + + h->mask |= USER_RECORD_STATUS; + return 0; +} + +int user_record_bad_authentication(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + uint64_t counter, usec; + sd_id128_t mid; + int r; + + assert(h); + + switch (h->bad_authentication_counter) { + case UINT64_MAX: + counter = 1; + break; + case UINT64_MAX-1: + counter = h->bad_authentication_counter; /* saturate */ + break; + default: + counter = h->bad_authentication_counter + 1; + break; + } + + usec = now(CLOCK_REALTIME); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "badAuthenticationCounter", counter); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "lastBadAuthenticationUSec", usec); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->bad_authentication_counter = counter; + h->last_bad_authentication_usec = usec; + + h->mask |= USER_RECORD_STATUS; + return 0; +} + +int user_record_ratelimit(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + usec_t usec, new_ratelimit_begin_usec, new_ratelimit_count; + char buf[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(h); + + usec = now(CLOCK_REALTIME); + + if (h->ratelimit_begin_usec != UINT64_MAX && h->ratelimit_begin_usec > usec) + /* Hmm, time is running backwards? Say no! */ + return 0; + else if (h->ratelimit_begin_usec == UINT64_MAX || + usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)) <= usec) { + /* Fresh start */ + new_ratelimit_begin_usec = usec; + new_ratelimit_count = 1; + } else if (h->ratelimit_count < user_record_ratelimit_burst(h)) { + /* Count up */ + new_ratelimit_begin_usec = h->ratelimit_begin_usec; + new_ratelimit_count = h->ratelimit_count + 1; + } else + /* Limit hit */ + return 0; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "rateLimitBeginUSec", new_ratelimit_begin_usec); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "rateLimitCount", new_ratelimit_count); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->ratelimit_begin_usec = new_ratelimit_begin_usec; + h->ratelimit_count = new_ratelimit_count; + + h->mask |= USER_RECORD_STATUS; + return 1; +} + +int user_record_is_supported(UserRecord *hr, sd_bus_error *error) { + assert(hr); + + if (hr->disposition >= 0 && hr->disposition != USER_REGULAR) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage anything but regular users."); + + if (hr->storage >= 0 && !IN_SET(hr->storage, USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has storage type this service cannot manage."); + + if (gid_is_valid(hr->gid) && hr->uid != (uid_t) hr->gid) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has to have matching UID/GID fields."); + + if (hr->service && !streq(hr->service, "io.systemd.Home")) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home."); + + return 0; +} diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h new file mode 100644 index 0000000000000..ce70b0938d461 --- /dev/null +++ b/src/home/user-record-util.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "user-record.h" +#include "group-record.h" + +int user_record_synthesize(UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid); +int group_record_synthesize(GroupRecord *g, UserRecord *u); + +typedef enum UserReconcileMode { + USER_RECONCILE_ANY, + USER_RECONCILE_REQUIRE_NEWER, /* host version must be newer than embedded version */ + USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, /* similar, but may also be equal */ + _USER_RECONCILE_MODE_MAX, + _USER_RECONCILE_MODE_INVALID = -1, +} UserReconcileMode; + +enum { /* return values */ + USER_RECONCILE_HOST_WON, + USER_RECONCILE_EMBEDDED_WON, + USER_RECONCILE_IDENTICAL, +}; + +int user_record_reconcile(UserRecord *host, UserRecord *embedded, UserReconcileMode mode, UserRecord **ret); +int user_record_add_binding(UserRecord *h, UserStorage storage, const char *image_path, sd_id128_t partition_uuid, sd_id128_t luks_uuid, sd_id128_t fs_uuid, const char *luks_cipher, const char *luks_cipher_mode, uint64_t luks_volume_key_size, const char *file_system_type, const void *fscrypt_salt, size_t fscrypt_salt_size, const char *home_directory, uid_t uid, gid_t gid); + +/* Results of the two test functions below. */ +enum { + USER_TEST_UNDEFINED, /* Returned by user_record_test_image_path() if the storage type knows no image paths */ + USER_TEST_ABSENT, + USER_TEST_EXISTS, + USER_TEST_MOUNTED, /* Only applies to user_record_test_home_directory(), when the home directory exists. */ + USER_TEST_MAYBE, /* Only applies to LUKS devices: block device exists, but we don't know if it's the right one */ +}; + +int user_record_test_home_directory(UserRecord *h); +int user_record_test_home_directory_and_warn(UserRecord *h); +int user_record_test_image_path(UserRecord *h); +int user_record_test_image_path_and_warn(UserRecord *h); + +int user_record_test_secret(UserRecord *h, UserRecord *secret); + +int user_record_update_last_changed(UserRecord *h, bool with_password); +int user_record_set_disk_size(UserRecord *h, uint64_t disk_size); +int user_record_set_password(UserRecord *h, char **password, bool prepend); +int user_record_make_hashed_password(UserRecord *h, UserRecord *secret); +int user_record_merge_secret(UserRecord *h, UserRecord *secret); + +int user_record_good_authentication(UserRecord *h); +int user_record_bad_authentication(UserRecord *h); +int user_record_ratelimit(UserRecord *h); + +int user_record_is_supported(UserRecord *hr, sd_bus_error *error); diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index edd30bf84d8ad..8f7a79fb39b70 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -104,5 +104,28 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { SD_BUS_ERROR_MAP(BUS_ERROR_SPEED_METER_INACTIVE, EOPNOTSUPP), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_HOME, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_UID_IN_USE, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_USER_NAME_EXISTS, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_EXISTS, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_ACTIVE, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_FIXATED, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_UNFIXATED, EADDRNOTAVAIL), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_ACTIVE, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ABSENT, EREMOTE), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_BUSY, EBUSY), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD, ENOKEY), + SD_BUS_ERROR_MAP(BUS_ERROR_LOW_PASSWORD_QUALITY, EUCLEAN), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_SIGNATURE, EKEYREJECTED), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_MISMATCH, EUCLEAN), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_DOWNGRADE, ESTALE), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_SIGNED, EROFS), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_HOME_SIZE, ERANGE), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_PRIVATE_KEY, ENOPKG), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_LOCKED, ENOEXEC), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_LOCKED, ENOEXEC), + SD_BUS_ERROR_MAP(BUS_ERROR_TOO_MANY_OPERATIONS, ENOBUFS), + SD_BUS_ERROR_MAP(BUS_ERROR_AUTHENTICATION_LIMIT_HIT, ETOOMANYREFS), + SD_BUS_ERROR_MAP_END }; diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index 4a29b3bea8ecb..d3f002d75a7d5 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -83,4 +83,28 @@ #define BUS_ERROR_SPEED_METER_INACTIVE "org.freedesktop.network1.SpeedMeterInactive" +#define BUS_ERROR_NO_SUCH_HOME "org.freedesktop.home1.NoSuchHome" +#define BUS_ERROR_UID_IN_USE "org.freedesktop.home1.UIDInUse" +#define BUS_ERROR_USER_NAME_EXISTS "org.freedesktop.home1.UserNameExists" +#define BUS_ERROR_HOME_EXISTS "org.freedesktop.home1.HomeExists" +#define BUS_ERROR_HOME_ALREADY_ACTIVE "org.freedesktop.home1.HomeAlreadyActive" +#define BUS_ERROR_HOME_ALREADY_FIXATED "org.freedesktop.home1.HomeAlreadyFixated" +#define BUS_ERROR_HOME_UNFIXATED "org.freedesktop.home1.HomeUnfixated" +#define BUS_ERROR_HOME_NOT_ACTIVE "org.freedesktop.home1.HomeNotActive" +#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent" +#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy" +#define BUS_ERROR_BAD_PASSWORD "org.freedesktop.home1.BadPassword" +#define BUS_ERROR_LOW_PASSWORD_QUALITY "org.freedesktop.home1.LowPasswordQuality" +#define BUS_ERROR_BAD_SIGNATURE "org.freedesktop.home1.BadSignature" +#define BUS_ERROR_HOME_RECORD_MISMATCH "org.freedesktop.home1.RecordMismatch" +#define BUS_ERROR_HOME_RECORD_DOWNGRADE "org.freedesktop.home1.RecordDowngrade" +#define BUS_ERROR_HOME_RECORD_SIGNED "org.freedesktop.home1.RecordSigned" +#define BUS_ERROR_BAD_HOME_SIZE "org.freedesktop.home1.BadHomeSize" +#define BUS_ERROR_NO_PRIVATE_KEY "org.freedesktop.home1.NoPrivateKey" +#define BUS_ERROR_HOME_LOCKED "org.freedesktop.home1.HomeLocked" +#define BUS_ERROR_HOME_NOT_LOCKED "org.freedesktop.home1.HomeNotLocked" +#define BUS_ERROR_NO_DISK_SPACE "org.freedesktop.home1.NoDiskSpace" +#define BUS_ERROR_TOO_MANY_OPERATIONS "org.freedesktop.home1.TooManyOperations" +#define BUS_ERROR_AUTHENTICATION_LIMIT_HIT "org.freedesktop.home1.AuthenticationLimitHit" + BUS_ERROR_MAP_ELF_USE(bus_common_errors); diff --git a/src/shared/gpt.h b/src/shared/gpt.h index 31e01bd5a5cc9..3d44514d12407 100644 --- a/src/shared/gpt.h +++ b/src/shared/gpt.h @@ -19,6 +19,7 @@ #define GPT_SWAP SD_ID128_MAKE(06,57,fd,6d,a4,ab,43,c4,84,e5,09,33,c8,4b,4f,4f) #define GPT_HOME SD_ID128_MAKE(93,3a,c7,e1,2e,b4,4f,13,b8,44,0e,14,e2,ae,f9,15) #define GPT_SRV SD_ID128_MAKE(3b,8f,84,25,20,e0,4f,3b,90,7f,1a,25,a7,6f,98,e8) +#define GPT_USER_HOME SD_ID128_MAKE(77,3f,91,ef,66,d4,49,b5,bd,83,d6,83,bf,40,ad,16) /* Verity partitions for the root partitions above (we only define them for the root partitions, because only they are * are commonly read-only and hence suitable for verity). */ diff --git a/units/meson.build b/units/meson.build index 83cd88db13a80..2ced8abc7273e 100644 --- a/units/meson.build +++ b/units/meson.build @@ -183,6 +183,8 @@ in_units = [ ['systemd-portabled.service', 'ENABLE_PORTABLED', 'dbus-org.freedesktop.portable1.service'], ['systemd-userdbd.service', 'ENABLE_USERDB'], + ['systemd-homed.service', 'ENABLE_HOMED', + 'multi-user.target.wants/ dbus-org.freedesktop.home1.service'], ['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'], ['systemd-random-seed.service', 'ENABLE_RANDOMSEED', 'sysinit.target.wants/'], diff --git a/units/systemd-homed.service.in b/units/systemd-homed.service.in new file mode 100644 index 0000000000000..cb5edcb30d8e6 --- /dev/null +++ b/units/systemd-homed.service.in @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Home Manager +Documentation=man:systemd-homed.service(8) +RequiresMountsFor=/home + +[Service] +ExecStart=@rootlibexecdir@/systemd-homed +BusName=org.freedesktop.home1 +WatchdogSec=3min +#CapabilityBoundingSet=CAP_KILL CAP_SYS_PTRACE CAP_SYS_ADMIN CAP_SETGID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD +MemoryDenyWriteExecute=yes +ProtectHostname=yes +RestrictRealtime=yes +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 AF_ALG +SystemCallFilter=@system-service @mount +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +LockPersonality=yes +IPAddressDeny=any +StateDirectory=systemd/home From fa30c4c785b02246b0cd76db1d3465baaf057c67 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 19:06:15 +0200 Subject: [PATCH 074/109] home: add homectl client tool --- meson.build | 11 + src/home/homectl.c | 2998 +++++++++++++++++++++++++++++++++++++ src/home/meson.build | 10 + src/home/pwquality-util.c | 49 + src/home/pwquality-util.h | 2 + 5 files changed, 3070 insertions(+) create mode 100644 src/home/homectl.c diff --git a/meson.build b/meson.build index c07f9cf88bf78..949a0648c6bf3 100644 --- a/meson.build +++ b/meson.build @@ -2000,6 +2000,17 @@ if conf.get('ENABLE_HOMED') == 1 install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) + + executable('homectl', + homectl_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcrypt, + libpwquality], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootbindir) endif foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] diff --git a/src/home/homectl.c b/src/home/homectl.c new file mode 100644 index 0000000000000..263eae3087272 --- /dev/null +++ b/src/home/homectl.c @@ -0,0 +1,2998 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "sd-bus.h" + +#include "alloc-util.h" +#include "ask-password-api.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-util.h" +#include "cgroup-util.h" +#include "dns-domain.h" +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "format-util.h" +#include "fs-util.h" +#include "home-util.h" +#include "locale-util.h" +#include "main-func.h" +#include "memory-util.h" +#include "pager.h" +#include "parse-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "process-util.h" +#include "pwquality-util.h" +#include "rlimit-util.h" +#include "spawn-polkit-agent.h" +#include "terminal-util.h" +#include "user-record-show.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "verbs.h" + +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +static bool arg_ask_password = true; +static BusTransport arg_transport = BUS_TRANSPORT_LOCAL; +static const char *arg_host = NULL; +static const char *arg_identity = NULL; +static JsonVariant *arg_identity_extra = NULL; +static JsonVariant *arg_identity_extra_privileged = NULL; +static JsonVariant *arg_identity_extra_this_machine = NULL; +static JsonVariant *arg_identity_extra_rlimits = NULL; +static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */ +static char **arg_identity_filter_rlimits = NULL; +static uint64_t arg_disk_size = UINT64_MAX; +static uint64_t arg_disk_size_relative = UINT64_MAX; +static bool arg_json = false; +static JsonFormatFlags arg_json_format_flags = 0; +static enum { + EXPORT_FORMAT_FULL, /* export the full record */ + EXPORT_FORMAT_STRIPPED, /* strip "state" + "binding", but leave signature in place */ + EXPORT_FORMAT_MINIMAL, /* also strip signature */ +} arg_export_format = EXPORT_FORMAT_FULL; + +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep); + +static int acquire_bus(sd_bus **bus) { + int r; + + assert(bus); + + if (*bus) + return 0; + + r = bus_connect_transport(arg_transport, arg_host, false, bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to bus: %m"); + + (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password); + + return 0; +} + +static int list_homes(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + (void) pager_open(arg_pager_flags); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ListHomes", + &error, + &reply, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list homes: %s", bus_error_message(&error, r)); + + table = table_new("name", "uid", "gid", "state", "realname", "home", "shell"); + if (!table) + return log_oom(); + + r = sd_bus_message_enter_container(reply, 'a', "(susussso)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name, *state, *realname, *home, *shell, *color; + TableCell *cell; + uint32_t uid, gid; + + r = sd_bus_message_read(reply, "(susussso)", &name, &uid, &state, &gid, &realname, &home, &shell, NULL); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = table_add_many(table, + TABLE_STRING, name, + TABLE_UID, uid, + TABLE_GID, gid); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + + r = table_add_cell(table, &cell, TABLE_STRING, state); + if (r < 0) + return log_error_errno(r, "Failed to add field to table: %m"); + + color = user_record_state_color(state); + if (color) + (void) table_set_color(table, cell, color); + + r = table_add_many(table, + TABLE_STRING, strna(empty_to_null(realname)), + TABLE_STRING, home, + TABLE_STRING, strna(empty_to_null(shell))); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (table_get_rows(table) > 1 || arg_json) { + r = table_set_sort(table, (size_t) 0, (size_t) -1); + if (r < 0) + return log_error_errno(r, "Failed to sort table: %m"); + + table_set_header(table, arg_legend); + + if (arg_json) + r = table_print_json(table, stdout, arg_json_format_flags); + else + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + if (arg_legend && !arg_json) { + if (table_get_rows(table) > 1) + printf("\n%zu homes listed.\n", table_get_rows(table) - 1); + else + printf("No homes.\n"); + } + + return 0; +} + +static int acquire_existing_password(const char *user_name, UserRecord *hr) { + _cleanup_(strv_free_erasep) char **password = NULL; + _cleanup_free_ char *question = NULL; + char *e; + int r; + + assert(user_name); + assert(hr); + + e = getenv("PASSWORD"); + if (e) { + /* People really shouldn't use environment variables for passing passwords. We support this + * only for testing purposes, and do not document the behaviour, so that people won't + * actually use this outside of testing. */ + + r = user_record_set_password(hr, STRV_MAKE(e), true); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + string_erase(e); + + if (unsetenv("PASSWORD") < 0) + return log_error_errno(errno, "Failed to unset $PASSWORD: %m"); + + return 0; + } + + if (asprintf(&question, "Please enter password for user %s:", user_name) < 0) + return log_oom(); + + r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &password); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + r = user_record_set_password(hr, password, true); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + return 0; +} + +static int activate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(*i, secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ActivateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + log_error("Password incorrect, please try again."); + else { + log_error_errno(r, "Failed to activate user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int deactivate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "DeactivateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + } + } + + return ret; +} + +static void dump_home_record(UserRecord *hr) { + int r; + + assert(hr); + + if (hr->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", hr->user_name); + } + + if (arg_json) { + _cleanup_(user_record_unrefp) UserRecord *stripped = NULL; + + if (arg_export_format == EXPORT_FORMAT_STRIPPED) + r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &stripped); + else if (arg_export_format == EXPORT_FORMAT_MINIMAL) + r = user_record_clone(hr, USER_RECORD_EXTRACT_SIGNABLE, &stripped); + else + r = 0; + if (r < 0) + log_warning_errno(r, "Failed to strip user record, ignoring: %m"); + if (stripped) + hr = stripped; + + json_variant_dump(hr->json, arg_json_format_flags, stdout, NULL); + } else + user_record_show(hr, true); +} + +static char **mangle_user_list(char **list, char ***ret_allocated) { + _cleanup_free_ char *myself = NULL; + char **l; + + if (!strv_isempty(list)) { + *ret_allocated = NULL; + return list; + } + + myself = getusername_malloc(); + if (!myself) + return NULL; + + l = new(char*, 2); + if (!l) + return NULL; + + l[0] = TAKE_PTR(myself); + l[1] = NULL; + + *ret_allocated = l; + return l; +} + +static int inspect_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(strv_freep) char **mangled_list = NULL; + int r, ret = EXIT_SUCCESS; + char **items, **i; + + (void) pager_open(arg_pager_flags); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + items = mangle_user_list(strv_skip(argv, 1), &mangled_list); + if (!items) + return log_oom(); + + STRV_FOREACH(i, items) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + const char *json; + int incomplete; + uid_t uid; + + r = parse_uid(*i, &uid); + if (r < 0) { + if (!valid_user_group_name(*i)) { + log_error("Invalid user name '%s'.", *i); + if (ret == EXIT_SUCCESS) + ret = -EINVAL; + + continue; + } + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + *i); + } else { + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByUID", + &error, + &reply, + "u", + (uint32_t) uid); + } + + if (r < 0) { + log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + r = sd_bus_message_read(reply, "sbo", &json, &incomplete, NULL); + if (r < 0) { + bus_log_parse_error(r); + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) { + log_error_errno(r, "Failed to parse JSON identity: %m"); + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + 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) { + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + hr->incomplete = incomplete; + dump_home_record(hr); + } + + return ret; +} + +static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + const char *json; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + if (!valid_user_group_name(argv[1])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name '%s'.", argv[1]); + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + argv[1]); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + log_debug_errno(r, "systemd-homed is not available: %s", bus_error_message(&error, r)); + return EXIT_SUCCESS; + } + + if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) { + log_debug_errno(r, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r)); + return EXIT_SUCCESS; + } + + return log_error_errno(r, "Failed to query user record for %s: %m", argv[1]); + } + + r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL); + if (r < 0) + return bus_log_parse_error(r); + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON record: %m"); + + 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 (strv_isempty(hr->ssh_authorized_keys)) + log_debug("User record for %s has no public SSH keys.", argv[1]); + else { + char **i; + + STRV_FOREACH(i, hr->ssh_authorized_keys) + printf("%s\n", *i); + } + + return EXIT_SUCCESS; +} + +static int authenticate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(strv_freep) char **mangled_list = NULL; + int r, ret = EXIT_SUCCESS; + char **i, **items; + + items = mangle_user_list(strv_skip(argv, 1), &mangled_list); + if (!items) + return log_oom(); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, items) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(*i, secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AuthenticateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + log_error("Password incorrect, please try again."); + else { + log_error_errno(r, "Failed to authenticate user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int update_last_change(JsonVariant **v, bool override) { + JsonVariant *c; + usec_t n; + int r; + + assert(v); + + n = now(CLOCK_REALTIME); + + c = json_variant_by_key(*v, "lastChangeUSec"); + if (c) { + uintmax_t u; + + if (!override) + return 0; + + if (!json_variant_is_unsigned(c)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec field is not an unsigned integer, refusing."); + + u = json_variant_unsigned(c); + if (u >= n) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec is from the future, can't update."); + } + + r = json_variant_set_field_unsigned(v, "lastChangeUSec", n); + if (r < 0) + return log_error_errno(r, "Failed to merge identities: %m"); + + return 1; +} + +static int apply_identity_changes(JsonVariant **_v) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(_v); + + v = json_variant_ref(*_v); + + r = json_variant_filter(&v, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter identity: %m"); + + r = json_variant_merge(&v, arg_identity_extra); + if (r < 0) + return log_error_errno(r, "Failed to merge identities: %m"); + + if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) { + _cleanup_(json_variant_unrefp) JsonVariant *per_machine = NULL, *mmid = NULL; + char mids[SD_ID128_STRING_MAX]; + sd_id128_t mid; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return log_error_errno(r, "Failed to acquire machine ID: %m"); + + r = json_variant_new_string(&mmid, sd_id128_to_string(mid, mids)); + if (r < 0) + return log_error_errno(r, "Failed to allocate matchMachineId object: %m"); + + per_machine = json_variant_ref(json_variant_by_key(v, "perMachine")); + if (per_machine) { + _cleanup_(json_variant_unrefp) JsonVariant *npm = NULL, *add = NULL; + _cleanup_free_ JsonVariant **array = NULL; + JsonVariant *z; + size_t i = 0; + + if (!json_variant_is_array(per_machine)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing."); + + array = new(JsonVariant*, json_variant_elements(per_machine) + 1); + if (!array) + return log_oom(); + + JSON_VARIANT_ARRAY_FOREACH(z, per_machine) { + JsonVariant *u; + + if (!json_variant_is_object(z)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine entry is not an object, refusing."); + + array[i++] = z; + + u = json_variant_by_key(z, "matchMachineId"); + if (!u) + continue; + + if (!json_variant_equal(u, mmid)) + continue; + + r = json_variant_merge(&add, z); + if (r < 0) + return log_error_errno(r, "Failed to merge perMachine entry: %m"); + + i--; + } + + r = json_variant_filter(&add, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter perMachine: %m"); + + r = json_variant_merge(&add, arg_identity_extra_this_machine); + if (r < 0) + return log_error_errno(r, "Failed to merge in perMachine fields: %m"); + + if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) { + _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL; + + rlv = json_variant_ref(json_variant_by_key(add, "resourceLimits")); + + r = json_variant_filter(&rlv, arg_identity_filter_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to filter resource limits: %m"); + + r = json_variant_merge(&rlv, arg_identity_extra_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to set resource limits: %m"); + + if (json_variant_is_blank_object(rlv)) { + r = json_variant_filter(&add, STRV_MAKE("resourceLimits")); + if (r < 0) + return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); + } else { + r = json_variant_set_field(&add, "resourceLimits", rlv); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + } + + if (!json_variant_is_blank_object(add)) { + r = json_variant_set_field(&add, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); + + array[i++] = add; + } + + r = json_variant_new_array(&npm, array, i); + if (r < 0) + return log_error_errno(r, "Failed to allocate new perMachine array: %m"); + + json_variant_unref(per_machine); + per_machine = TAKE_PTR(npm); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *item = json_variant_ref(arg_identity_extra_this_machine); + + if (arg_identity_extra_rlimits) { + r = json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + + r = json_variant_set_field(&item, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); + + r = json_variant_append_array(&per_machine, item); + if (r < 0) + return log_error_errno(r, "Failed to append to perMachine array: %m"); + } + + r = json_variant_set_field(&v, "perMachine", per_machine); + if (r < 0) + return log_error_errno(r, "Failed to update per machine record: %m"); + } + + if (arg_identity_extra_privileged || arg_identity_filter) { + _cleanup_(json_variant_unrefp) JsonVariant *privileged = NULL; + + privileged = json_variant_ref(json_variant_by_key(v, "privileged")); + + r = json_variant_filter(&privileged, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter identity (privileged part): %m"); + + r = json_variant_merge(&privileged, arg_identity_extra_privileged); + if (r < 0) + return log_error_errno(r, "Failed to merge identities (privileged part): %m"); + + if (json_variant_is_blank_object(privileged)) { + r = json_variant_filter(&v, STRV_MAKE("privileged")); + if (r < 0) + return log_error_errno(r, "Failed to drop privileged part from identity: %m"); + } else { + r = json_variant_set_field(&v, "privileged", privileged); + if (r < 0) + return log_error_errno(r, "Failed to update privileged part of identity: %m"); + } + } + + if (arg_identity_filter_rlimits) { + _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL; + + rlv = json_variant_ref(json_variant_by_key(v, "resourceLimits")); + + r = json_variant_filter(&rlv, arg_identity_filter_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to filter resource limits: %m"); + + /* Note that we only filter resource limits here, but don't apply them. We do that in the perMachine section */ + + if (json_variant_is_blank_object(rlv)) { + r = json_variant_filter(&v, STRV_MAKE("resourceLimits")); + if (r < 0) + return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); + } else { + r = json_variant_set_field(&v, "resourceLimits", rlv); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + } + + if (DEBUG_LOGGING) + json_variant_dump(v, JSON_FORMAT_PRETTY, NULL, NULL); + + json_variant_unref(*_v); + *_v = TAKE_PTR(v); + + return 0; +} + +static int add_disposition(JsonVariant **v) { + int r; + + assert(v); + + if (json_variant_by_key(*v, "disposition")) + return 0; + + /* Set the disposition to regular, if not configured explicitly */ + r = json_variant_set_field_string(v, "disposition", "regular"); + if (r < 0) + return log_error_errno(r, "Failed to set disposition field: %m"); + + return 1; +} + +static int acquire_home_record(UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(ret); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + if (arg_identity) { + unsigned line, column; + + r = json_parse_file( + streq(arg_identity, "-") ? stdin : NULL, + streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + } + + r = apply_identity_changes(&v); + if (r < 0) + return r; + + r = update_last_change(&v, false); + if (r < 0) + return r; + + r = add_disposition(&v); + if (r < 0) + return r; + + r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG); + if (r < 0) + return r; + + *ret = TAKE_PTR(hr); + return 0; +} + +static int acquire_new_password( + const char *user_name, + UserRecord *hr, + bool suggest) { + + unsigned i = 5; + char *e; + int r; + + assert(user_name); + assert(hr); + + e = getenv("NEWPASSWORD"); + if (e) { + /* As above, this is not for use, just for testing */ + + r = user_record_set_password(hr, STRV_MAKE(e), false); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + string_erase(e); + + if (unsetenv("NEWPASSWORD") < 0) + return log_error_errno(errno, "Failed to unse $NEWPASSWORD: %m"); + + return 0; + } + + if (suggest) + (void) suggest_passwords(); + + for (;;) { + _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL; + _cleanup_free_ char *question = NULL; + + if (--i == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:"); + + if (asprintf(&question, "Please enter new password for user %s:", user_name) < 0) + return log_oom(); + + r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &first); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + question = mfree(question); + if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0) + return log_oom(); + + r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &second); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + if (strv_equal(first, second)) { + r = user_record_set_password(hr, first, false); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + return 0; + } + + log_error("Password didn't mach, try again."); + } +} + +static int create_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (argc >= 2) { + /* If a username was specified, use it */ + + if (valid_user_group_name(argv[1])) + r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]); + else { + _cleanup_free_ char *un = NULL, *rr = NULL; + + /* Before we consider the user name invalid, let's check if we can split it? */ + r = split_user_name_realm(argv[1], &un, &rr); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]); + + if (rr) { + r = json_variant_set_field_string(&arg_identity_extra, "realm", rr); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + } + + r = json_variant_set_field_string(&arg_identity_extra, "userName", un); + } + if (r < 0) + return log_error_errno(r, "Failed to set userName field: %m"); + } else { + /* If neither a username nor an identity have been specified we cannot operate. */ + if (!arg_identity) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required."); + } + + r = acquire_home_record(&hr); + if (r < 0) + return r; + + /* If the JSON record carries no secrets, then let's query them manually */ + if (!FLAGS_SET(hr->mask, USER_RECORD_SECRET)) { + + if (strv_isempty(hr->hashed_password)) { + /* No hashed passwords set in the record, let's fix that. */ + r = acquire_new_password(hr->user_name, hr, /* suggest = */ true); + if (r < 0) + return r; + + r = user_record_make_hashed_password(hr, hr); + if (r < 0) + return log_error_errno(r, "Failed to hash password: %m"); + } else { + /* There's a hash password set in the record, acquire the unhashed version of it. */ + r = acquire_existing_password(hr->user_name, hr); + if (r < 0) + return r; + + r = user_record_test_secret(hr, hr); + if (r < 0) + return log_error_errno(r, "Password does not match record."); + } + } + + if (hr->enforce_password_policy == 0) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + /* If password quality enforcement is disabled, let's at least warn client side */ + + r = quality_check_password(hr, hr, &error); + if (r < 0) + log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r)); + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(erase_and_freep) char *formatted = NULL; + + r = json_variant_format(hr->json, 0, &formatted); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "CreateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) + return log_error_errno(r, "Failed to create user home: %s", bus_error_message(&error, r)); + + log_error_errno(r, "%s", bus_error_message(&error, r)); + log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)"); + } else + break; /* done */ + + r = acquire_new_password(hr->user_name, hr, /* suggest = */ false); + if (r < 0) + return r; + + r = user_record_make_hashed_password(hr, hr); + if (r < 0) + return log_error_errno(r, "Failed to hash passwords: %m"); + } + + return EXIT_SUCCESS; +} + +static int remove_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "RemoveHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + } + } + + return ret; +} + +static int update_home(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *json = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_free_ char *buffer = NULL; + const char *username; + int r; + + if (argc >= 2) + username = argv[1]; + else if (!arg_identity) { + buffer = getusername_malloc(); + if (!buffer) + return log_oom(); + + username = buffer; + } else + username = NULL; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_identity) { + unsigned line, column; + JsonVariant *un; + + r = json_parse_file( + streq(arg_identity, "-") ? stdin : NULL, + streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &json, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + + un = json_variant_by_key(json, "userName"); + if (un) { + if (!json_variant_is_string(un) || (username && !streq(json_variant_string(un), username))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match."); + + if (!username) { + buffer = strdup(json_variant_string(un)); + if (!buffer) + return log_oom(); + + username = buffer; + } + } else { + if (!username) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified."); + + r = json_variant_set_field_string(&arg_identity_extra, "userName", username); + if (r < 0) + return log_error_errno(r, "Failed to set userName field: %m"); + } + + } else { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int incomplete; + const char *text; + + if (!arg_identity_extra && + !arg_identity_extra_this_machine && + !arg_identity_extra_privileged && + !arg_identity_extra_rlimits && + strv_isempty(arg_identity_filter) && + strv_isempty(arg_identity_filter_rlimits)) + return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified."); + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + username); + if (r < 0) + return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL); + if (r < 0) + return bus_log_parse_error(r); + + if (incomplete) + return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record."); + + r = json_parse(text, JSON_PARSE_SENSITIVE, &json, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON identity: %m"); + + reply = sd_bus_message_unref(reply); + + r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature")); + if (r < 0) + return log_error_errno(r, "Failed to strip binding and status from record to update: %m"); + } + + r = apply_identity_changes(&json); + if (r < 0) + return r; + + /* If the user supplied a full record, then add in lastChange, but do not override. Otherwise always override. */ + r = update_last_change(&json, !arg_identity); + if (r < 0) + return r; + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, json, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG); + if (r < 0) + return r; + + if (!FLAGS_SET(hr->mask, USER_RECORD_SECRET)) { + r = acquire_existing_password(username, hr); + if (r < 0) + return r; + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_free_ char *formatted = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "UpdateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = json_variant_format(hr->json, 0, &formatted); + if (r < 0) + return r; + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + return log_error_errno(r, "Failed to update home: %s", bus_error_message(&error, r)); + } else + break; + + log_error("Password incorrect, please try again."); + + r = acquire_existing_password(username, hr); + if (r < 0) + return r; + } + + return EXIT_SUCCESS; +} + +static int passwd_home(int argc, char *argv[], void *userdata) { + _cleanup_(user_record_unrefp) UserRecord *old_secret = NULL, *new_secret = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *buffer = NULL; + const char *username; + int r; + + if (argc >= 2) + username = argv[1]; + else { + buffer = getusername_malloc(); + if (!buffer) + return log_oom(); + + username = buffer; + } + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + old_secret = user_record_new(); + if (!old_secret) + return log_oom(); + + r = acquire_existing_password(username, old_secret); + if (r < 0) + return r; + + new_secret = user_record_new(); + if (!new_secret) + return log_oom(); + + r = acquire_new_password(username, new_secret, /* suggest = */ true); + if (r < 0) + return r; + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ChangePasswordHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", username); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, new_secret); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, old_secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) { + + log_error_errno(r, "%s", bus_error_message(&error, r)); + + r = acquire_new_password(username, new_secret, /* suggest = */ false); + if (r < 0) + return r; + + continue; + + } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) { + + log_notice("Old password incorrect, please try again."); + + r = acquire_existing_password(username, old_secret); + if (r < 0) + return r; + + continue; + } else + return log_error_errno(r, "Failed to change password for home: %s", bus_error_message(&error, r)); + } else + break; + } + + return EXIT_SUCCESS; +} + +static int resize_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + uint64_t ds = UINT64_MAX; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_disk_size_relative != UINT64_MAX || + (argc > 2 && parse_percent(argv[2]) >= 0)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Relative disk size specification currently not supported when resizing."); + + if (argc > 2) { + r = parse_size(argv[2], 1024, &ds); + if (r < 0) + return log_error_errno(r, "Failed to parse disk size parameter: %s", argv[2]); + } + + if (arg_disk_size != UINT64_MAX) { + if (ds != UINT64_MAX && ds != arg_disk_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size specified twice and doesn't match, refusing."); + + ds = arg_disk_size; + } + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(argv[1], secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ResizeHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "st", argv[1], ds); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + return log_error_errno(r, "Failed to resize home: %s", bus_error_message(&error, r)); + } else + break; + + log_error("Password incorrect, please try again."); + } + + return EXIT_SUCCESS; +} + +static int lock_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + } + } + + return ret; +} + +static int unlock_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(*i, secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "UnlockHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + log_error("Password incorrect, please try again."); + else { + log_error_errno(r, "Failed to unlock user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int with_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + _cleanup_close_ int acquired_fd = -1; + _cleanup_strv_free_ char **cmdline = NULL; + const char *home; + int r, ret; + pid_t pid; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + if (argc < 3) { + _cleanup_free_ char *shell = NULL; + + /* If no command is specified, spawn a shell */ + r = get_shell(&shell); + if (r < 0) + return log_error_errno(r, "Failed to acquire shell: %m"); + + cmdline = strv_new(shell); + } else + cmdline = strv_copy(argv + 2); + if (!cmdline) + return log_oom(); + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + r = acquire_existing_password(argv[1], secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AcquireHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", argv[1]); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "b", /* please_suspend = */ getenv_bool("SYSTEMD_PLEASE_SUSPEND_HOME") > 0); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); + m = sd_bus_message_unref(m); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + return log_error_errno(r, "Failed to activate user home: %s", bus_error_message(&error, r)); + + log_error("Password incorrect, please try again."); + sd_bus_error_free(&error); + } else { + int fd; + + r = sd_bus_message_read(reply, "h", &fd); + if (r < 0) + return bus_log_parse_error(r); + + acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (acquired_fd < 0) + return log_error_errno(errno, "Failed to duplicate acquired fd: %m"); + + reply = sd_bus_message_unref(reply); + break; + } + } + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetHomeByName", + &error, + &reply, + "s", + argv[1]); + if (r < 0) + return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "usussso", NULL, NULL, NULL, NULL, &home, NULL, NULL); + if (r < 0) + return bus_log_parse_error(r); + + r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + if (chdir(home) < 0) { + log_error_errno(errno, "Failed to change to directory %s: %m", home); + _exit(255); + } + + execvp(cmdline[0], cmdline); + log_error_errno(errno, "Failed to execute %s: %m", cmdline[0]); + _exit(255); + } + + ret = wait_for_terminate_and_check(cmdline[0], pid, WAIT_LOG_ABNORMAL); + + /* Close the fd that pings the home now. */ + acquired_fd = safe_close(acquired_fd); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ReleaseHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", argv[1]); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) + log_notice("Not deactivating home directory of %s, as it is still used.", argv[1]); + else + return log_error_errno(r, "Failed to release user home: %s", bus_error_message(&error, r)); + } + + return ret; +} + +static int lock_all_homes(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockAllHomes"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + + return EXIT_SUCCESS; +} + +static int drop_from_identity(const char *field) { + int r; + + assert(field); + + /* If we are called to update an identity record and drop some field, let's keep track of what to + * remove from the old record */ + r = strv_extend(&arg_identity_filter, field); + if (r < 0) + return log_oom(); + + /* Let's also drop the field if it was previously set to a new value on the same command line */ + r = json_variant_filter(&arg_identity_extra, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + r = json_variant_filter(&arg_identity_extra_this_machine, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + return 0; +} + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + (void) pager_open(arg_pager_flags); + + r = terminal_urlify_man("homectl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] {COMMAND} ...\n\n" + "Create, manipulate or inspect home directories.\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" + " --no-ask-password Do not ask for system passwords\n" + " -H --host=[USER@]HOST Operate on remote host\n" + " -M --machine=CONTAINER Operate on local container\n" + " --identity=PATH Read JSON identity from file\n" + " --json=FORMAT Output inspection data in JSON (takes one of\n" + " pretty, short, off)\n" + " -j Equivalent to --json=pretty (on TTY) or\n" + " --json=short (otherwise)\n" + " --export-format= Strip JSON inspection data (full, stripped,\n" + " minimal)\n" + " -E When specified once equals -j --export-format=\n" + " stripped, when specified twice equals\n" + " -j --export-format=minimal\n" + "\n%3$sGeneral User Record Properties:%4$s\n" + " -c --real-name=REALNAME Real name for user\n" + " --realm=REALM Realm to create user in\n" + " --email-address=EMAIL Email address for user\n" + " --location=LOCATION Set location of user on earth\n" + " --password-hint=HINT Set Password hint\n" + " --icon-name=NAME Icon name for user\n" + " -d --home-dir=PATH Home directory\n" + " --uid=UID Numeric UID for user\n" + " -G --member-of=GROUP Add user to group\n" + " --skel=PATH Skeleton directory to use\n" + " --shell=PATH Shell for account\n" + " --setenv=VARIABLE=VALUE Set an environment variable at log-in\n" + " --timezone=TIMEZONE Set a time-zone\n" + " --language=LOCALE Set preferred language\n" + " --ssh-authorized-keys=KEYS\n" + " Specify SSH public keys\n" + "\n%3$sAccount Management User Record Properties:%4$s\n" + " --locked=BOOL Set locked account state\n" + " --not-before=TIMESTAMP Do not allow logins before\n" + " --not-after=TIMESTAMP Do not allow logins after\n" + " --rate-limit-interval=SECS\n" + " Login rate-limit interval in seconds\n" + " --rate-limit-burst=NUMBER\n" + " Login rate-limit attempts per interval\n" + " --enforce-password-policy=BOOL\n" + " Control whether to enforce system's password\n" + " policy for this user\n" + " -P Equivalent to --enforce-password-password=no\n" + "\n%3$sResource Management User Record Properties:%4$s\n" + " --disk-size=BYTES Size to assign the user on disk\n" + " --access-mode=MODE User home directory access mode\n" + " --umask=MODE Umask for user when logging in\n" + " --nice=NICE Nice level for user\n" + " --rlimit=LIMIT=VALUE[:VALUE]\n" + " Set resource limits\n" + " --tasks-max=MAX Set maximum number of per-user tasks\n" + " --memory-high=BYTES Set high memory threshold in bytes\n" + " --memory-max=BYTES Set maximum memory limit\n" + " --cpu-weight=WEIGHT Set CPU weight\n" + " --io-weight=WEIGHT Set IO weight\n" + "\n%3$sStorage User Record Properties:%4$s\n" + " --storage=STORAGE Storage type to use (luks, fscrypt, directory,\n" + " subvolume, cifs)\n" + " --image-path=PATH Path to image file/directory\n" + "\n%3$sLUKS Storage User Record Properties:%4$s\n" + " --fs-type=TYPE File system type to use in case of luks\n" + " storage (ext4, xfs, btrfs)\n" + " --luks-discard=BOOL Whether to use 'discard' feature of file system\n" + " --luks-cipher=CIPHER Cipher to use for LUKS encryption\n" + " --luks-cipher-mode=MODE Cipher mode to use for LUKS encryption\n" + " --luks-volume-key-size=BITS\n" + " Volume key size to use for LUKS encryption\n" + " --luks-pbkdf-type=TYPE Password-based Key Derivation Function to use\n" + " --luks-pbkdf-hash-algorithm=ALGORITHM\n" + " PBKDF hash algorithm to use\n" + " --luks-pbkdf-time-cost=SECS\n" + " Time cost for PBKDF in seconds\n" + " --luks-pbkdf-memory-cost=BYTES\n" + " Memory cost for PBKDF in bytes\n" + " --luks-pbkdf-parallel-threads=NUMBER\n" + " Number of parallel threads for PKBDF\n" + "\n%3$sMounting User Record Properties:%4$s\n" + " --nosuid=BOOL Control the 'nosuid' flag of the home mount\n" + " --nodev=BOOL Control the 'nodev' flag of the home mount\n" + " --noexec=BOOL Control the 'noexec' flag of the home mount\n" + "\n%3$sCIFS User Record Properties:%4$s\n" + " --cifs-domain=DOMAIN CIFS (Windows) domain\n" + " --cifs-user-name=USER CIFS (Windows) user name\n" + " --cifs-service=SERVICE CIFS (Windows) service to mount as home\n" + "\n%3$sLogin Behaviour User Record Properties:%4$s\n" + " --stop-delay=SECS How long to leave user services running after\n" + " logout\n" + " --kill-processes=BOOL Whether to kill user processes when sessions\n" + " terminate\n" + " --auto-login=BOOL Try to log this user in automatically\n" + "\n%3$sCommands:%4$s\n" + " list List homes\n" + " activate USER… Activate home\n" + " deactivate USER… Deactivate home\n" + " inspect USER… Inspect home\n" + " authenticate USER… Authenticate home\n" + " create USER Create a home\n" + " remove USER… Remove a home\n" + " update USER Update a home\n" + " passwd USER Change password of a home\n" + " resize USER SIZE Resize a home\n" + " lock USER… Temporarily lock an active home\n" + " unlock USER… Unlock a temporarily locked home\n" + " lock-all Lock all suitable homes\n" + " with USER [COMMAND…] Run shell or command with access to home\n" + "\nSee the %2$s for details.\n" + , program_invocation_short_name + , link + , ansi_underline(), ansi_normal() + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_NO_ASK_PASSWORD, + ARG_REALM, + ARG_EMAIL_ADDRESS, + ARG_DISK_SIZE, + ARG_ACCESS_MODE, + ARG_STORAGE, + ARG_FS_TYPE, + ARG_IMAGE_PATH, + ARG_UMASK, + ARG_LUKS_DISCARD, + ARG_JSON, + ARG_SETENV, + ARG_TIMEZONE, + ARG_LANGUAGE, + ARG_LOCKED, + ARG_SSH_AUTHORIZED_KEYS, + ARG_LOCATION, + ARG_ICON_NAME, + ARG_PASSWORD_HINT, + ARG_NICE, + ARG_RLIMIT, + ARG_NOT_BEFORE, + ARG_NOT_AFTER, + ARG_LUKS_CIPHER, + ARG_LUKS_CIPHER_MODE, + ARG_LUKS_VOLUME_KEY_SIZE, + ARG_NOSUID, + ARG_NODEV, + ARG_NOEXEC, + ARG_CIFS_DOMAIN, + ARG_CIFS_USER_NAME, + ARG_CIFS_SERVICE, + ARG_TASKS_MAX, + ARG_MEMORY_HIGH, + ARG_MEMORY_MAX, + ARG_CPU_WEIGHT, + ARG_IO_WEIGHT, + ARG_LUKS_PBKDF_TYPE, + ARG_LUKS_PBKDF_HASH_ALGORITHM, + ARG_LUKS_PBKDF_TIME_COST, + ARG_LUKS_PBKDF_MEMORY_COST, + ARG_LUKS_PBKDF_PARALLEL_THREADS, + ARG_RATE_LIMIT_INTERVAL, + ARG_RATE_LIMIT_BURST, + ARG_STOP_DELAY, + ARG_KILL_PROCESSES, + ARG_ENFORCE_PASSWORD_POLICY, + ARG_EXPORT_FORMAT, + ARG_AUTO_LOGIN, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "host", required_argument, NULL, 'H' }, + { "machine", required_argument, NULL, 'M' }, + { "identity", required_argument, NULL, 'I' }, + { "real-name", required_argument, NULL, 'c' }, + { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "realm", required_argument, NULL, ARG_REALM }, + { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, + { "location", required_argument, NULL, ARG_LOCATION }, + { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT }, + { "icon-name", required_argument, NULL, ARG_ICON_NAME }, + { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */ + { "uid", required_argument, NULL, 'u' }, /* Compatible with useradd(8) */ + { "member-of", required_argument, NULL, 'G' }, + { "groups", required_argument, NULL, 'G' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "skel", required_argument, NULL, 'k' }, /* Compatible with useradd(8) */ + { "shell", required_argument, NULL, 's' }, /* Compatible with useradd(8) */ + { "setenv", required_argument, NULL, ARG_SETENV }, + { "timezone", required_argument, NULL, ARG_TIMEZONE }, + { "language", required_argument, NULL, ARG_LANGUAGE }, + { "locked", required_argument, NULL, ARG_LOCKED }, + { "not-before", required_argument, NULL, ARG_NOT_BEFORE }, + { "not-after", required_argument, NULL, ARG_NOT_AFTER }, + { "expiredate", required_argument, NULL, 'e' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "ssh-authorized-keys", required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS }, + { "disk-size", required_argument, NULL, ARG_DISK_SIZE }, + { "access-mode", required_argument, NULL, ARG_ACCESS_MODE }, + { "umask", required_argument, NULL, ARG_UMASK }, + { "nice", required_argument, NULL, ARG_NICE }, + { "rlimit", required_argument, NULL, ARG_RLIMIT }, + { "tasks-max", required_argument, NULL, ARG_TASKS_MAX }, + { "memory-high", required_argument, NULL, ARG_MEMORY_HIGH }, + { "memory-max", required_argument, NULL, ARG_MEMORY_MAX }, + { "cpu-weight", required_argument, NULL, ARG_CPU_WEIGHT }, + { "io-weight", required_argument, NULL, ARG_IO_WEIGHT }, + { "storage", required_argument, NULL, ARG_STORAGE }, + { "image-path", required_argument, NULL, ARG_IMAGE_PATH }, + { "fs-type", required_argument, NULL, ARG_FS_TYPE }, + { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD }, + { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER }, + { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE }, + { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE }, + { "luks-pbkdf-type", required_argument, NULL, ARG_LUKS_PBKDF_TYPE }, + { "luks-pbkdf-hash-algorithm", required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM }, + { "luks-pbkdf-time-cost", required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST }, + { "luks-pbkdf-memory-cost", required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST }, + { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS }, + { "nosuid", required_argument, NULL, ARG_NOSUID }, + { "nodev", required_argument, NULL, ARG_NODEV }, + { "noexec", required_argument, NULL, ARG_NOEXEC }, + { "cifs-user-name", required_argument, NULL, ARG_CIFS_USER_NAME }, + { "cifs-domain", required_argument, NULL, ARG_CIFS_DOMAIN }, + { "cifs-service", required_argument, NULL, ARG_CIFS_SERVICE }, + { "rate-limit-interval", required_argument, NULL, ARG_RATE_LIMIT_INTERVAL }, + { "rate-limit-burst", required_argument, NULL, ARG_RATE_LIMIT_BURST }, + { "stop-delay", required_argument, NULL, ARG_STOP_DELAY }, + { "kill-processes", required_argument, NULL, ARG_KILL_PROCESSES }, + { "enforce-password-policy", required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY }, + { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN }, + { "json", required_argument, NULL, ARG_JSON }, + { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT }, + {} + }; + + int r; + + assert(argc >= 0); + assert(argv); + + for (;;) { + int c; + + c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", options, NULL); + if (c < 0) + break; + + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + case 'H': + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = optarg; + break; + + case 'M': + arg_transport = BUS_TRANSPORT_MACHINE; + arg_host = optarg; + break; + + case 'I': + arg_identity = optarg; + break; + + case 'c': + if (isempty(optarg)) { + r = drop_from_identity("realName"); + if (r < 0) + return r; + + break; + } + + if (!valid_gecos(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "realName", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set realName field: %m"); + + break; + + case 'd': { + _cleanup_free_ char *hd = NULL; + + if (isempty(optarg)) { + r = drop_from_identity("homeDirectory"); + if (r < 0) + return r; + + break; + } + + r = parse_path_argument_and_warn(optarg, false, &hd); + if (r < 0) + return r; + + if (!valid_home(hd)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd); + + r = json_variant_set_field_string(&arg_identity_extra, "homeDirectory", hd); + if (r < 0) + return log_error_errno(r, "Failed to set homeDirectory field: %m"); + + break; + } + + case ARG_REALM: + if (isempty(optarg)) { + r = drop_from_identity("realm"); + if (r < 0) + return r; + + break; + } + + r = dns_name_is_valid(optarg); + if (r < 0) + return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", optarg); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain: %m", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + break; + + case ARG_EMAIL_ADDRESS: + case ARG_LOCATION: + case ARG_ICON_NAME: + case ARG_CIFS_USER_NAME: + case ARG_CIFS_DOMAIN: + case ARG_CIFS_SERVICE: { + + const char *field = + c == ARG_EMAIL_ADDRESS ? "emailAddress" : + c == ARG_LOCATION ? "location" : + c == ARG_ICON_NAME ? "iconName" : + c == ARG_CIFS_USER_NAME ? "cifsUserName" : + c == ARG_CIFS_DOMAIN ? "cifsDomain" : + c == ARG_CIFS_SERVICE ? "cifsService" : + NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = json_variant_set_field_string(&arg_identity_extra, field, optarg); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_PASSWORD_HINT: + if (isempty(optarg)) { + r = drop_from_identity("passwordHint"); + if (r < 0) + return r; + + break; + } + + r = json_variant_set_field_string(&arg_identity_extra_privileged, "passwordHint", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set passwordHint field: %m"); + + string_erase(optarg); + break; + + case ARG_NICE: { + int nc; + + if (isempty(optarg)) { + r = drop_from_identity("niceLevel"); + if (r < 0) + return r; + break; + } + + r = parse_nice(optarg, &nc); + if (r < 0) + return log_error_errno(r, "Failed to parse nice level: %s", optarg); + + r = json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc); + if (r < 0) + return log_error_errno(r, "Failed to set niceLevel field: %m"); + + break; + } + + case ARG_RLIMIT: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL; + _cleanup_free_ char *field = NULL, *t = NULL; + const char *eq; + struct rlimit rl; + int l; + + if (isempty(optarg)) { + /* Remove all resource limits */ + + r = drop_from_identity("resourceLimits"); + if (r < 0) + return r; + + arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits); + arg_identity_extra_rlimits = json_variant_unref(arg_identity_extra_rlimits); + break; + } + + eq = strchr(optarg, '='); + if (!eq) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", optarg); + + field = strndup(optarg, eq - optarg); + if (!field) + return log_oom(); + + l = rlimit_from_string_harder(field); + if (l < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown resource limit type: %s", field); + + if (isempty(eq + 1)) { + /* Remove only the specific rlimit */ + + r = strv_extend(&arg_identity_filter_rlimits, rlimit_to_string(l)); + if (r < 0) + return r; + + r = json_variant_filter(&arg_identity_extra_rlimits, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + break; + } + + r = rlimit_parse(l, eq + 1, &rl); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse resource limit value: %s", eq + 1); + + r = rl.rlim_cur == RLIM_INFINITY ? json_variant_new_null(&jcur) : json_variant_new_unsigned(&jcur, rl.rlim_cur); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate current integer: %m"); + + r = rl.rlim_max == RLIM_INFINITY ? json_variant_new_null(&jmax) : json_variant_new_unsigned(&jmax, rl.rlim_max); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m"); + + r = json_build(&v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)), + JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax)))); + if (r < 0) + return log_error_errno(r, "Failed to build resource limit: %m"); + + t = strjoin("RLIMIT_", rlimit_to_string(l)); + if (!t) + return log_oom(); + + r = json_variant_set_field(&arg_identity_extra_rlimits, t, v); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l)); + + break; + } + + case 'u': { + uid_t uid; + + if (isempty(optarg)) { + r = drop_from_identity("uid"); + if (r < 0) + return r; + + break; + } + + r = parse_uid(optarg, &uid); + if (r < 0) + return log_error_errno(r, "Failed to parse UID '%s'.", optarg); + + if (uid_is_system(uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in system range, refusing.", uid); + if (uid_is_dynamic(uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in dynamic range, refusing.", uid); + if (uid == UID_NOBODY) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is nobody UID, refusing.", uid); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "uid", uid); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + + break; + } + + case 'k': + case ARG_IMAGE_PATH: { + const char *field = c == 'k' ? "skeletonDirectory" : "imagePath"; + _cleanup_free_ char *v = NULL; + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_path_argument_and_warn(optarg, false, &v); + if (r < 0) + return r; + + r = json_variant_set_field_string(&arg_identity_extra_this_machine, field, v); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", v); + + break; + } + + case 's': + if (isempty(optarg)) { + r = drop_from_identity("shell"); + if (r < 0) + return r; + + break; + } + + if (!valid_shell(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "shell", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set shell field: %m"); + + break; + + case ARG_SETENV: { + _cleanup_free_ char **l = NULL, **k = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *ne = NULL; + JsonVariant *e; + + if (isempty(optarg)) { + r = drop_from_identity("environment"); + if (r < 0) + return r; + + break; + } + + if (!env_assignment_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Environment assignment '%s' not valid.", optarg); + + e = json_variant_by_key(arg_identity_extra, "environment"); + if (e) { + r = json_variant_strv(e, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON environment field: %m"); + } + + k = strv_env_set(l, optarg); + if (!k) + return log_oom(); + + strv_sort(k); + + r = json_variant_new_array_strv(&ne, k); + if (r < 0) + return log_error_errno(r, "Failed to allocate environment list JSON: %m"); + + r = json_variant_set_field(&arg_identity_extra, "environment", ne); + if (r < 0) + return log_error_errno(r, "Failed to set environent list: %m"); + + break; + } + + case ARG_TIMEZONE: + + if (isempty(optarg)) { + r = drop_from_identity("timeZone"); + if (r < 0) + return r; + + break; + } + + if (!timezone_is_valid(optarg, LOG_DEBUG)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set timezone field: %m"); + + break; + + case ARG_LANGUAGE: + if (isempty(optarg)) { + r = drop_from_identity("language"); + if (r < 0) + return r; + + break; + } + + if (!locale_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set preferredLanguage field: %m"); + + break; + + case ARG_NOSUID: + case ARG_NODEV: + case ARG_NOEXEC: + case ARG_LOCKED: + case ARG_KILL_PROCESSES: + case ARG_ENFORCE_PASSWORD_POLICY: + case ARG_AUTO_LOGIN: { + const char *field = + c == ARG_LOCKED ? "locked" : + c == ARG_NOSUID ? "mountNoSUID" : + c == ARG_NODEV ? "mountNoDevices" : + c == ARG_NOEXEC ? "mountNoExecute" : + c == ARG_KILL_PROCESSES ? "killProcesses" : + c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" : + c == ARG_AUTO_LOGIN ? "autoLogin" : + NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse %s boolean: %m", field); + + r = json_variant_set_field_boolean(&arg_identity_extra, field, r > 0); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'P': + r = json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false); + if (r < 0) + return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m"); + + break; + + case ARG_DISK_SIZE: + if (isempty(optarg)) { + r = drop_from_identity("diskSize"); + if (r < 0) + return r; + + r = drop_from_identity("diskSizeRelative"); + if (r < 0) + return r; + + arg_disk_size = arg_disk_size_relative = UINT64_MAX; + break; + } + + r = parse_permille(optarg); + if (r < 0) { + r = parse_size(optarg, 1024, &arg_disk_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size '%s' not valid.", optarg); + + r = drop_from_identity("diskSizeRelative"); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size); + if (r < 0) + return log_error_errno(r, "Failed to set diskSize field: %m"); + + arg_disk_size_relative = UINT64_MAX; + } else { + /* Normalize to UINT32_MAX == 100% */ + arg_disk_size_relative = (uint64_t) r * UINT32_MAX / 1000U; + + r = drop_from_identity("diskSize"); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); + if (r < 0) + return log_error_errno(r, "Failed to set diskSizeRelative field: %m"); + + arg_disk_size = UINT64_MAX; + } + + break; + + case ARG_ACCESS_MODE: { + mode_t mode; + + if (isempty(optarg)) { + r = drop_from_identity("accessMode"); + if (r < 0) + return r; + + break; + } + + r = parse_mode(optarg, &mode); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "accessMode", mode); + if (r < 0) + return log_error_errno(r, "Failed to set access mode field: %m"); + + break; + } + + case ARG_LUKS_DISCARD: + if (isempty(optarg)) { + r = drop_from_identity("luksDiscard"); + if (r < 0) + return r; + + break; + } + + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg); + + r = json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r); + if (r < 0) + return log_error_errno(r, "Failed to set discard field: %m"); + + break; + + case ARG_LUKS_VOLUME_KEY_SIZE: + case ARG_LUKS_PBKDF_PARALLEL_THREADS: + case ARG_RATE_LIMIT_BURST: { + const char *field = + c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" : + c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" : + c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" : NULL; + unsigned n; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + } + + r = safe_atou(optarg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_UMASK: { + mode_t m; + + if (isempty(optarg)) { + r = drop_from_identity("umask"); + if (r < 0) + return r; + + break; + } + + r = parse_mode(optarg, &m); + if (r < 0) + return log_error_errno(r, "Failed to parse umask: %m"); + + r = json_variant_set_field_integer(&arg_identity_extra, "umask", m); + if (r < 0) + return log_error_errno(r, "Failed to set umask field: %m"); + + break; + } + + case ARG_SSH_AUTHORIZED_KEYS: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *p = NULL; + _cleanup_(strv_freep) char **l = NULL, **add = NULL; + + if (isempty(optarg)) { + r = drop_from_identity("sshAuthorizedKeys"); + if (r < 0) + return r; + + break; + } + + if (optarg[0] == '@') { + _cleanup_fclose_ FILE *f = NULL; + + /* If prefixed with '@' read from a file */ + + f = fopen(optarg+1, "re"); + if (!f) + return log_error_errno(errno, "Failed to open '%s': %m", optarg+1); + + for (;;) { + _cleanup_free_ char *line = NULL; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Faile dto read from '%s': %m", optarg+1); + if (r == 0) + break; + + if (isempty(line)) + continue; + + if (line[0] == '#') + continue; + + r = strv_consume(&add, TAKE_PTR(line)); + if (r < 0) + return log_oom(); + } + } else { + /* Otherwise, assume it's a literal key. Let's do some superficial checks + * before accept it though. */ + + if (string_has_cc(optarg, NULL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Authorized key contains control characters, refusing."); + if (optarg[0] == '#') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?"); + + add = strv_new(optarg); + if (!add) + return log_oom(); + } + + v = json_variant_ref(json_variant_by_key(arg_identity_extra_privileged, "sshAuthorizedKeys")); + if (v) { + r = json_variant_strv(v, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse SSH authorized keys list: %m"); + } + + r = strv_extend_strv(&l, add, true); + if (r < 0) + return log_oom(); + + v = json_variant_unref(v); + + r = json_variant_new_array_strv(&v, l); + if (r < 0) + return log_oom(); + + r = json_variant_set_field(&arg_identity_extra_privileged, "sshAuthorizedKeys", v); + if (r < 0) + return log_error_errno(r, "Failed to set authorized keys: %m"); + + break; + } + + case ARG_NOT_BEFORE: + case ARG_NOT_AFTER: + case 'e': { + const char *field; + usec_t n; + + field = c == ARG_NOT_BEFORE ? "notBeforeUSec" : + IN_SET(c, ARG_NOT_AFTER, 'e') ? "notAfterUSec" : NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + /* Note the minor discrepancy regarding -e parsing here: we support that for compat + * reasons, and in the original useradd(8) implementation it accepts dates in the + * format YYYY-MM-DD. Coincidentally, we accept dates formatted like that too, but + * with greater precision. */ + r = parse_timestamp(optarg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %m", field); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + break; + } + + case ARG_STORAGE: + case ARG_FS_TYPE: + case ARG_LUKS_CIPHER: + case ARG_LUKS_CIPHER_MODE: + case ARG_LUKS_PBKDF_TYPE: + case ARG_LUKS_PBKDF_HASH_ALGORITHM: { + + const char *field = + c == ARG_STORAGE ? "storage" : + c == ARG_FS_TYPE ? "fileSytemType" : + c == ARG_LUKS_CIPHER ? "luksCipher" : + c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" : + c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" : + c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" : NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + if (!string_is_safe(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg); + + r = json_variant_set_field_string( + IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? + &arg_identity_extra_this_machine : + &arg_identity_extra, field, optarg); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_LUKS_PBKDF_TIME_COST: + case ARG_RATE_LIMIT_INTERVAL: + case ARG_STOP_DELAY: { + const char *field = + c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" : + c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" : + c == ARG_STOP_DELAY ? "stopDelayUSec" : + NULL; + usec_t t; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_sec(optarg, &t); + if (r < 0) + return log_error_errno(r, "Failed to parse %s field: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, t); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'G': { + const char *p = optarg; + + if (isempty(p)) { + r = drop_from_identity("memberOf"); + if (r < 0) + return r; + + break; + } + + for (;;) { + _cleanup_(json_variant_unrefp) JsonVariant *mo = NULL; + _cleanup_strv_free_ char **list = NULL; + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", 0); + if (r < 0) + return log_error_errno(r, "Failed to parse group list: %m"); + if (r == 0) + break; + + if (!valid_user_group_name(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word); + + mo = json_variant_ref(json_variant_by_key(arg_identity_extra, "memberOf")); + + r = json_variant_strv(mo, &list); + if (r < 0) + return log_error_errno(r, "Failed to parse group list: %m"); + + r = strv_extend(&list, word); + if (r < 0) + return log_error_errno(r, "Failed to extend group list: %m"); + + strv_sort(list); + strv_uniq(list); + + mo = json_variant_unref(mo); + r = json_variant_new_array_strv(&mo, list); + if (r < 0) + return log_error_errno(r, "Failed to create group list JSON: %m"); + + r = json_variant_set_field(&arg_identity_extra, "memberOf", mo); + if (r < 0) + return log_error_errno(r, "Failed to update group list: %m"); + } + + break; + } + + case ARG_TASKS_MAX: { + uint64_t u; + + if (isempty(optarg)) { + r = drop_from_identity("tasksMax"); + if (r < 0) + return r; + break; + } + + r = safe_atou64(optarg, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u); + if (r < 0) + return log_error_errno(r, "Failed to set tasksMax field: %m"); + + break; + } + + case ARG_MEMORY_MAX: + case ARG_MEMORY_HIGH: + case ARG_LUKS_PBKDF_MEMORY_COST: { + const char *field = + c == ARG_MEMORY_MAX ? "memoryMax" : + c == ARG_MEMORY_HIGH ? "memoryHigh" : + c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" : NULL; + + uint64_t u; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + break; + } + + r = parse_size(optarg, 1024, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_CPU_WEIGHT: + case ARG_IO_WEIGHT: { + const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" : + c == ARG_IO_WEIGHT ? "ioWeight" : NULL; + uint64_t u; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + break; + } + + r = safe_atou64(optarg, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse --cpu-weight=/--io-weight= parameter: %s", optarg); + + if (!CGROUP_WEIGHT_IS_OK(u)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'j': + arg_json = true; + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case ARG_JSON: + if (streq(optarg, "pretty")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO; + } else if (streq(optarg, "short")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_NEWLINE; + } else if (streq(optarg, "off")) { + arg_json = false; + arg_json_format_flags = 0; + } else if (streq(optarg, "help")) { + puts("pretty\n" + "short\n" + "off"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown argument to --json=: %s", optarg); + + break; + + case 'E': + if (arg_export_format == EXPORT_FORMAT_FULL) + arg_export_format = EXPORT_FORMAT_STRIPPED; + else if (arg_export_format == EXPORT_FORMAT_STRIPPED) + arg_export_format = EXPORT_FORMAT_MINIMAL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported."); + + arg_json = true; + if (arg_json_format_flags == 0) + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case ARG_EXPORT_FORMAT: + if (streq(optarg, "full")) + arg_export_format = EXPORT_FORMAT_FULL; + else if (streq(optarg, "stripped")) + arg_export_format = EXPORT_FORMAT_STRIPPED; + else if (streq(optarg, "minimal")) + arg_export_format = EXPORT_FORMAT_MINIMAL; + else if (streq(optarg, "help")) { + puts("full\n" + "stripped\n" + "minimal"); + return 0; + } + + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached("Unhandled option"); + } + } + + return 1; +} + +static int run(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "list", VERB_ANY, 1, VERB_DEFAULT, list_homes }, + { "activate", 2, VERB_ANY, 0, activate_home }, + { "deactivate", 2, VERB_ANY, 0, deactivate_home }, + { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home }, + { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, + { "create", VERB_ANY, 2, 0, create_home }, + { "remove", 2, VERB_ANY, 0, remove_home }, + { "update", VERB_ANY, 2, 0, update_home }, + { "passwd", VERB_ANY, 2, 0, passwd_home }, + { "resize", 2, 3, 0, resize_home }, + { "lock", 2, VERB_ANY, 0, lock_home }, + { "unlock", 2, VERB_ANY, 0, unlock_home }, + { "with", 2, VERB_ANY, 0, with_home }, + { "lock-all", VERB_ANY, 1, 0, lock_all_homes }, + + /* This one is a helper for sshd_config's AuthorizedKeysCommand= setting, it's not a + * user-facing verb and thus should not appear in man pages or --help texts. */ + { "ssh-authorized-keys", 2, 2, 0, ssh_authorized_keys }, + {} + }; + + int r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/meson.build b/src/home/meson.build index d6f46716dbb90..62602aebb3c10 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -34,6 +34,16 @@ systemd_homed_sources = files(''' user-record-util.h '''.split()) +homectl_sources = files(''' + home-util.c + home-util.h + homectl.c + pwquality-util.c + pwquality-util.h + user-record-util.c + user-record-util.h +'''.split()) + if conf.get('ENABLE_HOMED') == 1 install_data('org.freedesktop.home1.conf', install_dir : dbuspolicydir) diff --git a/src/home/pwquality-util.c b/src/home/pwquality-util.c index e15203e8dc242..be4fb1cb4a071 100644 --- a/src/home/pwquality-util.c +++ b/src/home/pwquality-util.c @@ -10,6 +10,7 @@ #include "bus-common-errors.h" #include "home-util.h" +#include "memory-util.h" #include "pwquality-util.h" #include "strv.h" @@ -124,10 +125,58 @@ int quality_check_password( return 0; } + +#define N_SUGGESTIONS 6 + +int suggest_passwords(void) { + _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL; + _cleanup_strv_free_erase_ char **suggestions = NULL; + _cleanup_(erase_and_freep) char *joined = NULL; + char buf[PWQ_MAX_ERROR_MESSAGE_LEN]; + void *auxerror; + size_t i; + int r; + + pwq = pwquality_default_settings(); + if (!pwq) + return log_oom(); + + r = pwquality_read_config(pwq, NULL, &auxerror); + if (r < 0) + log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + pwquality_maybe_disable_dictionary(pwq); + + suggestions = new0(char*, N_SUGGESTIONS); + if (!suggestions) + return log_oom(); + + for (i = 0; i < N_SUGGESTIONS; i++) { + _cleanup_free_ char *suggestion = NULL; + + r = pwquality_generate(pwq, 64, suggestions + i); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate password, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, NULL)); + } + + joined = strv_join(suggestions, " "); + if (!joined) + return log_oom(); + + log_info("Password suggestions: %s", joined); + return 0; +} + #else + int quality_check_password(UserRecord *hr, sd_bus_error *error) { assert(hr); return 0; } +int suggest_passwords(void) { + return 0; +} #endif diff --git a/src/home/pwquality-util.h b/src/home/pwquality-util.h index b44150b30568e..d61c04c34216f 100644 --- a/src/home/pwquality-util.h +++ b/src/home/pwquality-util.h @@ -5,3 +5,5 @@ #include "user-record.h" int quality_check_password(UserRecord *hr, UserRecord *secret, sd_bus_error *error); + +int suggest_passwords(void); From 8e6920392b16a20210d40c2e33da05e3b3b9ff60 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 19:06:26 +0200 Subject: [PATCH 075/109] home: add pam_systemd_home.so PAM hookup In a way fixes: https://bugs.freedesktop.org/show_bug.cgi?id=67474 --- factory/etc/pam.d/system-auth | 7 +- meson.build | 20 + src/home/meson.build | 9 + src/home/pam_systemd_home.c | 836 ++++++++++++++++++++++++++++++++++ src/home/pam_systemd_home.sym | 12 + src/shared/meson.build | 6 +- src/shared/pam-util.c | 81 ++++ src/shared/pam-util.h | 15 + 8 files changed, 983 insertions(+), 3 deletions(-) create mode 100644 src/home/pam_systemd_home.c create mode 100644 src/home/pam_systemd_home.sym create mode 100644 src/shared/pam-util.c create mode 100644 src/shared/pam-util.h diff --git a/factory/etc/pam.d/system-auth b/factory/etc/pam.d/system-auth index 747efc1fbdb69..02544808e6246 100644 --- a/factory/etc/pam.d/system-auth +++ b/factory/etc/pam.d/system-auth @@ -4,16 +4,19 @@ # unmodified you are not building systems safely and securely. auth sufficient pam_unix.so nullok try_first_pass +auth sufficient pam_systemd_home.so auth required pam_deny.so account required pam_nologin.so account sufficient pam_unix.so -account required pam_permit.so +account sufficient pam_systemd_home.so password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok +password sufficient pam_systemd_home.so password required pam_deny.so -session optional pam_keyinit.so revoke -session optional pam_loginuid.so +-session optional pam_systemd_home.so -session optional pam_systemd.so -session sufficient pam_unix.so +session required pam_unix.so diff --git a/meson.build b/meson.build index 949a0648c6bf3..b21b14b0f9831 100644 --- a/meson.build +++ b/meson.build @@ -2011,6 +2011,26 @@ if conf.get('ENABLE_HOMED') == 1 install_rpath : rootlibexecdir, install : true, install_dir : rootbindir) + + if conf.get('HAVE_PAM') == 1 + version_script_arg = join_paths(project_source_root, pam_systemd_home_sym) + pam_systemd = shared_library( + 'pam_systemd_home', + pam_systemd_home_c, + name_prefix : '', + include_directories : includes, + link_args : ['-shared', + '-Wl,--version-script=' + version_script_arg], + link_with : [libsystemd_static, + libshared_static], + dependencies : [threads, + libpam, + libpam_misc, + libcrypt], + link_depends : pam_systemd_home_sym, + install : true, + install_dir : pamlibdir) + endif endif foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] diff --git a/src/home/meson.build b/src/home/meson.build index 62602aebb3c10..33bf36345d4fb 100644 --- a/src/home/meson.build +++ b/src/home/meson.build @@ -44,6 +44,15 @@ homectl_sources = files(''' user-record-util.h '''.split()) +pam_systemd_home_sym = 'src/home/pam_systemd_home.sym' +pam_systemd_home_c = files(''' + home-util.c + home-util.h + pam_systemd_home.c + user-record-util.c + user-record-util.h +'''.split()) + if conf.get('ENABLE_HOMED') == 1 install_data('org.freedesktop.home1.conf', install_dir : dbuspolicydir) diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c new file mode 100644 index 0000000000000..6f795ecb96103 --- /dev/null +++ b/src/home/pam_systemd_home.c @@ -0,0 +1,836 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "sd-bus.h" + +#include "bus-common-errors.h" +#include "errno-util.h" +#include "fd-util.h" +#include "home-util.h" +#include "memory-util.h" +#include "pam-util.h" +#include "parse-util.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-record.h" + +/* Used for the "systemd-user-record-is-homed" PAM data field, to indicate whether we know whether this user + * record is managed by homed or by something else. */ +#define USER_RECORD_IS_HOMED INT_TO_PTR(1) +#define USER_RECORD_IS_OTHER INT_TO_PTR(2) + +static int parse_argv( + pam_handle_t *handle, + int argc, const char **argv, + bool *please_suspend, + bool *debug) { + + int i; + + assert(argc >= 0); + assert(argc == 0 || argv); + + for (i = 0; i < argc; i++) { + const char *v; + + if ((v = startswith(argv[1], "suspend="))) { + int k; + + k = parse_boolean(v); + if (k < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse suspend-please= argument, ignoring: %s", v); + else if (please_suspend) + *please_suspend = k; + + } else if ((v = startswith(argv[i], "debug="))) { + int k; + + k = parse_boolean(v); + if (k < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", v); + else if (debug) + *debug = k; + + } else + pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]); + } + + return 0; +} + +static int acquire_user_record( + pam_handle_t *handle, + UserRecord **ret_record) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + const char *username = NULL, *json = NULL; + const void *b = NULL; + int r; + + assert(handle); + + r = pam_get_user(handle, &username, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); + return r; + } + + if (isempty(username)) { + pam_syslog(handle, LOG_ERR, "User name not valid."); + return PAM_SERVICE_ERR; + } + + /* Let's bypass all IPC complexity for the two user names we know for sure we don't manage. */ + if (STR_IN_SET(username, "root", NOBODY_USER_NAME)) + return PAM_USER_UNKNOWN; + + /* Let's check if a previous run determined that this user is not managed by homed. If so, let's exit early */ + r = pam_get_data(handle, "systemd-user-record-is-homed", &b); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + /* Failure */ + pam_syslog(handle, LOG_ERR, "Failed to get PAM user record is homed flag: %s", pam_strerror(handle, r)); + return r; + } else if (b == NULL) + /* Nothing cached yet, need to acquire fresh */ + json = NULL; + else if (b != USER_RECORD_IS_HOMED) + /* Definitely not a homed record */ + return PAM_USER_UNKNOWN; + else { + /* It's a homed record, let's use the cache, so that we can share it between the session and + * the authentication hooks */ + r = pam_get_data(handle, "systemd-user-record", (const void**) &json); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + } + + if (!json) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *json_copy = NULL; + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + username); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + pam_syslog(handle, LOG_DEBUG, "systemd-homed is not available: %s", bus_error_message(&error, r)); + goto user_unknown; + } + + if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) { + pam_syslog(handle, LOG_DEBUG, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r)); + goto user_unknown; + } + + pam_syslog(handle, LOG_ERR, "Failed to query user record: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL); + if (r < 0) + return pam_bus_log_parse_error(handle, r); + + json_copy = strdup(json); + if (!json_copy) + return pam_log_oom(handle); + + r = pam_set_data(handle, "systemd-user-record", json_copy, pam_cleanup_free); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + + TAKE_PTR(json_copy); + + r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_HOMED, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag: %s", pam_strerror(handle, r)); + return r; + } + } + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + ur = user_record_new(); + if (!ur) + return pam_log_oom(handle); + + r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + if (!streq_ptr(username, ur->user_name)) { + pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name."); + return PAM_SERVICE_ERR; + } + + if (ret_record) + *ret_record = TAKE_PTR(ur); + + return PAM_SUCCESS; + +user_unknown: + /* Cache this, so that we don't check again */ + r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_OTHER, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag, ignoring: %s", pam_strerror(handle, r)); + + return PAM_USER_UNKNOWN; +} + +static int release_user_record(pam_handle_t *handle) { + int r, k; + + r = pam_set_data(handle, "systemd-user-record", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r)); + + k = pam_set_data(handle, "systemd-user-record-is-homed", NULL, NULL); + if (k != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record is homed flag: %s", pam_strerror(handle, k)); + + return IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA) ? k : r; +} + +static void cleanup_home_fd(pam_handle_t *handle, void *data, int error_status) { + safe_close(PTR_TO_FD(data)); +} + +static int acquire_home( + pam_handle_t *handle, + bool please_authenticate, + bool please_suspend, + bool debug) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + _cleanup_close_ int acquired_fd = -1; + const void *home_fd_ptr = NULL; + const char *password = NULL; + unsigned n_attempts = 0; + int r; + + assert(handle); + + /* This acquires a reference to a home directory in one of two ways: if please_authenticate is true, + * then we'll call AcquireHome() after asking the user for a password. Otherwise it tries to call + * RefHome() and if that fails queries the user for a password and uses AcquireHome(). + * + * The idea is that the PAM authentication hook sets please_authenticate and thus always + * authenticates, while the other PAM hooks unset it so that they can a ref of their own without + * authentication if possible, but with authentication if necessary. */ + + /* If we already have acquired the fd, let's shortcut this */ + r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr); + if (r == PAM_SUCCESS && PTR_TO_INT(home_fd_ptr) >= 0) + return PAM_SUCCESS; + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + /* Eventually, we should analyze the record we got here, and if there's some other authentication + * data configured in it query for that. For now we don't care and just unconditionally ask for a + * password. */ + + if (please_authenticate) { + r = pam_get_authtok(handle, PAM_AUTHTOK, &password, "Password: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + secret = user_record_new(); + if (!secret) + return pam_log_oom(handle); + + r = user_record_set_password(secret, STRV_MAKE(password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + } + + /* Implement our own retry loop here instead of relying on the PAM client's one. That's because it + * might happen that the the record we stored on the host does not match the encryption password of + * the LUKS image in case the image was used in a different system where the password was + * changed. In that case it will happen that the LUKS password and the host password are + * different, and we handle that by collecting and passing multiple passwords in that case. Hence we + * treat bad passwords as a request to collect one more password and pass the new all all previously + * used passwords again. */ + + for (;;) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(erase_and_freep) char *newp = NULL; + + password = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + secret ? "AcquireHome" : "RefHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", ur->user_name); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + if (secret) { + r = bus_message_append_secret(m, secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + } + + r = sd_bus_message_append(m, "b", please_suspend); + if (r < 0) + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_ABSENT)) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", ur->user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_PERM_DENIED; + } else if (sd_bus_error_has_name(&error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too frequent unsuccessful login attempts for user %s, try again later.", ur->user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_MAXTRIES; + } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) { + + if (++n_attempts >= 5) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many unsuccessful login attempts for user %s, refusing.", ur->user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_MAXTRIES; + } + + /* This didn't work? Ask for an additional password */ + + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password incorrect or not sufficient for authentication of user %s, please try again: ", ur->user_name); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to prompt for additional password: %s", pam_strerror(handle, r)); + return r; + } + + password = newp; + + } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE) || + sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)) { + + int q; + + /* Only on RefHome(): We can't access the home directory currently, unless + * it's unlocked with a password. Try to do so hence, ask for a password. */ + + q = pam_get_authtok(handle, PAM_AUTHTOK, &password, "Password: "); + if (q != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + pam_syslog(handle, LOG_DEBUG, "Failed to get password: %s", pam_strerror(handle, q)); + + /* This will fail in certain environments, for example when we are + * called from OpenSSH's account or session hooks, or in systemd's + * per-service PAM logic. In that case, print a friendly message and + * accept failure. */ + + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE)) + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently not active, please log in locally first.", ur->user_name); + else { + assert(sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)); + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently locked, please unlock locally first.", ur->user_name); + } + + return PAM_PERM_DENIED; + } + } else { + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + if (isempty(password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + } else { + int fd; + + r = sd_bus_message_read(reply, "h", &fd); + if (r < 0) + return pam_bus_log_parse_error(handle, r); + + acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (acquired_fd < 0) { + pam_syslog(handle, LOG_ERR, "Failed to duplicate acquired fd: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + break; + } + + if (password) { + /* We acquire a new password? Then add it to our set of secrets */ + if (!secret) { + secret = user_record_new(); + if (!secret) + return pam_log_oom(handle); + } + + /* Add the password to the list of passwords we already have, in case the + * record and LUKS passwords differ and we need multiple to unlock things */ + r = user_record_set_password(secret, STRV_MAKE(password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + } + + /* Try again */ + } + + r = pam_set_data(handle, "systemd-home-fd", FD_TO_PTR(acquired_fd), cleanup_home_fd); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r)); + return r; + } + TAKE_FD(acquired_fd); + + if (secret) { + /* We likely just activated the home directory, let's flush out the user record, since a + * newer embedded user record might have been acquired from the activation. */ + + r = release_user_record(handle); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) + return r; + } + + pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name); + + return PAM_SUCCESS; +} + +static int release_home_fd(pam_handle_t *handle) { + const void *home_fd_ptr = NULL; + int r; + + r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr); + if (r == PAM_NO_MODULE_DATA || PTR_TO_FD(home_fd_ptr) < 0) + return PAM_NO_MODULE_DATA; + + r = pam_set_data(handle, "systemd-home-fd", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM home reference fd: %s", pam_strerror(handle, r)); + + return r; +} + +_public_ PAM_EXTERN int pam_sm_authenticate( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + bool debug = true, suspend_please = false; + + if (parse_argv(handle, + argc, argv, + &suspend_please, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed authenticating"); + + return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug); +} + +_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_open_session( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + bool debug = true, suspend_please = false; + int r; + + if (parse_argv(handle, + argc, argv, + &suspend_please, + &debug) < 0) + return PAM_SESSION_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session start"); + + r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug); + if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */ + return PAM_SUCCESS; + if (r != PAM_SUCCESS) + return r; + + r = pam_putenv(handle, "SYSTEMD_HOME=1"); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $SYSTEMD_HOME: %s", pam_strerror(handle, r)); + return r; + } + + /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are + * not going to process the bus connection in that time, so let's better close before the daemon + * kicks us off because we are not processing anything. */ + (void) pam_release_bus_connection(handle); + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_close_session( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + const char *username = NULL; + bool debug = true; + int r; + + if (parse_argv(handle, + argc, argv, + NULL, + &debug) < 0) + return PAM_SESSION_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session end"); + + /* Let's explicitly drop the reference to the homed session, so that the subsequent ReleaseHome() + * call will be able to do its thing. */ + r = release_home_fd(handle); + if (r == PAM_NO_MODULE_DATA) /* Nothing to do, we never acquired an fd */ + return PAM_SUCCESS; + if (r != PAM_SUCCESS) + return r; + + r = pam_get_user(handle, &username, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); + return r; + } + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ReleaseHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", username); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) + pam_syslog(handle, LOG_NOTICE, "Not deactivating home directory of %s, as it is still used.", username); + else { + pam_syslog(handle, LOG_ERR, "Failed to release user home: %s", bus_error_message(&error, r)); + return PAM_SESSION_ERR; + } + } + + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_acct_mgmt( + pam_handle_t *handle, + int flags, + int argc, + const char **argv) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + bool debug = true, please_suspend = false; + usec_t t; + int r; + + if (parse_argv(handle, + argc, argv, + &please_suspend, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management"); + + r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug); + if (r == PAM_USER_UNKNOWN) + return PAM_SUCCESS; /* we don't have anything to say about users we don't manage */ + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + r = user_record_test_blocked(ur); + switch (r) { + + case -ESTALE: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is newer than current system time, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -ENOLCK: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is blocked, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -EL2HLT: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid yet, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -EL3HLT: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid anymore, prohibiting access."); + return PAM_ACCT_EXPIRED; + + default: + if (r < 0) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access."); + return PAM_ACCT_EXPIRED; + } + + break; + } + + t = user_record_ratelimit_next_try(ur); + if (t != USEC_INFINITY) { + usec_t n = now(CLOCK_REALTIME); + + if (t > n) { + char buf[FORMAT_TIMESPAN_MAX]; + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many logins, try again in %s.", + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC)); + + return PAM_MAXTRIES; + } + } + + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_chauthtok( + pam_handle_t *handle, + int flags, + int argc, + const char **argv) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *old_secret = NULL, *new_secret = NULL; + const char *old_password = NULL, *new_password = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + unsigned n_attempts = 0; + bool debug = true; + int r; + + if (parse_argv(handle, + argc, argv, + NULL, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management"); + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + /* Regardless if in the PAM_PRELIM_CHECK phase or not let's ask for the passwords with the right + * prompts, so that the password is definitely cached. */ + r = pam_get_authtok(handle, PAM_OLDAUTHTOK, &old_password, "Old password: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(old_password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + /* Is the new password already cached? */ + r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password); + if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) { + pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r)); + return r; + } + + if (isempty(new_password)) { + /* No, it's not cached, then let's ask for the password and its verification, and cache + * it. */ + + r = pam_get_authtok_noverify(handle, &new_password, "New password: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get new password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(new_password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = pam_get_authtok_verify(handle, &new_password, "new password: "); /* Lower case, since PAM prefixes 'Repeat' */ + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get password again: %s", pam_strerror(handle, r)); + return r; + } + + // FIXME: pam_pwquality will ask for the password a third time. It really shouldn't do + // that, and instead assume the password was already verified once when it is found to be + // cached already. needs to be fixed in pam_pwquality + } + + /* Now everything is cached and checked, let's exit from the preliminary check */ + if (FLAGS_SET(flags, PAM_PRELIM_CHECK)) + return PAM_SUCCESS; + + old_secret = user_record_new(); + if (!old_secret) + return pam_log_oom(handle); + + r = user_record_set_password(old_secret, STRV_MAKE(old_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store old password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + new_secret = user_record_new(); + if (!new_secret) + return pam_log_oom(handle); + + r = user_record_set_password(new_secret, STRV_MAKE(new_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store new password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ChangePasswordHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", ur->user_name); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = bus_message_append_secret(m, new_secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = bus_message_append_secret(m, old_secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) { + pam_error(handle, "Home of user %s is currently being modified, please come back later.", ur->user_name); + return PAM_PERM_DENIED; + } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_ABSENT)) { + pam_error(handle, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", ur->user_name); + return PAM_PERM_DENIED; + } else if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) { + pam_syslog(handle, LOG_ERR, "Failed to change password for home: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + } else { + pam_syslog(handle, LOG_NOTICE, "Successfully changed password for user %s.", ur->user_name); + return PAM_SUCCESS; + } + + /* Invalidate old token */ + r = pam_set_item(handle, PAM_OLDAUTHTOK, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to reset cached password: %s", pam_strerror(handle, r)); + return r; + } + + if (++n_attempts >= 5) + break; + + /* Acquire new old token */ + r = pam_get_authtok(handle, PAM_OLDAUTHTOK, &old_password, + "Old password incorrect or not sufficient for authentication, please try again: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(old_password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_password(old_secret, STRV_MAKE(old_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set old password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + /* Try again */ + }; + + pam_syslog(handle, LOG_NOTICE, "Failed to change password for user %s: %m", ur->user_name); + return PAM_MAXTRIES; +} diff --git a/src/home/pam_systemd_home.sym b/src/home/pam_systemd_home.sym new file mode 100644 index 0000000000000..daec0499e1148 --- /dev/null +++ b/src/home/pam_systemd_home.sym @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +{ +global: + pam_sm_authenticate; + pam_sm_setcred; + pam_sm_open_session; + pam_sm_close_session; + pam_sm_acct_mgmt; + pam_sm_chauthtok; +local: *; +}; diff --git a/src/shared/meson.build b/src/shared/meson.build index 234526d7d01d6..1dc6a721c4461 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -236,6 +236,10 @@ if conf.get('HAVE_KMOD') == 1 shared_sources += files('module-util.c') endif +if conf.get('HAVE_PAM') == 1 + shared_sources += files('pam-util.c', 'pam-util.h') +endif + generate_ip_protocol_list = find_program('generate-ip-protocol-list.sh') ip_protocol_list_txt = custom_target( 'ip-protocol-list.txt', @@ -293,7 +297,7 @@ libshared_deps = [threads, libxz, liblz4, libblkid, - libcrypt] + libpam] libshared_sym_path = '@0@/libshared.sym'.format(meson.current_source_dir()) diff --git a/src/shared/pam-util.c b/src/shared/pam-util.c new file mode 100644 index 0000000000000..f000798ce0683 --- /dev/null +++ b/src/shared/pam-util.c @@ -0,0 +1,81 @@ +#include +#include +#include + +#include "alloc-util.h" +#include "errno-util.h" +#include "macro.h" +#include "pam-util.h" + +int pam_log_oom(pam_handle_t *handle) { + /* This is like log_oom(), but uses PAM logging */ + pam_syslog(handle, LOG_ERR, "Out of memory."); + return PAM_BUF_ERR; +} + +int pam_bus_log_create_error(pam_handle_t *handle, int r) { + /* This is like bus_log_create_error(), but uses PAM logging */ + pam_syslog(handle, LOG_ERR, "Failed to create bus message: %s", strerror_safe(r)); + return PAM_BUF_ERR; +} + +int pam_bus_log_parse_error(pam_handle_t *handle, int r) { + /* This is like bus_log_parse_error(), but uses PAM logging */ + pam_syslog(handle, LOG_ERR, "Failed to parse bus message: %s", strerror_safe(r)); + return PAM_BUF_ERR; +} + +static void cleanup_system_bus(pam_handle_t *handle, void *data, int error_status) { + sd_bus_flush_close_unref(data); +} + +int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret) { + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + int r; + + assert(handle); + assert(ret); + + /* We cache the bus connection so that we can share it between the session and the authentication hooks */ + r = pam_get_data(handle, "systemd-system-bus", (const void**) &bus); + if (r == PAM_SUCCESS && bus) { + *ret = sd_bus_ref(TAKE_PTR(bus)); /* Increase the reference counter, so that the PAM data stays valid */ + return PAM_SUCCESS; + } + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + pam_syslog(handle, LOG_ERR, "Failed to get bus connection: %s", pam_strerror(handle, r)); + return r; + } + + r = sd_bus_open_system(&bus); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + r = pam_set_data(handle, "systemd-system-bus", bus, cleanup_system_bus); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r)); + return r; + } + + sd_bus_ref(bus); + *ret = TAKE_PTR(bus); + + return PAM_SUCCESS; +} + +int pam_release_bus_connection(pam_handle_t *handle) { + int r; + + r = pam_set_data(handle, "systemd-system-bus", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r)); + + return r; +} + +void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status) { + /* A generic destructor for pam_set_data() that just frees the specified data */ + free(data); +} diff --git a/src/shared/pam-util.h b/src/shared/pam-util.h new file mode 100644 index 0000000000000..26d07b7f0cbed --- /dev/null +++ b/src/shared/pam-util.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" + +int pam_log_oom(pam_handle_t *handle); +int pam_bus_log_create_error(pam_handle_t *handle, int r); +int pam_bus_log_parse_error(pam_handle_t *handle, int r); + +int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret); +int pam_release_bus_connection(pam_handle_t *handle); + +void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status); From cdc615388e07292c06d25f3c9fd62f01831a37f4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Sun, 7 Jul 2019 17:30:15 +0200 Subject: [PATCH 076/109] test: add test case for homed --- test/TEST-36-HOMED/Makefile | 1 + test/TEST-36-HOMED/test.sh | 49 +++++++++++++++++++++++++++++++++ test/TEST-36-HOMED/testsuite.sh | 40 +++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 120000 test/TEST-36-HOMED/Makefile create mode 100755 test/TEST-36-HOMED/test.sh create mode 100755 test/TEST-36-HOMED/testsuite.sh diff --git a/test/TEST-36-HOMED/Makefile b/test/TEST-36-HOMED/Makefile new file mode 120000 index 0000000000000..e9f93b1104cd2 --- /dev/null +++ b/test/TEST-36-HOMED/Makefile @@ -0,0 +1 @@ +../TEST-01-BASIC/Makefile \ No newline at end of file diff --git a/test/TEST-36-HOMED/test.sh b/test/TEST-36-HOMED/test.sh new file mode 100755 index 0000000000000..8fee9e585616f --- /dev/null +++ b/test/TEST-36-HOMED/test.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e +TEST_DESCRIPTION="testing homed" +TEST_NO_QEMU=1 + +. $TEST_BASE_DIR/test-functions + +test_setup() { + create_empty_image + mkdir -p $TESTDIR/root + mount ${LOOPDEV}p1 $TESTDIR/root + + ( + LOG_LEVEL=5 + eval $(udevadm info --export --query=env --name=${LOOPDEV}p2) + + setup_basic_environment + + # mask some services that we do not want to run in these tests + ln -fs /dev/null $initdir/etc/systemd/system/systemd-hwdb-update.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-journal-catalog-update.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-networkd.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-networkd.socket + ln -fs /dev/null $initdir/etc/systemd/system/systemd-resolved.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-machined.service + + # setup the testsuite service + cat >$initdir/etc/systemd/system/testsuite.service < /testok + +exit 0 From 50a2d98e12324fa2af6f3bcd6b5c1fff4ca81788 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 16:22:35 +0200 Subject: [PATCH 077/109] logind: port to UserRecord object This changes the user tracking of logind to use the new-style UserRecord object. In a later commit this enables us to do per-user resource management. --- src/login/logind-core.c | 39 ++++++++++--------- src/login/logind-dbus.c | 10 ++--- src/login/logind-seat.c | 8 ++-- src/login/logind-session-dbus.c | 22 +++++------ src/login/logind-session.c | 20 +++++----- src/login/logind-user-dbus.c | 68 +++++++++++++++++++++++++++++---- src/login/logind-user.c | 58 ++++++++++++---------------- src/login/logind-user.h | 10 ++--- src/login/logind.h | 5 ++- 9 files changed, 144 insertions(+), 96 deletions(-) diff --git a/src/login/logind-core.c b/src/login/logind-core.c index 1d21e90a2eaa9..7a29ccdf91ebd 100644 --- a/src/login/logind-core.c +++ b/src/login/logind-core.c @@ -28,6 +28,7 @@ #include "terminal-util.h" #include "udev-util.h" #include "user-util.h" +#include "userdb.h" void manager_reset_config(Manager *m) { assert(m); @@ -139,21 +140,18 @@ int manager_add_session(Manager *m, const char *id, Session **_session) { int manager_add_user( Manager *m, - uid_t uid, - gid_t gid, - const char *name, - const char *home, + UserRecord *ur, User **_user) { User *u; int r; assert(m); - assert(name); + assert(ur); - u = hashmap_get(m->users, UID_TO_PTR(uid)); + u = hashmap_get(m->users, UID_TO_PTR(ur->uid)); if (!u) { - r = user_new(&u, m, uid, gid, name, home); + r = user_new(&u, m, ur); if (r < 0) return r; } @@ -169,32 +167,35 @@ int manager_add_user_by_name( const char *name, User **_user) { - const char *home = NULL; - uid_t uid; - gid_t gid; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; int r; assert(m); assert(name); - r = get_user_creds(&name, &uid, &gid, &home, NULL, 0); + r = userdb_by_name(name, 0, &ur); if (r < 0) return r; - return manager_add_user(m, uid, gid, name, home, _user); + return manager_add_user(m, ur, _user); } -int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user) { - struct passwd *p; +int manager_add_user_by_uid( + Manager *m, + uid_t uid, + User **_user) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + int r; assert(m); + assert(uid_is_valid(uid)); - errno = 0; - p = getpwuid(uid); - if (!p) - return errno_or_else(ENOENT); + r = userdb_by_uid(uid, 0, &ur); + if (r < 0) + return r; - return manager_add_user(m, uid, p->pw_gid, p->pw_name, p->pw_dir, _user); + return manager_add_user(m, ur, _user); } int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **ret) { diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c index b36616e55a03b..695e6dfbe21ea 100644 --- a/src/login/logind-dbus.c +++ b/src/login/logind-dbus.c @@ -540,8 +540,8 @@ static int method_list_sessions(sd_bus_message *message, void *userdata, sd_bus_ r = sd_bus_message_append(reply, "(susso)", session->id, - (uint32_t) session->user->uid, - session->user->name, + (uint32_t) session->user->user_record->uid, + session->user->user_record->user_name, session->seat ? session->seat->id : "", p); if (r < 0) @@ -581,8 +581,8 @@ static int method_list_users(sd_bus_message *message, void *userdata, sd_bus_err return -ENOMEM; r = sd_bus_message_append(reply, "(uso)", - (uint32_t) user->uid, - user->name, + (uint32_t) user->user_record->uid, + user->user_record->user_name, p); if (r < 0) return r; @@ -1491,7 +1491,7 @@ static int have_multiple_sessions( * count, and non-login sessions do not count either. */ HASHMAP_FOREACH(session, m->sessions, i) if (session->class == SESSION_USER && - session->user->uid != uid) + session->user->user_record->uid != uid) return true; return false; diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c index 094fcd668dcb1..a2b4cac976d03 100644 --- a/src/login/logind-seat.c +++ b/src/login/logind-seat.c @@ -120,7 +120,7 @@ int seat_save(Seat *s) { "ACTIVE=%s\n" "ACTIVE_UID="UID_FMT"\n", s->active->id, - s->active->user->uid); + s->active->user->user_record->uid); } if (s->sessions) { @@ -138,7 +138,7 @@ int seat_save(Seat *s) { LIST_FOREACH(sessions_by_seat, i, s->sessions) fprintf(f, UID_FMT"%c", - i->user->uid, + i->user->user_record->uid, i->sessions_by_seat_next ? ' ' : '\n'); } @@ -217,8 +217,8 @@ int seat_apply_acls(Seat *s, Session *old_active) { r = devnode_acl_all(s->id, false, - !!old_active, old_active ? old_active->user->uid : 0, - !!s->active, s->active ? s->active->user->uid : 0); + !!old_active, old_active ? old_active->user->user_record->uid : 0, + !!s->active, s->active ? s->active->user->user_record->uid : 0); if (r < 0) return log_error_errno(r, "Failed to apply ACLs: %m"); diff --git a/src/login/logind-session-dbus.c b/src/login/logind-session-dbus.c index a37bbf56b75ad..59e6fe40091fd 100644 --- a/src/login/logind-session-dbus.c +++ b/src/login/logind-session-dbus.c @@ -44,7 +44,7 @@ static int property_get_user( if (!p) return -ENOMEM; - return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->uid, p); + return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->user_record->uid, p); } static int property_get_name( @@ -62,7 +62,7 @@ static int property_get_name( assert(reply); assert(s); - return sd_bus_message_append(reply, "s", s->user->name); + return sd_bus_message_append(reply, "s", s->user->user_record->user_name); } static int property_get_seat( @@ -169,7 +169,7 @@ int bus_session_method_terminate(sd_bus_message *message, void *userdata, sd_bus "org.freedesktop.login1.manage", NULL, false, - s->user->uid, + s->user->user_record->uid, &s->manager->polkit_registry, error); if (r < 0) @@ -211,7 +211,7 @@ int bus_session_method_lock(sd_bus_message *message, void *userdata, sd_bus_erro "org.freedesktop.login1.lock-sessions", NULL, false, - s->user->uid, + s->user->user_record->uid, &s->manager->polkit_registry, error); if (r < 0) @@ -247,7 +247,7 @@ static int method_set_idle_hint(sd_bus_message *message, void *userdata, sd_bus_ if (r < 0) return r; - if (uid != 0 && uid != s->user->uid) + if (uid != 0 && uid != s->user->user_record->uid) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set idle hint"); session_set_idle_hint(s, b); @@ -276,7 +276,7 @@ static int method_set_locked_hint(sd_bus_message *message, void *userdata, sd_bu if (r < 0) return r; - if (uid != 0 && uid != s->user->uid) + if (uid != 0 && uid != s->user->user_record->uid) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set locked hint"); session_set_locked_hint(s, b); @@ -315,7 +315,7 @@ int bus_session_method_kill(sd_bus_message *message, void *userdata, sd_bus_erro "org.freedesktop.login1.manage", NULL, false, - s->user->uid, + s->user->user_record->uid, &s->manager->polkit_registry, error); if (r < 0) @@ -351,7 +351,7 @@ static int method_take_control(sd_bus_message *message, void *userdata, sd_bus_e if (r < 0) return r; - if (uid != 0 && (force || uid != s->user->uid)) + if (uid != 0 && (force || uid != s->user->user_record->uid)) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may take control"); r = session_set_controller(s, sd_bus_message_get_sender(message), force, true); @@ -520,7 +520,7 @@ static int method_set_brightness(sd_bus_message *message, void *userdata, sd_bus if (r < 0) return r; - if (uid != 0 && uid != s->user->uid) + if (uid != 0 && uid != s->user->user_record->uid) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may change brightness."); r = sd_device_new_from_subsystem_sysname(&d, subsystem, name); @@ -821,7 +821,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) { "session_fd=%d seat=%s vtnr=%u", s->id, p, - (uint32_t) s->user->uid, + (uint32_t) s->user->user_record->uid, s->user->runtime_path, fifo_fd, s->seat ? s->seat->id : "", @@ -833,7 +833,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) { p, s->user->runtime_path, fifo_fd, - (uint32_t) s->user->uid, + (uint32_t) s->user->user_record->uid, s->seat ? s->seat->id : "", (uint32_t) s->vtnr, false); diff --git a/src/login/logind-session.c b/src/login/logind-session.c index 7e8025a0ea22b..196659d2f2544 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -231,8 +231,8 @@ int session_save(Session *s) { "IS_DISPLAY=%i\n" "STATE=%s\n" "REMOTE=%i\n", - s->user->uid, - s->user->name, + s->user->user_record->uid, + s->user->user_record->user_name, session_is_active(s), s->user->display == s, session_state_to_string(session_get_state(s)), @@ -642,7 +642,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er if (!scope) return log_oom(); - description = strjoina("Session ", s->id, " of user ", s->user->name); + description = strjoina("Session ", s->id, " of user ", s->user->user_record->user_name); r = manager_start_scope( s->manager, @@ -658,7 +658,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er "systemd-user-sessions.service", s->user->runtime_dir_service, s->user->service), - s->user->home, + user_record_home_directory(s->user->user_record), properties, error, &s->scope_job); @@ -699,9 +699,9 @@ int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error) { log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SESSION_START_STR, "SESSION_ID=%s", s->id, - "USER_ID=%s", s->user->name, + "USER_ID=%s", s->user->user_record->user_name, "LEADER="PID_FMT, s->leader, - LOG_MESSAGE("New session %s of user %s.", s->id, s->user->name)); + LOG_MESSAGE("New session %s of user %s.", s->id, s->user->user_record->user_name)); if (!dual_timestamp_is_set(&s->timestamp)) dual_timestamp_get(&s->timestamp); @@ -751,7 +751,7 @@ static int session_stop_scope(Session *s, bool force) { s->scope_job = mfree(s->scope_job); /* Optionally, let's kill everything that's left now. */ - if (force || manager_shall_kill(s->manager, s->user->name)) { + if (force || manager_shall_kill(s->manager, s->user->user_record->user_name)) { r = manager_stop_unit(s->manager, s->scope, &error, &s->scope_job); if (r < 0) { @@ -767,7 +767,7 @@ static int session_stop_scope(Session *s, bool force) { * Session stop is quite significant on its own, let's log it. */ log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, "SESSION_ID=%s", s->id, - "USER_ID=%s", s->user->name, + "USER_ID=%s", s->user->user_record->user_name, "LEADER="PID_FMT, s->leader, LOG_MESSAGE("Session %s logged out. Waiting for processes to exit.", s->id)); } @@ -825,7 +825,7 @@ int session_finalize(Session *s) { log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SESSION_STOP_STR, "SESSION_ID=%s", s->id, - "USER_ID=%s", s->user->name, + "USER_ID=%s", s->user->user_record->user_name, "LEADER="PID_FMT, s->leader, LOG_MESSAGE("Removed session %s.", s->id)); @@ -1194,7 +1194,7 @@ int session_prepare_vt(Session *s) { if (vt < 0) return vt; - r = fchown(vt, s->user->uid, -1); + r = fchown(vt, s->user->user_record->uid, -1); if (r < 0) { r = log_error_errno(errno, "Cannot change owner of /dev/tty%u: %m", diff --git a/src/login/logind-user-dbus.c b/src/login/logind-user-dbus.c index beb97362e7302..6af226d006e7e 100644 --- a/src/login/logind-user-dbus.c +++ b/src/login/logind-user-dbus.c @@ -16,6 +16,60 @@ #include "strv.h" #include "user-util.h" +static int property_get_uid( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + User *u = userdata; + + assert(bus); + assert(reply); + assert(u); + + return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->uid); +} + +static int property_get_gid( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + User *u = userdata; + + assert(bus); + assert(reply); + assert(u); + + return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->gid); +} + +static int property_get_name( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + User *u = userdata; + + assert(bus); + assert(reply); + assert(u); + + return sd_bus_message_append(reply, "s", u->user_record->user_name); +} + static BUS_DEFINE_PROPERTY_GET2(property_get_state, "s", User, user_get_state, user_state_to_string); static int property_get_display( @@ -153,7 +207,7 @@ int bus_user_method_terminate(sd_bus_message *message, void *userdata, sd_bus_er "org.freedesktop.login1.manage", NULL, false, - u->uid, + u->user_record->uid, &u->manager->polkit_registry, error); if (r < 0) @@ -182,7 +236,7 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error * "org.freedesktop.login1.manage", NULL, false, - u->uid, + u->user_record->uid, &u->manager->polkit_registry, error); if (r < 0) @@ -207,9 +261,9 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error * const sd_bus_vtable user_vtable[] = { SD_BUS_VTABLE_START(0), - SD_BUS_PROPERTY("UID", "u", bus_property_get_uid, offsetof(User, uid), SD_BUS_VTABLE_PROPERTY_CONST), - SD_BUS_PROPERTY("GID", "u", bus_property_get_gid, offsetof(User, gid), SD_BUS_VTABLE_PROPERTY_CONST), - SD_BUS_PROPERTY("Name", "s", NULL, offsetof(User, name), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("UID", "u", property_get_uid, 0, SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("GID", "u", property_get_gid, 0, SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Name", "s", property_get_name, 0, SD_BUS_VTABLE_PROPERTY_CONST), BUS_PROPERTY_DUAL_TIMESTAMP("Timestamp", offsetof(User, timestamp), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RuntimePath", "s", NULL, offsetof(User, runtime_path), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("Service", "s", NULL, offsetof(User, service), SD_BUS_VTABLE_PROPERTY_CONST), @@ -279,7 +333,7 @@ char *user_bus_path(User *u) { assert(u); - if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->uid) < 0) + if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->user_record->uid) < 0) return NULL; return s; @@ -348,7 +402,7 @@ int user_send_signal(User *u, bool new_user) { "/org/freedesktop/login1", "org.freedesktop.login1.Manager", new_user ? "UserNew" : "UserRemoved", - "uo", (uint32_t) u->uid, p); + "uo", (uint32_t) u->user_record->uid, p); } int user_send_changed(User *u, const char *properties, ...) { diff --git a/src/login/logind-user.c b/src/login/logind-user.c index b17fb2e3225e6..8628b704983be 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -38,10 +38,7 @@ int user_new(User **ret, Manager *m, - uid_t uid, - gid_t gid, - const char *name, - const char *home) { + UserRecord *ur) { _cleanup_(user_freep) User *u = NULL; char lu[DECIMAL_STR_MAX(uid_t) + 1]; @@ -49,7 +46,13 @@ int user_new(User **ret, assert(ret); assert(m); - assert(name); + assert(ur); + + if (!ur->user_name) + return -EINVAL; + + if (!uid_is_valid(ur->uid)) + return -EINVAL; u = new(User, 1); if (!u) @@ -57,28 +60,17 @@ int user_new(User **ret, *u = (User) { .manager = m, - .uid = uid, - .gid = gid, + .user_record = user_record_ref(ur), .last_session_timestamp = USEC_INFINITY, }; - u->name = strdup(name); - if (!u->name) - return -ENOMEM; - - u->home = strdup(home); - if (!u->home) + if (asprintf(&u->state_file, "/run/systemd/users/" UID_FMT, ur->uid) < 0) return -ENOMEM; - path_simplify(u->home, true); - - if (asprintf(&u->state_file, "/run/systemd/users/"UID_FMT, uid) < 0) - return -ENOMEM; - - if (asprintf(&u->runtime_path, "/run/user/"UID_FMT, uid) < 0) + if (asprintf(&u->runtime_path, "/run/user/" UID_FMT, ur->uid) < 0) return -ENOMEM; - xsprintf(lu, UID_FMT, uid); + xsprintf(lu, UID_FMT, ur->uid); r = slice_build_subslice(SPECIAL_USER_SLICE, lu, &u->slice); if (r < 0) return r; @@ -91,7 +83,7 @@ int user_new(User **ret, if (r < 0) return r; - r = hashmap_put(m->users, UID_TO_PTR(uid), u); + r = hashmap_put(m->users, UID_TO_PTR(ur->uid), u); if (r < 0) return r; @@ -130,9 +122,9 @@ User *user_free(User *u) { if (u->slice) hashmap_remove_value(u->manager->user_units, u->slice, u); - hashmap_remove_value(u->manager->users, UID_TO_PTR(u->uid), u); + hashmap_remove_value(u->manager->users, UID_TO_PTR(u->user_record->uid), u); - (void) sd_event_source_unref(u->timer_event_source); + sd_event_source_unref(u->timer_event_source); u->service_job = mfree(u->service_job); @@ -141,8 +133,8 @@ User *user_free(User *u) { u->slice = mfree(u->slice); u->runtime_path = mfree(u->runtime_path); u->state_file = mfree(u->state_file); - u->name = mfree(u->name); - u->home = mfree(u->home); + + user_record_unref(u->user_record); return mfree(u); } @@ -170,7 +162,7 @@ static int user_save_internal(User *u) { "NAME=%s\n" "STATE=%s\n" /* friendly user-facing state */ "STOPPING=%s\n", /* low-level state */ - u->name, + u->user_record->user_name, user_state_to_string(user_get_state(u)), yes_no(u->stopping)); @@ -377,7 +369,7 @@ int user_start(User *u) { u->stopping = false; if (!u->started) - log_debug("Starting services for new user %s.", u->name); + log_debug("Starting services for new user %s.", u->user_record->user_name); /* Save the user data so far, because pam_systemd will read the XDG_RUNTIME_DIR out of it while starting up * systemd --user. We need to do user_save_internal() because we have not "officially" started yet. */ @@ -461,7 +453,7 @@ int user_finalize(User *u) { * done. This is called as a result of an earlier user_done() when all jobs are completed. */ if (u->started) - log_debug("User %s logged out.", u->name); + log_debug("User %s logged out.", u->user_record->user_name); LIST_FOREACH(sessions_by_user, s, u->sessions) { k = session_finalize(s); @@ -475,8 +467,8 @@ int user_finalize(User *u) { * cases, as we shouldn't accidentally remove a system service's IPC objects while it is running, just because * a cronjob running as the same user just finished. Hence: exclude system users generally from IPC clean-up, * and do it only for normal users. */ - if (u->manager->remove_ipc && !uid_is_system(u->uid)) { - k = clean_ipc_by_uid(u->uid); + if (u->manager->remove_ipc && !uid_is_system(u->user_record->uid)) { + k = clean_ipc_by_uid(u->user_record->uid); if (k < 0) r = k; } @@ -532,7 +524,7 @@ int user_check_linger_file(User *u) { _cleanup_free_ char *cc = NULL; char *p = NULL; - cc = cescape(u->name); + cc = cescape(u->user_record->user_name); if (!cc) return -ENOMEM; @@ -713,7 +705,7 @@ void user_elect_display(User *u) { /* This elects a primary session for each user, which we call the "display". We try to keep the assignment * stable, but we "upgrade" to better choices. */ - log_debug("Electing new display for user %s", u->name); + log_debug("Electing new display for user %s", u->user_record->user_name); LIST_FOREACH(sessions_by_user, s, u->sessions) { if (!elect_display_filter(s)) { @@ -776,7 +768,7 @@ void user_update_last_session_timer(User *u) { char s[FORMAT_TIMESPAN_MAX]; log_debug("Last session of user '%s' logged out, terminating user context in %s.", - u->name, + u->user_record->user_name, format_timespan(s, sizeof(s), u->manager->user_stop_delay, USEC_PER_MSEC)); } } diff --git a/src/login/logind-user.h b/src/login/logind-user.h index 4bd65d8373414..f8f172cb0f6d5 100644 --- a/src/login/logind-user.h +++ b/src/login/logind-user.h @@ -6,6 +6,7 @@ typedef struct User User; #include "conf-parser.h" #include "list.h" #include "logind.h" +#include "user-record.h" typedef enum UserState { USER_OFFLINE, /* Not logged in at all */ @@ -20,10 +21,9 @@ typedef enum UserState { struct User { Manager *manager; - uid_t uid; - gid_t gid; - char *name; - char *home; + + UserRecord *user_record; + char *state_file; char *runtime_path; @@ -50,7 +50,7 @@ struct User { LIST_FIELDS(User, gc_queue); }; -int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name, const char *home); +int user_new(User **out, Manager *m, UserRecord *ur); User *user_free(User *u); DEFINE_TRIVIAL_CLEANUP_FUNC(User *, user_free); diff --git a/src/login/logind.h b/src/login/logind.h index f260f2dc96de8..65bcf7c5fbc9f 100644 --- a/src/login/logind.h +++ b/src/login/logind.h @@ -12,6 +12,7 @@ #include "list.h" #include "set.h" #include "time-util.h" +#include "user-record.h" typedef struct Manager Manager; @@ -28,7 +29,7 @@ struct Manager { Hashmap *seats; Hashmap *sessions; Hashmap *sessions_by_leader; - Hashmap *users; + Hashmap *users; /* indexed by UID */ Hashmap *inhibitors; Hashmap *buttons; Hashmap *brightness_writers; @@ -131,7 +132,7 @@ int manager_add_device(Manager *m, const char *sysfs, bool master, Device **_dev int manager_add_button(Manager *m, const char *name, Button **_button); int manager_add_seat(Manager *m, const char *id, Seat **_seat); int manager_add_session(Manager *m, const char *id, Session **_session); -int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, const char *home, User **_user); +int manager_add_user(Manager *m, UserRecord *ur, User **_user); int manager_add_user_by_name(Manager *m, const char *name, User **_user); int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user); int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **_inhibitor); From 78c8c7a0aa62c08dacbc6f9f680f4690cc7eeba1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Aug 2019 16:58:06 +0200 Subject: [PATCH 078/109] logind: enforce user record resource settings when user logs in --- src/login/logind-user.c | 97 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/login/logind-user.c b/src/login/logind-user.c index 8628b704983be..d9613c739858b 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -356,6 +356,100 @@ static void user_start_service(User *u) { "Failed to start user service '%s', ignoring: %s", u->service, bus_error_message(&error, r)); } +static int update_slice_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + _cleanup_(user_record_unrefp) UserRecord *ur = userdata; + + assert(m); + assert(ur); + + if (sd_bus_message_is_method_error(m, NULL)) { + log_error_errno(sd_bus_message_get_errno(m), + "Failed to update slice of %s: %s", + ur->user_name, + sd_bus_message_get_error(m)->message); + + return 0; + } + + log_debug("Successfully set slice parameters of %s.", ur->user_name); + return 0; +} + +static int user_update_slice(User *u) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + int r; + + assert(u); + + if (u->user_record->tasks_max == UINT64_MAX && + u->user_record->memory_high == UINT64_MAX && + u->user_record->memory_max == UINT64_MAX && + u->user_record->cpu_weight == UINT64_MAX && + u->user_record->io_weight == UINT64_MAX) + return 0; + + r = sd_bus_message_new_method_call( + u->manager->bus, + &m, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "SetUnitProperties"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "sb", u->slice, true); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'a', "(sv)"); + if (r < 0) + return bus_log_create_error(r); + + if (u->user_record->tasks_max != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "TasksMax", "t", u->user_record->tasks_max); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->memory_high != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", u->user_record->memory_max); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->memory_high != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "MemoryHigh", "t", u->user_record->memory_high); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->cpu_weight != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "CPUWeight", "t", u->user_record->cpu_weight); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->io_weight != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "IOWeight", "t", u->user_record->io_weight); + 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_call_async(u->manager->bus, NULL, m, update_slice_callback, u->user_record, 0); + if (r < 0) + return log_error_errno(r, "Failed to change user slice properties: %m"); + + /* Ref the user record pointer, so that the slot keeps it pinned */ + user_record_ref(u->user_record); + + return 0; +} + int user_start(User *u) { assert(u); @@ -375,6 +469,9 @@ int user_start(User *u) { * systemd --user. We need to do user_save_internal() because we have not "officially" started yet. */ user_save_internal(u); + /* Set slice parameters */ + (void) user_update_slice(u); + /* Start user@UID.service */ user_start_service(u); From c7bef7bc057653e8c67869d8d0fbae210ccccf07 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Aug 2019 13:46:25 +0200 Subject: [PATCH 079/109] logind: honour killProcesses field of user record --- src/login/logind-session.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/login/logind-session.c b/src/login/logind-session.c index 196659d2f2544..e962fe60e2410 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -751,7 +751,10 @@ static int session_stop_scope(Session *s, bool force) { s->scope_job = mfree(s->scope_job); /* Optionally, let's kill everything that's left now. */ - if (force || manager_shall_kill(s->manager, s->user->user_record->user_name)) { + if (force || + (s->user->user_record->kill_processes != 0 && + (s->user->user_record->kill_processes > 0 || + manager_shall_kill(s->manager, s->user->user_record->user_name)))) { r = manager_stop_unit(s->manager, s->scope, &error, &s->scope_job); if (r < 0) { From 07ea9372ddb49c0eec1cbd6442c8ed8c8bc40bb2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Aug 2019 13:46:43 +0200 Subject: [PATCH 080/109] logind: honour per-user stopDelayUSec property --- src/login/logind-user.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/login/logind-user.c b/src/login/logind-user.c index d9613c739858b..903b9f0cb84b8 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -657,6 +657,18 @@ static bool user_unit_active(User *u) { return false; } +static usec_t user_get_stop_delay(User *u) { + assert(u); + + if (u->user_record->stop_delay_usec != UINT64_MAX) + return u->user_record->stop_delay_usec; + + if (user_record_removable(u->user_record) > 0) + return 0; /* For removable users lower the stop delay to zero */ + + return u->manager->user_stop_delay; +} + bool user_may_gc(User *u, bool drop_not_started) { int r; @@ -669,12 +681,16 @@ bool user_may_gc(User *u, bool drop_not_started) { return false; if (u->last_session_timestamp != USEC_INFINITY) { + usec_t user_stop_delay; + /* All sessions have been closed. Let's see if we shall leave the user record around for a bit */ - if (u->manager->user_stop_delay == USEC_INFINITY) + user_stop_delay = user_get_stop_delay(u); + + if (user_stop_delay == USEC_INFINITY) return false; /* Leave it around forever! */ - if (u->manager->user_stop_delay > 0 && - now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, u->manager->user_stop_delay)) + if (user_stop_delay > 0 && + now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, user_stop_delay)) return false; /* Leave it around for a bit longer. */ } @@ -827,6 +843,7 @@ static int user_stop_timeout_callback(sd_event_source *es, uint64_t usec, void * } void user_update_last_session_timer(User *u) { + usec_t user_stop_delay; int r; assert(u); @@ -845,7 +862,8 @@ void user_update_last_session_timer(User *u) { assert(!u->timer_event_source); - if (IN_SET(u->manager->user_stop_delay, 0, USEC_INFINITY)) + user_stop_delay = user_get_stop_delay(u); + if (IN_SET(user_stop_delay, 0, USEC_INFINITY)) return; if (sd_event_get_state(u->manager->event) == SD_EVENT_FINISHED) { @@ -856,7 +874,7 @@ void user_update_last_session_timer(User *u) { r = sd_event_add_time(u->manager->event, &u->timer_event_source, CLOCK_MONOTONIC, - usec_add(u->last_session_timestamp, u->manager->user_stop_delay), 0, + usec_add(u->last_session_timestamp, user_stop_delay), 0, user_stop_timeout_callback, u); if (r < 0) log_warning_errno(r, "Failed to enqueue user stop event source, ignoring: %m"); @@ -866,7 +884,7 @@ void user_update_last_session_timer(User *u) { log_debug("Last session of user '%s' logged out, terminating user context in %s.", u->user_record->user_name, - format_timespan(s, sizeof(s), u->manager->user_stop_delay, USEC_PER_MSEC)); + format_timespan(s, sizeof(s), user_stop_delay, USEC_PER_MSEC)); } } From 1b22da6f1a25b783925cb4c5da6276b9de3051f9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Aug 2019 17:26:26 +0200 Subject: [PATCH 081/109] logind: switch to short copyright header in polkit policy file --- src/login/org.freedesktop.login1.policy | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/login/org.freedesktop.login1.policy b/src/login/org.freedesktop.login1.policy index 6dc79aa32aa4d..b9a0b68dfbf2c 100644 --- a/src/login/org.freedesktop.login1.policy +++ b/src/login/org.freedesktop.login1.policy @@ -2,16 +2,7 @@ - + From c337c0c767c8218b861dcd3bf266a85c14ddad19 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 16:38:21 +0200 Subject: [PATCH 082/109] pam-systemd: include PAM error code in all our log messages where that makes sense --- src/login/pam_systemd.c | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 3f762cbbc30b4..b8187ee717266 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -50,28 +50,30 @@ static int parse_argv( assert(argc == 0 || argv); for (i = 0; i < (unsigned) argc; i++) { - if (startswith(argv[i], "class=")) { + const char *p; + + if ((p = startswith(argv[i], "class="))) { if (class) - *class = argv[i] + 6; + *class = p; - } else if (startswith(argv[i], "type=")) { + } else if ((p = startswith(argv[i], "type="))) { if (type) - *type = argv[i] + 5; + *type = p; - } else if (startswith(argv[i], "desktop=")) { + } else if ((p = startswith(argv[i], "desktop="))) { if (desktop) - *desktop = argv[i] + 8; + *desktop = p; } else if (streq(argv[i], "debug")) { if (debug) *debug = true; - } else if (startswith(argv[i], "debug=")) { + } else if ((p = startswith(argv[i], "debug="))) { int k; - k = parse_boolean(argv[i] + 6); + k = parse_boolean(p); if (k < 0) - pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring."); + pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", p); else if (debug) *debug = k; @@ -97,7 +99,7 @@ static int get_user_data( r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to get user name."); + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); return r; } @@ -362,7 +364,7 @@ static int update_environment(pam_handle_t *handle, const char *key, const char r = pam_misc_setenv(handle, key, value, 0); if (r != PAM_SUCCESS) - pam_syslog(handle, LOG_ERR, "Failed to set environment variable %s.", key); + pam_syslog(handle, LOG_ERR, "Failed to set environment variable %s: %s", key, pam_strerror(handle, r)); return r; } @@ -457,7 +459,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (validate_runtime_directory(handle, rt, pw->pw_uid)) { r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0); if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to set runtime dir."); + pam_syslog(handle, LOG_ERR, "Failed to set runtime dir: %s", pam_strerror(handle, r)); return r; } } @@ -711,7 +713,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( r = pam_set_data(handle, "systemd.existing", INT_TO_PTR(!!existing), NULL); if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to install existing flag."); + pam_syslog(handle, LOG_ERR, "Failed to install existing flag: %s", pam_strerror(handle, r)); return r; } @@ -724,7 +726,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( r = pam_set_data(handle, "systemd.session-fd", FD_TO_PTR(session_fd), NULL); if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to install session fd."); + pam_syslog(handle, LOG_ERR, "Failed to install session fd: %s", pam_strerror(handle, r)); safe_close(session_fd); return r; } From 65509fb4ab650c307fb369411fa0d8524ad29802 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 16:39:10 +0200 Subject: [PATCH 083/109] pam-systemd: port to pam_bus_log_{create|parse}_error() and pam_log_oom() --- src/login/pam_systemd.c | 107 +++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 61 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index b8187ee717266..7bff352f5c162 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -28,6 +28,7 @@ #include "hostname-util.h" #include "login-util.h" #include "macro.h" +#include "pam-util.h" #include "parse-util.h" #include "path-util.h" #include "process-util.h" @@ -231,17 +232,15 @@ static int export_legacy_dbus_address( return PAM_SUCCESS; if (asprintf(&t, DEFAULT_USER_BUS_ADDRESS_FMT, runtime) < 0) - goto error; + return pam_log_oom(handle); r = pam_misc_setenv(handle, "DBUS_SESSION_BUS_ADDRESS", t, 0); - if (r != PAM_SUCCESS) - goto error; + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set bus variable: %s", pam_strerror(handle, r)); + return r; + } return PAM_SUCCESS; - -error: - pam_syslog(handle, LOG_ERR, "Failed to set bus variable."); - return r; } static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) { @@ -253,31 +252,31 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co if (streq(limit, "infinity")) { r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", (uint64_t)-1); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r)); - return r; - } - } else { - r = parse_permille(limit); - if (r >= 0) { - r = sd_bus_message_append(m, "(sv)", "MemoryMaxScale", "u", (uint32_t) (((uint64_t) r * UINT32_MAX) / 1000U)); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r)); - return r; - } - } else { - r = parse_size(limit, 1024, &val); - if (r >= 0) { - r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", val); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r)); - return r; - } - } else - pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max: %s, ignoring.", limit); - } + if (r < 0) + return pam_bus_log_create_error(handle, r); + + return 0; + } + + r = parse_permille(limit); + if (r >= 0) { + r = sd_bus_message_append(m, "(sv)", "MemoryMaxScale", "u", (uint32_t) (((uint64_t) r * UINT32_MAX) / 1000U)); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + return 0; + } + + r = parse_size(limit, 1024, &val); + if (r >= 0) { + r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", val); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + return 0; } + pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max, ignoring: %s", limit); return 0; } @@ -292,12 +291,10 @@ static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, con r = safe_atou64(limit, &val); if (r >= 0) { r = sd_bus_message_append(m, "(sv)", "TasksMax", "t", val); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r)); - return r; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); } else - pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.tasks_max: %s, ignoring.", limit); + pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.tasks_max, ignoring: %s", limit); return 0; } @@ -312,14 +309,12 @@ static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, con r = cg_weight_parse(limit, &val); if (r >= 0) { r = sd_bus_message_append(m, "(sv)", field, "t", val); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r)); - return r; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); } else if (streq(field, "CPUWeight")) - pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.cpu_weight: %s, ignoring.", limit); + pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.cpu_weight, ignoring: %s", limit); else - pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.io_weight: %s, ignoring.", limit); + pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.io_weight, ignoring: %s", limit); return 0; } @@ -576,10 +571,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "CreateSession"); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to create CreateSession method call: %s", strerror_safe(r)); - return PAM_SESSION_ERR; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); r = sd_bus_message_append(m, "uusssssussbss", (uint32_t) pw->pw_uid, @@ -595,16 +588,12 @@ _public_ PAM_EXTERN int pam_sm_open_session( remote, remote_user, remote_host); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r)); - return PAM_SESSION_ERR; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); r = sd_bus_message_open_container(m, 'a', "(sv)"); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to open message container: %s", strerror_safe(r)); - return PAM_SYSTEM_ERR; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); r = append_session_memory_max(handle, m, memory_max); if (r < 0) @@ -623,10 +612,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( return PAM_SESSION_ERR; r = sd_bus_message_close_container(m); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to close message container: %s", strerror_safe(r)); - return PAM_SYSTEM_ERR; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); r = sd_bus_call(bus, m, 0, &error, &reply); if (r < 0) { @@ -650,10 +637,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( &seat, &vtnr, &existing); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to parse message: %s", strerror_safe(r)); - return PAM_SESSION_ERR; - } + if (r < 0) + return pam_bus_log_parse_error(handle, r); if (debug) pam_syslog(handle, LOG_DEBUG, "Reply from logind: " From 0fee194c9f4575b5ac1cb2772e3bef177749f26a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 16:39:40 +0200 Subject: [PATCH 084/109] pam-systemd: remove duplicate error logging --- src/login/pam_systemd.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 7bff352f5c162..d828f389a05b6 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -435,10 +435,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( pam_syslog(handle, LOG_DEBUG, "pam-systemd initializing"); r = get_user_data(handle, &username, &pw); - if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to get user data."); + if (r != PAM_SUCCESS) return r; - } /* Make sure we don't enter a loop by talking to * systemd-logind when it is actually waiting for the From 4be6bb8ee05495b1172643190df04a0af5941486 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 16:39:55 +0200 Subject: [PATCH 085/109] pam-systemd: voidify pam_get_item() calls --- src/login/pam_systemd.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index d828f389a05b6..13a246ca5f327 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -444,7 +444,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( * "systemd-user" we simply set XDG_RUNTIME_DIR and * leave. */ - pam_get_item(handle, PAM_SERVICE, (const void**) &service); + (void) pam_get_item(handle, PAM_SERVICE, (const void**) &service); if (streq_ptr(service, "systemd-user")) { char rt[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)]; @@ -466,10 +466,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( /* Otherwise, we ask logind to create a session for us */ - pam_get_item(handle, PAM_XDISPLAY, (const void**) &display); - pam_get_item(handle, PAM_TTY, (const void**) &tty); - pam_get_item(handle, PAM_RUSER, (const void**) &remote_user); - pam_get_item(handle, PAM_RHOST, (const void**) &remote_host); + (void) pam_get_item(handle, PAM_XDISPLAY, (const void**) &display); + (void) pam_get_item(handle, PAM_TTY, (const void**) &tty); + (void) pam_get_item(handle, PAM_RUSER, (const void**) &remote_user); + (void) pam_get_item(handle, PAM_RHOST, (const void**) &remote_host); seat = getenv_harder(handle, "XDG_SEAT", NULL); cvtnr = getenv_harder(handle, "XDG_VTNR", NULL); From aac37f85ea9ef378b3ffacd48a3e870ed3febae2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 16:55:48 +0200 Subject: [PATCH 086/109] pam-systemd: share bus connection with pam_systemd_home if we can Let's use the pam-util.h provided helpers to acquire them. --- src/login/pam_systemd.c | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 13a246ca5f327..3c466a3ead25d 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -543,11 +543,9 @@ _public_ PAM_EXTERN int pam_sm_open_session( /* Talk to logind over the message bus */ - r = sd_bus_open_system(&bus); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r)); - return PAM_SESSION_ERR; - } + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; if (debug) { pam_syslog(handle, LOG_DEBUG, "Asking logind to create session: " @@ -715,6 +713,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( } } + /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are + * not going to process the bus connection in that time, so let's better close before the daemon + * kicks us off because we are not processing anything. */ + (void) pam_release_bus_connection(handle); return PAM_SUCCESS; } @@ -723,8 +725,6 @@ _public_ PAM_EXTERN int pam_sm_close_session( int flags, int argc, const char **argv) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; const void *existing = NULL; const char *id; int r; @@ -737,17 +737,15 @@ _public_ PAM_EXTERN int pam_sm_close_session( id = pam_getenv(handle, "XDG_SESSION_ID"); if (id && !existing) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - /* Before we go and close the FIFO we need to tell - * logind that this is a clean session shutdown, so - * that it doesn't just go and slaughter us - * immediately after closing the fd */ + /* Before we go and close the FIFO we need to tell logind that this is a clean session + * shutdown, so that it doesn't just go and slaughter us immediately after closing the fd */ - r = sd_bus_open_system(&bus); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r)); - return PAM_SESSION_ERR; - } + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; r = sd_bus_call_method(bus, "org.freedesktop.login1", @@ -764,11 +762,9 @@ _public_ PAM_EXTERN int pam_sm_close_session( } } - /* Note that we are knowingly leaking the FIFO fd here. This - * way, logind can watch us die. If we closed it here it would - * not have any clue when that is completed. Given that one - * cannot really have multiple PAM sessions open from the same - * process this means we will leak one FD at max. */ + /* Note that we are knowingly leaking the FIFO fd here. This way, logind can watch us die. If we + * closed it here it would not have any clue when that is completed. Given that one cannot really + * have multiple PAM sessions open from the same process this means we will leak one FD at max. */ return PAM_SUCCESS; } From d4120d76bef558ccd478d0d7a8129d919942cf64 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 18:55:26 +0200 Subject: [PATCH 087/109] pam-systemd: port over to use a UserRecord structure Later on this allows us to set various session properties from user record. --- src/login/pam_systemd.c | 107 ++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 25 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 3c466a3ead25d..dadefba885cc6 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -36,6 +36,8 @@ #include "stdio-util.h" #include "strv.h" #include "terminal-util.h" +#include "user-util.h" +#include "userdb.h" static int parse_argv( pam_handle_t *handle, @@ -85,18 +87,15 @@ static int parse_argv( return 0; } -static int get_user_data( +static int acquire_user_record( pam_handle_t *handle, - const char **ret_username, - struct passwd **ret_pw) { + UserRecord **ret_record) { - const char *username = NULL; - struct passwd *pw = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + const char *username = NULL, *json = NULL; int r; assert(handle); - assert(ret_username); - assert(ret_pw); r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) { @@ -106,17 +105,75 @@ static int get_user_data( if (isempty(username)) { pam_syslog(handle, LOG_ERR, "User name not valid."); - return PAM_AUTH_ERR; + return PAM_SERVICE_ERR; } - pw = pam_modutil_getpwnam(handle, username); - if (!pw) { - pam_syslog(handle, LOG_ERR, "Failed to get user data."); - return PAM_USER_UNKNOWN; + /* If pam_systemd_homed (or some other module) already acqired the user record we can reuse it + * here. */ + r = pam_get_data(handle, "systemd-user-record", (const void**) &json); + if (r != PAM_SUCCESS || !json) { + _cleanup_free_ char *formatted = NULL; + + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + + /* Request the record ourselves */ + r = userdb_by_name(username, 0, &ur); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to get user record: %s", strerror_safe(r)); + return PAM_USER_UNKNOWN; + } + + r = json_variant_format(ur->json, 0, &formatted); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to format user JSON: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + /* And cache it for everyone else */ + r = pam_set_data(handle, "systemd-user-record", formatted, pam_cleanup_free); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + + TAKE_PTR(formatted); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + /* Parse cached record */ + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + ur = user_record_new(); + if (!ur) + return pam_log_oom(handle); + + r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + /* Safety check if cached record actually matches what we are looking for */ + if (!streq_ptr(username, ur->user_name)) { + pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name."); + return PAM_SERVICE_ERR; + } + } + + if (!uid_is_valid(ur->uid)) { + pam_syslog(handle, LOG_ERR, "Acquired user record does not have a UID."); + return PAM_SERVICE_ERR; } - *ret_pw = pw; - *ret_username = username; + if (ret_record) + *ret_record = TAKE_PTR(ur); return PAM_SUCCESS; } @@ -402,7 +459,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; const char - *username, *id, *object_path, *runtime_path, + *id, *object_path, *runtime_path, *service = NULL, *tty = NULL, *display = NULL, *remote_user = NULL, *remote_host = NULL, @@ -411,9 +468,9 @@ _public_ PAM_EXTERN int pam_sm_open_session( *class_pam = NULL, *type_pam = NULL, *cvtnr = NULL, *desktop = NULL, *desktop_pam = NULL, *memory_max = NULL, *tasks_max = NULL, *cpu_weight = NULL, *io_weight = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; int session_fd = -1, existing, r; bool debug = false, remote; - struct passwd *pw; uint32_t vtnr = 0; uid_t original_uid; @@ -434,7 +491,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (debug) pam_syslog(handle, LOG_DEBUG, "pam-systemd initializing"); - r = get_user_data(handle, &username, &pw); + r = acquire_user_record(handle, &ur); if (r != PAM_SUCCESS) return r; @@ -448,8 +505,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (streq_ptr(service, "systemd-user")) { char rt[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)]; - xsprintf(rt, "/run/user/"UID_FMT, pw->pw_uid); - if (validate_runtime_directory(handle, rt, pw->pw_uid)) { + xsprintf(rt, "/run/user/"UID_FMT, ur->uid); + if (validate_runtime_directory(handle, rt, ur->uid)) { r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0); if (r != PAM_SUCCESS) { pam_syslog(handle, LOG_ERR, "Failed to set runtime dir: %s", pam_strerror(handle, r)); @@ -457,7 +514,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( } } - r = export_legacy_dbus_address(handle, pw->pw_uid, rt); + r = export_legacy_dbus_address(handle, ur->uid, rt); if (r != PAM_SUCCESS) return r; @@ -550,7 +607,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (debug) { pam_syslog(handle, LOG_DEBUG, "Asking logind to create session: " "uid="UID_FMT" pid="PID_FMT" service=%s type=%s class=%s desktop=%s seat=%s vtnr=%"PRIu32" tty=%s display=%s remote=%s remote_user=%s remote_host=%s", - pw->pw_uid, getpid_cached(), + ur->uid, getpid_cached(), strempty(service), type, class, strempty(desktop), strempty(seat), vtnr, strempty(tty), strempty(display), @@ -571,7 +628,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( return pam_bus_log_create_error(handle, r); r = sd_bus_message_append(m, "uusssssussbss", - (uint32_t) pw->pw_uid, + (uint32_t) ur->uid, 0, service, type, @@ -645,20 +702,20 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (r != PAM_SUCCESS) return r; - if (original_uid == pw->pw_uid) { + if (original_uid == ur->uid) { /* Don't set $XDG_RUNTIME_DIR if the user we now * authenticated for does not match the original user * of the session. We do this in order not to result * in privileged apps clobbering the runtime directory * unnecessarily. */ - if (validate_runtime_directory(handle, runtime_path, pw->pw_uid)) { + if (validate_runtime_directory(handle, runtime_path, ur->uid)) { r = update_environment(handle, "XDG_RUNTIME_DIR", runtime_path); if (r != PAM_SUCCESS) return r; } - r = export_legacy_dbus_address(handle, pw->pw_uid, runtime_path); + r = export_legacy_dbus_address(handle, ur->uid, runtime_path); if (r != PAM_SUCCESS) return r; } From e179e91671b295b8032f17ffd34493d1be50891a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 13 Aug 2019 13:17:51 +0200 Subject: [PATCH 088/109] pam-systemd: normalize return values of append_session_xyz() Let's propagate the PAM errors we got. --- src/login/pam_systemd.c | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index dadefba885cc6..0e86904df842a 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -305,14 +305,14 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co int r; if (isempty(limit)) - return 0; + return PAM_SUCCESS; if (streq(limit, "infinity")) { r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", (uint64_t)-1); if (r < 0) return pam_bus_log_create_error(handle, r); - return 0; + return PAM_SUCCESS; } r = parse_permille(limit); @@ -321,7 +321,7 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co if (r < 0) return pam_bus_log_create_error(handle, r); - return 0; + return PAM_SUCCESS; } r = parse_size(limit, 1024, &val); @@ -330,11 +330,11 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co if (r < 0) return pam_bus_log_create_error(handle, r); - return 0; + return PAM_SUCCESS; } pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max, ignoring: %s", limit); - return 0; + return PAM_SUCCESS; } static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) { @@ -343,7 +343,7 @@ static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, con /* No need to parse "infinity" here, it will be set unconditionally later in manager_start_scope() */ if (isempty(limit) || streq(limit, "infinity")) - return 0; + return PAM_SUCCESS; r = safe_atou64(limit, &val); if (r >= 0) { @@ -353,7 +353,7 @@ static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, con } else pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.tasks_max, ignoring: %s", limit); - return 0; + return PAM_SUCCESS; } static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, const char *limit, const char *field) { @@ -361,7 +361,7 @@ static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, con int r; if (isempty(limit)) - return 0; + return PAM_SUCCESS; r = cg_weight_parse(limit, &val); if (r >= 0) { @@ -373,7 +373,7 @@ static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, con else pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.io_weight, ignoring: %s", limit); - return 0; + return PAM_SUCCESS; } static const char* getenv_harder(pam_handle_t *handle, const char *key, const char *fallback) { @@ -650,19 +650,19 @@ _public_ PAM_EXTERN int pam_sm_open_session( r = append_session_memory_max(handle, m, memory_max); if (r < 0) - return PAM_SESSION_ERR; + return r; r = append_session_tasks_max(handle, m, tasks_max); if (r < 0) - return PAM_SESSION_ERR; + return r; r = append_session_cg_weight(handle, m, cpu_weight, "CPUWeight"); if (r < 0) - return PAM_SESSION_ERR; + return r; r = append_session_cg_weight(handle, m, io_weight, "IOWeight"); if (r < 0) - return PAM_SESSION_ERR; + return r; r = sd_bus_message_close_container(m); if (r < 0) From 7c5bb1a70c896ebedb410f5ec4a60f3b9e2fd732 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 13 Aug 2019 13:18:15 +0200 Subject: [PATCH 089/109] pam-systemd: apply user record properties to session This way any component providing us with JSON user record data can use this for automatic resource management and other session properties. --- src/login/pam_systemd.c | 145 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 0e86904df842a..7ac44043daeef 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -25,13 +25,16 @@ #include "fd-util.h" #include "fileio.h" #include "format-util.h" +#include "fs-util.h" #include "hostname-util.h" +#include "locale-util.h" #include "login-util.h" #include "macro.h" #include "pam-util.h" #include "parse-util.h" #include "path-util.h" #include "process-util.h" +#include "rlimit-util.h" #include "socket-util.h" #include "stdio-util.h" #include "strv.h" @@ -451,6 +454,140 @@ static bool validate_runtime_directory(pam_handle_t *handle, const char *path, u return false; } +static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool debug) { + char **i; + int r; + + assert(handle); + assert(ur); + + if (ur->umask != MODE_INVALID) { + umask(ur->umask); + + if (debug) + pam_syslog(handle, LOG_DEBUG, "Set user umask to %04o, based on user record.", ur->umask); + } + + STRV_FOREACH(i, ur->environment) { + _cleanup_free_ char *n = NULL; + const char *e; + + assert_se(e = strchr(*i, '=')); /* environment was already validated while parsing JSON record, this thus must hold */ + + n = strndup(*i, e - *i); + if (!n) + return pam_log_oom(handle); + + if (pam_getenv(handle, n)) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $%s already set, not changing based on record.", *i); + continue; + } + + r = pam_putenv(handle, *i); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $EMAIL: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable %s set, based on user record.", *i); + } + + if (ur->email_address) { + if (pam_getenv(handle, "EMAIL")) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $EMAIL already set, not changing based on user record."); + } else { + _cleanup_free_ char *joined = NULL; + + joined = strjoin("EMAIL=", ur->email_address); + if (!joined) + return pam_log_oom(handle); + + r = pam_putenv(handle, joined); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $EMAIL: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $EMAIL set, based on user record."); + } + } + + if (ur->time_zone) { + if (pam_getenv(handle, "TZ")) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $TZ already set, not changing based on user record."); + } else if (!timezone_is_valid(ur->time_zone, LOG_DEBUG)) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "Time zone specified in user record is not valid locally, not setting $TZ."); + } else { + _cleanup_free_ char *joined = NULL; + + joined = strjoin("TZ=", ur->time_zone); + if (!joined) + return pam_log_oom(handle); + + r = pam_putenv(handle, joined); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $TZ: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $TZ set, based on user record."); + } + } + + if (ur->preferred_language) { + if (pam_getenv(handle, "LANG")) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $LANG already set, not changing based on user record."); + } else if (!locale_is_valid(ur->preferred_language)) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "Preferred language specified in user record is not valid locally, not setting $LANG."); + } else { + _cleanup_free_ char *joined = NULL; + + joined = strjoin("LANG=", ur->preferred_language); + if (!joined) + return pam_log_oom(handle); + + r = pam_putenv(handle, joined); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $LANG: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $LANG set, based on user record."); + } + } + + if (nice_is_valid(ur->nice_level)) { + if (nice(ur->nice_level) < 0) + pam_syslog(handle, LOG_ERR, "Failed to set nice level to %i, ignoring: %s", ur->nice_level, strerror(errno)); + else if (debug) + pam_syslog(handle, LOG_DEBUG, "Nice level set, based on user record."); + } + + for (int rl = 0; rl < _RLIMIT_MAX; rl++) { + + if (!ur->rlimits[rl]) + continue; + + r = setrlimit_closest(rl, ur->rlimits[rl]); + if (r < 0) + pam_syslog(handle, LOG_ERR, "Failed to set resource limit %s, ignoring: %s", rlimit_to_string(rl), strerror(errno)); + else if (debug) + pam_syslog(handle, LOG_DEBUG, "Resource limit %s set, based on user record.", rlimit_to_string(rl)); + } + + return PAM_SUCCESS; +} + _public_ PAM_EXTERN int pam_sm_open_session( pam_handle_t *handle, int flags, @@ -518,6 +655,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (r != PAM_SUCCESS) return r; + r = apply_user_record_settings(handle, ur, debug); + if (r != PAM_SUCCESS) + return r; + return PAM_SUCCESS; } @@ -770,6 +911,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( } } + r = apply_user_record_settings(handle, ur, debug); + if (r != PAM_SUCCESS) + return r; + /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are * not going to process the bus connection in that time, so let's better close before the daemon * kicks us off because we are not processing anything. */ From ba49efd7bcf2268081aa9506f944a52720295aed Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 13 Aug 2019 14:14:42 +0200 Subject: [PATCH 090/109] pam_systemd: add one more assert --- src/login/pam_systemd.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 7ac44043daeef..1d825ad50f0b7 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -427,6 +427,7 @@ static int update_environment(pam_handle_t *handle, const char *key, const char static bool validate_runtime_directory(pam_handle_t *handle, const char *path, uid_t uid) { struct stat st; + assert(handle); assert(path); /* Just some extra paranoia: let's not set $XDG_RUNTIME_DIR if the directory we'd set it to isn't actually set From dc28f48896ca3d73170e45a73f6ee3f232d5c27b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 13 Aug 2019 14:14:47 +0200 Subject: [PATCH 091/109] pam_systemd: don't use PAM_SYSTEM_ERR for something that isn't precisely a system error It's not really clear which PAM errors to use for which conditions, but something called PAM_SYSTEM_ERR should probably not be used when the error is not the result of some system call failure. --- src/login/pam_systemd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 1d825ad50f0b7..08d3a8de49825 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -818,7 +818,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( return PAM_SUCCESS; } else { pam_syslog(handle, LOG_ERR, "Failed to create session: %s", bus_error_message(&error, r)); - return PAM_SYSTEM_ERR; + return PAM_SESSION_ERR; } } From df1d49868a02f9362f9b6c5a504dbaa146cf9e07 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 19 Aug 2019 15:15:13 +0200 Subject: [PATCH 092/109] pam_systemd: prolong method call timeout when allocating session Starting a session might involve starting the user@.service instance, hence let's make the bus call timeout substantially longer. This mimics what we already do for pam_systemd_home. Fixes: https://bugs.freedesktop.org/show_bug.cgi?id=83828 --- src/login/pam_systemd.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 08d3a8de49825..261b738564163 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -42,6 +42,8 @@ #include "user-util.h" #include "userdb.h" +#define LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE) + static int parse_argv( pam_handle_t *handle, int argc, const char **argv, @@ -810,7 +812,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (r < 0) return pam_bus_log_create_error(handle, r); - r = sd_bus_call(bus, m, 0, &error, &reply); + r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); if (r < 0) { if (sd_bus_error_has_name(&error, BUS_ERROR_SESSION_BUSY)) { if (debug) From 723c9542b601cfc33e13ee043049f4e986cd2ee7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 19 Aug 2019 18:56:11 +0200 Subject: [PATCH 093/109] sleep: automatically lock all home directories when suspending --- src/sleep/sleep.c | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/sleep/sleep.c b/src/sleep/sleep.c index b9fe96635d22b..3b0f4f462424b 100644 --- a/src/sleep/sleep.c +++ b/src/sleep/sleep.c @@ -18,11 +18,12 @@ #include "sd-messages.h" #include "btrfs-util.h" +#include "bus-error.h" #include "def.h" #include "exec-util.h" #include "fd-util.h" -#include "format-util.h" #include "fileio.h" +#include "format-util.h" #include "log.h" #include "main-func.h" #include "parse-util.h" @@ -182,6 +183,49 @@ static int configure_hibernation(void) { return write_hibernate_location_info(); } +static int lock_all_homes(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + /* Let's synchronously lock all home directories managed by homed that have been marked for it. This + * way the key material required to access these volumes is hopefully removed from memory. */ + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_warning_errno(r, "Failed to connect to system bus, ignoring: %m"); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockAllHomes"); + if (r < 0) + return bus_log_create_error(r); + + /* If homed is not running it can't have any home directories active either. */ + r = sd_bus_message_set_auto_start(m, false); + if (r < 0) + return log_error_errno(r, "Failed to disable auto-start of LockAllHomes() message: %m"); + + r = sd_bus_call(bus, m, DEFAULT_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + log_debug("systemd-homed is not running, skipping locking of home."); + return 0; + } + + return log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + } + + log_debug("Successfully requested that all home directories shall be suspended."); + return 0; +} + static int execute(char **modes, char **states) { char *arguments[] = { NULL, @@ -217,6 +261,7 @@ static int execute(char **modes, char **states) { } (void) execute_directories(dirs, DEFAULT_TIMEOUT_USEC, NULL, NULL, arguments, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS); + (void) lock_all_homes(); log_struct(LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SLEEP_START_STR, From 2eb8a9c53b51cd3ad1c39c65662f86ef3a34accd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 21 Aug 2019 10:40:04 +0200 Subject: [PATCH 094/109] cryptsetup: minor coding style clean-ups --- src/cryptsetup/cryptsetup.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 78732a0a577f9..499c81c6677ca 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -276,10 +276,10 @@ static int parse_options(const char *options) { } /* sanity-check options */ - if (arg_type != NULL && !streq(arg_type, CRYPT_PLAIN)) { - if (arg_offset) + if (arg_type && !streq(arg_type, CRYPT_PLAIN)) { + if (arg_offset != 0) log_warning("offset= ignored with type %s", arg_type); - if (arg_skip) + if (arg_skip != 0) log_warning("skip= ignored with type %s", arg_type); } @@ -487,11 +487,13 @@ static int attach_tcrypt( return 0; } -static int attach_luks_or_plain(struct crypt_device *cd, - const char *name, - const char *key_file, - char **passwords, - uint32_t flags) { +static int attach_luks_or_plain( + struct crypt_device *cd, + const char *name, + const char *key_file, + char **passwords, + uint32_t flags) { + int r = 0; bool pass_volume_key = false; @@ -565,6 +567,7 @@ static int attach_luks_or_plain(struct crypt_device *cd, } if (r < 0) return log_error_errno(r, "Failed to activate with key file '%s': %m", key_file); + } else { char **p; @@ -677,7 +680,7 @@ static int run(int argc, char *argv[]) { } /* A delicious drop of snake oil */ - mlockall(MCL_FUTURE); + (void) mlockall(MCL_FUTURE); if (arg_header) { log_debug("LUKS header: %s", arg_header); @@ -751,11 +754,7 @@ static int run(int argc, char *argv[]) { if (streq_ptr(arg_type, CRYPT_TCRYPT)) r = attach_tcrypt(cd, argv[2], key_file, passwords, flags); else - r = attach_luks_or_plain(cd, - argv[2], - key_file, - passwords, - flags); + r = attach_luks_or_plain(cd, argv[2], key_file, passwords, flags); if (r >= 0) break; if (r != -EAGAIN) From bc43b2e7a8d0173ee10fb48101e97891bf844dc6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 21 Aug 2019 10:45:42 +0200 Subject: [PATCH 095/109] cryptsetup: use STR_IN_SET() where appropriate Note that this slightly changes behaviour: "none" is only allowed as option, if it's the only option specified, but not in combination with other options. I think this makes more sense, since it's the choice when no options shall be specified. --- src/cryptsetup/cryptsetup.c | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 499c81c6677ca..d5c7ab0d4acaa 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -251,7 +251,7 @@ static int parse_one_option(const char *option) { if (r < 0) return log_error_errno(r, "Failed to parse %s: %m", option); - } else if (!streq(option, "none")) + } else log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option); return 0; @@ -662,18 +662,14 @@ static int run(int argc, char *argv[]) { if (argc < 4) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "attach requires at least two arguments."); - if (argc >= 5 && - argv[4][0] && - !streq(argv[4], "-") && - !streq(argv[4], "none")) { - - if (!path_is_absolute(argv[4])) - log_warning("Password file path '%s' is not absolute. Ignoring.", argv[4]); - else + if (argc >= 5 && !STR_IN_SET(argv[4], "", "-", "none")) { + if (path_is_absolute(argv[4])) key_file = argv[4]; + else + log_warning("Password file path '%s' is not absolute. Ignoring.", argv[4]); } - if (argc >= 6 && argv[5][0] && !streq(argv[5], "-")) { + if (argc >= 6 && !STR_IN_SET(argv[5], "", "-", "none")) { r = parse_options(argv[5]); if (r < 0) return r; From d04455872200e62ef429361dfa5786ade6665ba3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 22 Aug 2019 10:21:11 +0200 Subject: [PATCH 096/109] cryptsetup: add native pkcs#11 support to cryptsetup This adds a new crypttab option for volumes "pkcs11-uri=" which takes a PKCS#11 URI. When used the key stored in the line's key file is decrypted with the private key the PKCS#11 URI indiciates. This means any smartcard that can store private RSA keys is usable for unlocking LUKS devices. --- CRYPTSETUP | 63 +++ meson.build | 18 +- meson_options.txt | 2 + src/cryptsetup/cryptsetup-pkcs11.c | 707 +++++++++++++++++++++++++++++ src/cryptsetup/cryptsetup-pkcs11.h | 35 ++ src/cryptsetup/cryptsetup.c | 115 ++++- 6 files changed, 914 insertions(+), 26 deletions(-) create mode 100644 CRYPTSETUP create mode 100644 src/cryptsetup/cryptsetup-pkcs11.c create mode 100644 src/cryptsetup/cryptsetup-pkcs11.h diff --git a/CRYPTSETUP b/CRYPTSETUP new file mode 100644 index 0000000000000..bca0e07b80615 --- /dev/null +++ b/CRYPTSETUP @@ -0,0 +1,63 @@ +# Using a Yubikey for unlocking arbitrary LUKS devices. +# +# A few notes on the used parameters: +# +# → We use RSA (and not ECC), since Yubikeys support PKCS#11 Decrypt() only for +# RSA keys (ECC keys can only be used for signing?) +# → We use RSA2048, which is the longest key size current Yubikeys support for +# RSA +# → LUKS key size must be << 2048bit due to RSA padding, hence we use 128 bytes +# → We use Yubikey key slot 9d, since that's apparently the keyslot to use for +# decryption purposes, see: +# https://developers.yubico.com/PIV/Introduction/Certificate_slots.html + +# Make sure noone can read the files we generate, but us +umask 077 + +# Clear the Yubikey from any old keys +ykman piv reset + +# Generate a new private/public key pair on th device, store the public key in +# 'pubkey.pem'. +ykman piv generate-key -a RSA2048 9d pubkey.pem + +# Create a self-signed certificate from this public key, and store it on the +# device. +ykman piv generate-certificate --subject "Knobelei" 9d pubkey.pem + +# Check if the newly create key on the Yubikey shows up as token in +# PKCS#11. Have a look at the output, and copy the resulting token URI to the +# clipboard. +p11tool --list-tokens + +# Generate a (secret) random key to use as LUKS decryption key. +dd if=/dev/urandom of=plaintext.bin bs=128 count=1 + +# Encrypt this newly generated LUKS decryption key using the public key whose +# private key is on the Yubikey, store the result in +# /etc/encrypted-luks-key.bin, where we'll look for it during boot. +openssl rsautl -encrypt -pubin -inkey pubkey.pem -in plaintext.bin -out /etc/encrypted-luks-key.bin + +# Configure the LUKS decryption key on the LUKS device. We use very low pbkdf +# settings since the key already has quite a high quality (it comes directly +# from /dev/urandom after all), and thus we don't need to do much key +# derivation. +cryptsetup luksAddKey /dev/sda1 plaintext.bin --pbkdf-memory=32 --pbkdf-parallel=1 --pbkdf-force-iterations=4 + +# Now securely delete the plain text LUKS key, we don't need it anymore, and +# since it contains secret key material it should be removed from disk +# thoroughly. +shred -u plaintext.bin + +# We don't need the public key anymore either, let's remove it too. Since this +# one is not security sensitive we just do a regular "rm" here. +rm pubkey.pem + +# Test: Let's run systemd-cryptsetup to test if this all worked. The option +# string should contain the full PKCS#11 URI we have in the clipboard, it tells +# the tool how to decypher the encrypted LUKS key. +systemd-cryptsetup attach mytest /dev/sda1 /etc/encrypted-luks-key.bin 'pkcs11-uri=pkcs11:…' + +# If that worked, let's now add the same line persistently to /etc/crypttab, +# for the future. +echo "mytest /dev/sda1 /etc/encrypted-luks-key 'pkcs11-uri=pkcs11:…' >> /etc/crypttab diff --git a/meson.build b/meson.build index b21b14b0f9831..1bdb6d7d891ee 100644 --- a/meson.build +++ b/meson.build @@ -1081,6 +1081,17 @@ else endif conf.set10('HAVE_OPENSSL', have) +want_p11kit = get_option('p11kit') +if want_p11kit != 'false' and not skip_deps + libp11kit = dependency('p11-kit-1', + required : want_p11kit == 'true') + have = libp11kit.found() +else + have = false + libp11kit = [] +endif +conf.set10('HAVE_P11KIT', have) + want_elfutils = get_option('elfutils') if want_elfutils != 'false' and not skip_deps libdw = dependency('libdw', @@ -2069,10 +2080,13 @@ executable('systemd-system-update-generator', if conf.get('HAVE_LIBCRYPTSETUP') == 1 executable('systemd-cryptsetup', - 'src/cryptsetup/cryptsetup.c', + ['src/cryptsetup/cryptsetup.c', + 'src/cryptsetup/cryptsetup-pkcs11.c', + 'src/cryptsetup/cryptsetup-pkcs11.h'], include_directories : includes, link_with : [libshared], - dependencies : [libcryptsetup], + dependencies : [libcryptsetup, + libp11kit], install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) diff --git a/meson_options.txt b/meson_options.txt index 0b1403b44813b..3c314adf0140e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -282,6 +282,8 @@ option('gnutls', type : 'combo', choices : ['auto', 'true', 'false'], description : 'gnutls support') option('openssl', type : 'combo', choices : ['auto', 'true', 'false'], description : 'openssl support') +option('p11kit', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'p11kit support') option('elfutils', type : 'combo', choices : ['auto', 'true', 'false'], description : 'elfutils support') option('zlib', type : 'combo', choices : ['auto', 'true', 'false'], diff --git a/src/cryptsetup/cryptsetup-pkcs11.c b/src/cryptsetup/cryptsetup-pkcs11.c new file mode 100644 index 0000000000000..a13e5427f2dee --- /dev/null +++ b/src/cryptsetup/cryptsetup-pkcs11.c @@ -0,0 +1,707 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include + +#include +#include + +#include "alloc-util.h" +#include "ask-password-api.h" +#include "cryptsetup-pkcs11.h" +#include "escape.h" +#include "fd-util.h" +#include "macro.h" +#include "memory-util.h" +#include "stat-util.h" +#include "strv.h" + +DEFINE_TRIVIAL_CLEANUP_FUNC(P11KitUri*, p11_kit_uri_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(CK_FUNCTION_LIST**, p11_kit_modules_finalize_and_release); + +static int token_login( + const char *friendly_name, + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_SLOT_ID slotid, + const CK_TOKEN_INFO *token_info, + const char *token_uri_string, + const char *token_label, + usec_t until) { + + _cleanup_free_ char *token_uri_escaped = NULL, *id = NULL; + CK_TOKEN_INFO updated_token_info; + CK_RV rv; + int r; + + assert(friendly_name); + assert(m); + assert(token_info); + assert(token_uri_string); + assert(token_label); + + if (FLAGS_SET(token_info->flags, CKF_PROTECTED_AUTHENTICATION_PATH)) { + rv = m->C_Login(session, CKU_USER, NULL, 0); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv)); + + log_info("Successully logged into security token '%s' via protected authentication path.", token_label); + return 0; + } + + if (!FLAGS_SET(token_info->flags, CKF_LOGIN_REQUIRED)) { + log_info("No login into security token '%s' required.", token_label); + return 0; + } + + token_uri_escaped = cescape(token_uri_string); + if (!token_uri_escaped) + return log_oom(); + + id = strjoin("pkcs11:", token_uri_escaped); + if (!id) + return log_oom(); + + for (unsigned tries = 0; tries < 3; tries++) { + _cleanup_strv_free_erase_ char **passwords = NULL; + _cleanup_free_ char *text = NULL; + char **i; + + if (FLAGS_SET(token_info->flags, CKF_USER_PIN_FINAL_TRY)) + r = asprintf(&text, "Please enter correct PIN for security token '%s' in order to unlock disk %s (final try):", token_label, friendly_name); + if (FLAGS_SET(token_info->flags, CKF_USER_PIN_COUNT_LOW)) + r = asprintf(&text, "PIN has been entered incorrectly previously, please enter correct PIN for security token '%s' in order to unlock disk %s:", token_label, friendly_name); + else if (tries == 0) + r = asprintf(&text, "Please enter PIN for security token '%s' in order to unlock disk %s:", token_label, friendly_name); + else + r = asprintf(&text, "Please enter PIN for security token '%s' in order to unlock disk %s (try #%u):", token_label, friendly_name, tries+1); + if (r < 0) + return log_oom(); + + /* We never cache PINs, simply because it's fatal if we use wrong PINs, since usually there are only 3 tries */ + r = ask_password_auto(text, "drive-harddisk", id, "pkcs11-pin", until, 0, &passwords); + if (r < 0) + return log_error_errno(r, "Failed to query PIN for security token '%s': %m", token_label); + + STRV_FOREACH(i, passwords) { + rv = m->C_Login(session, CKU_USER, (CK_UTF8CHAR*) *i, strlen(*i)); + if (rv == CKR_OK) { + log_info("Successfully logged into security token '%s'.", token_label); + return 0; + } + if (rv == CKR_PIN_LOCKED) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "PIN has been locked, please reset PIN of security token '%s'.", token_label); + if (!IN_SET(rv, CKR_PIN_INCORRECT, CKR_PIN_LEN_RANGE)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv)); + + /* Referesh the token info, so that we can prompt knowing the new flags if they changed. */ + rv = m->C_GetTokenInfo(slotid, &updated_token_info); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire updated security token information for slot %lu: %s", slotid, p11_kit_strerror(rv)); + + token_info = &updated_token_info; + log_notice("PIN for token '%s' is incorrect, please try again.", token_label); + } + } + + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Too many attempts to log into token '%s'.", token_label); +} + +static int token_find_private_key( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + P11KitUri *search_uri, + CK_OBJECT_HANDLE *ret_object) { + + bool found_decrypt = false, found_class = false, found_key_type = false; + _cleanup_free_ CK_ATTRIBUTE *attributes_buffer = NULL; + CK_ULONG n_attributes, a, n_objects; + CK_ATTRIBUTE *attributes = NULL; + CK_OBJECT_HANDLE objects[2]; + CK_RV rv, rv2; + + assert(m); + assert(search_uri); + assert(ret_object); + + attributes = p11_kit_uri_get_attributes(search_uri, &n_attributes); + for (a = 0; a < n_attributes; a++) { + + /* We use the URI's included match attributes, but make them more strict. This allows users + * to specify a token URL instead of an object URL and the right thing should happen if + * there's only one suitable key on the token. */ + + switch (attributes[a].type) { + + case CKA_CLASS: { + CK_OBJECT_CLASS c; + + if (attributes[a].ulValueLen != sizeof(c)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_CLASS attribute size."); + + memcpy(&c, attributes[a].pValue, sizeof(c)); + if (c != CKO_PRIVATE_KEY) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not a private key, refusing."); + + found_class = true; + break; + } + + case CKA_DECRYPT: { + CK_BBOOL b; + + if (attributes[a].ulValueLen != sizeof(b)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_DECRYPT attribute size."); + + memcpy(&b, attributes[a].pValue, sizeof(b)); + if (!b) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not suitable for decryption, refusing."); + + found_decrypt = true; + break; + } + + case CKA_KEY_TYPE: { + CK_KEY_TYPE t; + + if (attributes[a].ulValueLen != sizeof(t)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_KEY_TYPE attribute size."); + + memcpy(&t, attributes[a].pValue, sizeof(t)); + if (t != CKK_RSA) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not an RSA key, refusing."); + + found_key_type = true; + break; + }} + } + + if (!found_decrypt || !found_class || !found_key_type) { + /* Hmm, let's slightly extend the attribute list we search for */ + + attributes_buffer = new(CK_ATTRIBUTE, n_attributes + !found_decrypt + !found_class + !found_key_type); + if (!attributes_buffer) + return log_oom(); + + memcpy(attributes_buffer, attributes, sizeof(CK_ATTRIBUTE) * n_attributes); + + if (!found_decrypt) { + static const CK_BBOOL yes = true; + + attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) { + .type = CKA_DECRYPT, + .pValue = (CK_BBOOL*) &yes, + .ulValueLen = sizeof(yes), + }; + } + + if (!found_class) { + static const CK_OBJECT_CLASS class = CKO_PRIVATE_KEY; + + attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) { + .type = CKA_CLASS, + .pValue = (CK_OBJECT_CLASS*) &class, + .ulValueLen = sizeof(class), + }; + } + + if (!found_key_type) { + static const CK_KEY_TYPE type = CKK_RSA; + + attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) { + .type = CKA_KEY_TYPE, + .pValue = (CK_KEY_TYPE*) &type, + .ulValueLen = sizeof(type), + }; + } + + attributes = attributes_buffer; + } + + rv = m->C_FindObjectsInit(session, attributes, n_attributes); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize object find call: %s", p11_kit_strerror(rv)); + + rv = m->C_FindObjects(session, objects, ELEMENTSOF(objects), &n_objects); + rv2 = m->C_FindObjectsFinal(session); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to search objects: %s", p11_kit_strerror(rv)); + if (rv2 != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to finalize object find call: %s", p11_kit_strerror(rv)); + if (n_objects == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Failed to find selected private key suitable for decryption on token."); + if (n_objects > 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Configured private key URI matches multiple keys, refusing."); + + *ret_object = objects[0]; + return 0; +} + +static int token_decrypt_our_key( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE object, + const char *token_label, + const void *encrypted_key, + size_t encrypted_key_size, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + static const CK_MECHANISM mechanism = { + .mechanism = CKM_RSA_PKCS + }; + _cleanup_(erase_and_freep) CK_BYTE *dbuffer = NULL; + CK_ULONG dbuffer_size = 0; + CK_RV rv; + + assert(m); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + rv = m->C_DecryptInit(session, (CK_MECHANISM*) &mechanism, object); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize decryption on security token '%s': %s", token_label, p11_kit_strerror(rv)); + + dbuffer_size = encrypted_key_size; /* Start with something reasonable */ + dbuffer = malloc(dbuffer_size); + if (!dbuffer) + return log_oom(); + + rv = m->C_Decrypt(session, (CK_BYTE*) encrypted_key, encrypted_key_size, dbuffer, &dbuffer_size); + if (rv == CKR_BUFFER_TOO_SMALL) { + erase_and_free(dbuffer); + + dbuffer = malloc(dbuffer_size); + if (!dbuffer) + return log_oom(); + + rv = m->C_Decrypt(session, (CK_BYTE*) encrypted_key, encrypted_key_size, dbuffer, &dbuffer_size); + } + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to decrypt key on security token '%s': %s", token_label, p11_kit_strerror(rv)); + + log_info("Successfully decrypted key with security token '%s'.", token_label); + + *ret_decrypted_key = TAKE_PTR(dbuffer); + *ret_decrypted_key_size = dbuffer_size; + return 0; +} + +static int token_process( + const char *friendly_name, + CK_FUNCTION_LIST *m, + CK_SLOT_ID slotid, + const CK_TOKEN_INFO *token_info, + const char *token_uri_string, + P11KitUri *search_uri, + const void *encrypted_key, + size_t encrypted_key_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_free_ char *token_label = NULL; + CK_SESSION_HANDLE session; + CK_OBJECT_HANDLE object; + CK_RV rv; + int r; + + assert(friendly_name); + assert(m); + assert(token_info); + assert(token_uri_string); + assert(search_uri); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + /* The label is not NUL terminated and likely padded with spaces, let's make a copy here, so that we can strip that. */ + token_label = strndup((char*) token_info->label, sizeof(token_info->label)); + if (!token_label) + return log_oom(); + + strstrip(token_label); + + rv = m->C_OpenSession(slotid, CKF_SERIAL_SESSION, NULL, NULL, &session); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to create session for security token '%s': %s", token_label, p11_kit_strerror(rv)); + + r = token_login(friendly_name, m, session, slotid, token_info, token_uri_string, token_label, until); + if (r < 0) + goto finish; + + r = token_find_private_key(m, session, search_uri, &object); + if (r < 0) + goto finish; + + r = token_decrypt_our_key(m, session, object, token_label, encrypted_key, encrypted_key_size, ret_decrypted_key, ret_decrypted_key_size); + if (r < 0) + goto finish; + + r = 1; + +finish: + rv = m->C_CloseSession(session); + if (rv != CKR_OK) + log_warning_errno(SYNTHETIC_ERRNO(rv), "Failed to close session on PKCS#11 token, ignoring: %s", p11_kit_strerror(rv)); + + return r; +} + +static int uri_from_string(const char *p, P11KitUri **ret) { + _cleanup_(p11_kit_uri_freep) P11KitUri *uri = NULL; + + assert(p); + assert(ret); + + uri = p11_kit_uri_new(); + if (!uri) + return -ENOMEM; + + if (p11_kit_uri_parse(p, P11_KIT_URI_FOR_ANY, uri) != P11_KIT_URI_OK) + return -EINVAL; + + *ret = TAKE_PTR(uri); + return 0; +} + +static P11KitUri *uri_from_module_info(const CK_INFO *info) { + P11KitUri *uri; + + assert(info); + + uri = p11_kit_uri_new(); + if (!uri) + return NULL; + + *p11_kit_uri_get_module_info(uri) = *info; + return uri; +} + +static P11KitUri *uri_from_slot_info(const CK_SLOT_INFO *slot_info) { + P11KitUri *uri; + + assert(slot_info); + + uri = p11_kit_uri_new(); + if (!uri) + return NULL; + + *p11_kit_uri_get_slot_info(uri) = *slot_info; + return uri; +} + +static P11KitUri *uri_from_token_info(const CK_TOKEN_INFO *token_info) { + P11KitUri *uri; + + assert(token_info); + + uri = p11_kit_uri_new(); + if (!uri) + return NULL; + + *p11_kit_uri_get_token_info(uri) = *token_info; + return uri; +} + +static int slot_process( + const char *friendly_name, + CK_FUNCTION_LIST *m, + CK_SLOT_ID slotid, + P11KitUri *search_uri, + const void *encrypted_key, + size_t encrypted_key_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_(p11_kit_uri_freep) P11KitUri* slot_uri = NULL, *token_uri = NULL; + _cleanup_free_ char *token_uri_string = NULL; + CK_TOKEN_INFO token_info; + CK_SLOT_INFO slot_info; + int uri_result; + CK_RV rv; + + assert(friendly_name); + assert(m); + assert(search_uri); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + /* We return -EAGAIN for all failures we can attribute to a specific slot in some way, so that the + * caller might try other slots before giving up. */ + + rv = m->C_GetSlotInfo(slotid, &slot_info); + if (rv != CKR_OK) { + log_warning("Failed to acquire slot info for slot %lu, ignoring slot: %s", slotid, p11_kit_strerror(rv)); + return -EAGAIN; + } + + slot_uri = uri_from_slot_info(&slot_info); + if (!slot_uri) + return log_oom(); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *slot_uri_string = NULL; + + uri_result = p11_kit_uri_format(slot_uri, P11_KIT_URI_FOR_ANY, &slot_uri_string); + if (uri_result != P11_KIT_URI_OK) { + log_warning("Failed to format slot URI, ignoring slot: %s", p11_kit_uri_message(uri_result)); + return -EAGAIN; + } + + log_debug("Found slot with URI %s", slot_uri_string); + } + + rv = m->C_GetTokenInfo(slotid, &token_info); + if (rv == CKR_TOKEN_NOT_PRESENT) { + log_debug("Token not present in slot, ignoring."); + return -EAGAIN; + } else if (rv != CKR_OK) { + log_warning("Failed to acquire token info for slot %lu, ignoring slot: %s", slotid, p11_kit_strerror(rv)); + return -EAGAIN; + } + + token_uri = uri_from_token_info(&token_info); + if (!token_uri) + return log_oom(); + + uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, &token_uri_string); + if (uri_result != P11_KIT_URI_OK) { + log_warning("Failed to format slot URI: %s", p11_kit_uri_message(uri_result)); + return -EAGAIN; + } + + if (!p11_kit_uri_match_token_info(search_uri, &token_info)) { + log_debug("Found non-matching token with URI %s.", token_uri_string); + return -EAGAIN; + } + + log_debug("Found matching token with URI %s.", token_uri_string); + + return token_process( + friendly_name, + m, + slotid, + &token_info, + token_uri_string, + search_uri, + encrypted_key, encrypted_key_size, + until, + ret_decrypted_key, ret_decrypted_key_size); +} + +static int module_process( + const char *friendly_name, + CK_FUNCTION_LIST *m, + P11KitUri *search_uri, + const void *encrypted_key, + size_t encrypted_key_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_free_ char *name = NULL, *module_uri_string = NULL; + _cleanup_(p11_kit_uri_freep) P11KitUri* module_uri = NULL; + _cleanup_free_ CK_SLOT_ID *slotids = NULL; + CK_ULONG n_slotids = 0; + int uri_result; + CK_INFO info; + size_t k; + CK_RV rv; + int r; + + /* We ignore most errors from modules here, in order to skip over faulty modules: one faulty module + * should not have the effect that we don't try the others anymore. We indicate such per-module + * failures with -EAGAIN, which let's the caller try the next module. */ + + name = p11_kit_module_get_name(m); + if (!name) + return log_oom(); + + log_debug("Trying PKCS#11 module %s.", name); + + rv = m->C_GetInfo(&info); + if (rv != CKR_OK) { + log_warning("Failed to get info on PKCS#11 module, ignoring module: %s", p11_kit_strerror(rv)); + return -EAGAIN; + } + + module_uri = uri_from_module_info(&info); + if (!module_uri) + return log_oom(); + + uri_result = p11_kit_uri_format(module_uri, P11_KIT_URI_FOR_ANY, &module_uri_string); + if (uri_result != P11_KIT_URI_OK) { + log_warning("Failed to format module URI, ignoring module: %s", p11_kit_uri_message(uri_result)); + return -EAGAIN; + } + + log_debug("Found module with URI %s", module_uri_string); + + for (unsigned tries = 0; tries < 16; tries++) { + slotids = mfree(slotids); + n_slotids = 0; + + rv = m->C_GetSlotList(0, NULL, &n_slotids); + if (rv != CKR_OK) { + log_warning("Failed to get slot list size, ignoring module: %s", p11_kit_strerror(rv)); + n_slotids = 0; + break; + } + if (n_slotids == 0) { + log_debug("This module has no slots? Ignoring module."); + break; + } + + slotids = new(CK_SLOT_ID, n_slotids); + if (!slotids) + return log_oom(); + + rv = m->C_GetSlotList(0, slotids, &n_slotids); + if (rv == CKR_OK) + break; + n_slotids = 0; + if (rv != CKR_BUFFER_TOO_SMALL) { + log_warning("Failed to acquire slot list, ignoring module: %s", p11_kit_strerror(rv)); + break; + } + + /* Hu? Maybe somebody plugged something in and things changed? Let's try again */ + } + + if (n_slotids == 0) + return -EAGAIN; + + for (k = 0; k < n_slotids; k++) { + r = slot_process( + friendly_name, + m, + slotids[k], + search_uri, + encrypted_key, encrypted_key_size, + until, + ret_decrypted_key, ret_decrypted_key_size); + if (r != -EAGAIN) + return r; + } + + return -EAGAIN; +} + +static int load_key_file( + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + void **ret_encrypted_key, + size_t *ret_encrypted_key_size) { + + _cleanup_(erase_and_freep) char *buffer = NULL; + _cleanup_close_ int fd = -1; + ssize_t n; + int r; + + assert(key_file); + assert(ret_encrypted_key); + assert(ret_encrypted_key_size); + + fd = open(key_file, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to load encrypted PKCS#11 key: %m"); + + if (key_file_size == 0) { + struct stat st; + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat key file: %m"); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Key file is not a regular file: %m"); + + if (st.st_size == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file is empty, refusing."); + if ((uint64_t) st.st_size > SIZE_MAX) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Key file too large, refsing."); + + if (key_file_offset >= (uint64_t) st.st_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file offset too large for file, refusing."); + + key_file_size = st.st_size - key_file_offset; + } + + buffer = malloc(key_file_size); + if (!buffer) + return log_oom(); + + if (key_file_offset > 0) + n = pread(fd, buffer, key_file_size, key_file_offset); + else + n = read(fd, buffer, key_file_size); + if (n < 0) + return log_error_errno(errno, "Failed to read PKCS#11 key file: %m"); + if (n == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty encrypted key found, refusing."); + + *ret_encrypted_key = TAKE_PTR(buffer); + *ret_encrypted_key_size = (size_t) n; + + return 0; +} + +int acquire_pkcs11_key( + const char *friendly_name, + const char *pkcs11_uri, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_(p11_kit_modules_finalize_and_releasep) CK_FUNCTION_LIST **modules = NULL; + _cleanup_(p11_kit_uri_freep) P11KitUri *search_uri = NULL; + _cleanup_(erase_and_freep) void *encrypted_key = NULL; + size_t encrypted_key_size; + int r; + + assert(friendly_name); + assert(pkcs11_uri); + assert(key_file); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + r = load_key_file(key_file, key_file_size, key_file_offset, &encrypted_key, &encrypted_key_size); + if (r < 0) + return r; + + r = uri_from_string(pkcs11_uri, &search_uri); + if (r < 0) + return log_error_errno(r, "Failed to parse PKCS#11 URI '%s': %m", pkcs11_uri); + + modules = p11_kit_modules_load_and_initialize(0); + if (!modules) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize pkcs11 modules"); + + for (CK_FUNCTION_LIST **i = modules; *i; i++) { + r = module_process( + friendly_name, + *i, + search_uri, + encrypted_key, + encrypted_key_size, + until, + ret_decrypted_key, + ret_decrypted_key_size); + if (r != -EAGAIN) + return r; + } + + return -EAGAIN; +} diff --git a/src/cryptsetup/cryptsetup-pkcs11.h b/src/cryptsetup/cryptsetup-pkcs11.h new file mode 100644 index 0000000000000..f207b6c749645 --- /dev/null +++ b/src/cryptsetup/cryptsetup-pkcs11.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "time-util.h" + +#if HAVE_P11KIT + +int acquire_pkcs11_key( + const char *friendly_name, + const char *pkcs11_uri, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size); + +#else + +static inline int acquire_pkcs11_key( + const char *friendly_name, + const char *pkcs11_uri, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + return -EOPNOTSUPP; +} + +#endif diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index d5c7ab0d4acaa..f00fdb71e91b6 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -13,6 +13,7 @@ #include "alloc-util.h" #include "ask-password-api.h" #include "crypt-util.h" +#include "cryptsetup-pkcs11.h" #include "device-util.h" #include "escape.h" #include "fileio.h" @@ -58,11 +59,13 @@ static char **arg_tcrypt_keyfiles = NULL; static uint64_t arg_offset = 0; static uint64_t arg_skip = 0; static usec_t arg_timeout = USEC_INFINITY; +static char *arg_pkcs11_uri = NULL; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_header, freep); STATIC_DESTRUCTOR_REGISTER(arg_tcrypt_keyfiles, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_uri, freep); /* Options Debian's crypttab knows we don't: @@ -251,6 +254,15 @@ static int parse_one_option(const char *option) { if (r < 0) return log_error_errno(r, "Failed to parse %s: %m", option); + } else if ((val = startswith(option, "pkcs11-uri="))) { + + if (!startswith(val, "pkcs11:")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "pkcs11-uri= parameter expects a PKCS#11 URI, refusing"); + + r = free_and_strdup(&arg_pkcs11_uri, val); + if (r < 0) + return log_oom(); + } else log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option); @@ -337,28 +349,19 @@ static char *disk_mount_point(const char *label) { return NULL; } -static int get_password(const char *vol, const char *src, usec_t until, bool accept_cached, char ***ret) { - _cleanup_free_ char *description = NULL, *name_buffer = NULL, *mount_point = NULL, *text = NULL, *disk_path = NULL; - _cleanup_strv_free_erase_ char **passwords = NULL; - const char *name = NULL; - char **p, *id; - int r = 0; +static char *friendly_disk_name(const char *src, const char *vol) { + _cleanup_free_ char *description = NULL, *mount_point = NULL; + char *name_buffer = NULL; + int r; - assert(vol); assert(src); - assert(ret); + assert(vol); description = disk_description(src); mount_point = disk_mount_point(vol); - disk_path = cescape(src); - if (!disk_path) - return log_oom(); - + /* If the description string is simply the volume name, then let's not show this twice */ if (description && streq(vol, description)) - /* If the description string is simply the - * volume name, then let's not show this - * twice */ description = mfree(description); if (mount_point && description) @@ -367,13 +370,39 @@ static int get_password(const char *vol, const char *src, usec_t until, bool acc r = asprintf(&name_buffer, "%s on %s", vol, mount_point); else if (description) r = asprintf(&name_buffer, "%s (%s)", description, vol); - + else + return strdup(vol); if (r < 0) + return NULL; + + return name_buffer; +} + +static int get_password( + const char *vol, + const char *src, + usec_t until, + bool accept_cached, + char ***ret) { + + _cleanup_free_ char *friendly = NULL, *text = NULL, *disk_path = NULL; + _cleanup_strv_free_erase_ char **passwords = NULL; + char **p, *id; + int r = 0; + + assert(vol); + assert(src); + assert(ret); + + friendly = friendly_disk_name(src, vol); + if (!friendly) return log_oom(); - name = name_buffer ? name_buffer : vol; + if (asprintf(&text, "Please enter passphrase for disk %s:", friendly) < 0) + return log_oom(); - if (asprintf(&text, "Please enter passphrase for disk %s:", name) < 0) + disk_path = cescape(src); + if (!disk_path) return log_oom(); id = strjoina("cryptsetup:", disk_path); @@ -389,7 +418,7 @@ static int get_password(const char *vol, const char *src, usec_t until, bool acc assert(strv_length(passwords) == 1); - if (asprintf(&text, "Please enter passphrase for disk %s (verification):", name) < 0) + if (asprintf(&text, "Please enter passphrase for disk %s (verification):", friendly) < 0) return log_oom(); id = strjoina("cryptsetup-verification:", disk_path); @@ -447,6 +476,11 @@ static int attach_tcrypt( assert(name); assert(key_file || (passwords && passwords[0])); + if (arg_pkcs11_uri) { + log_error("Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11 support."); + return -EAGAIN; /* Ask for a regular password */ + } + if (arg_tcrypt_hidden) params.flags |= CRYPT_TCRYPT_HIDDEN_HEADER; @@ -492,14 +526,14 @@ static int attach_luks_or_plain( const char *name, const char *key_file, char **passwords, - uint32_t flags) { + uint32_t flags, + usec_t until) { int r = 0; bool pass_volume_key = false; assert(cd); assert(name); - assert(key_file || passwords); if ((!arg_type && !crypt_get_type(cd)) || streq_ptr(arg_type, CRYPT_PLAIN)) { struct crypt_params_plain params = { @@ -555,7 +589,39 @@ static int attach_luks_or_plain( crypt_get_volume_key_size(cd)*8, crypt_get_device_name(cd)); - if (key_file) { + if (arg_pkcs11_uri) { + _cleanup_free_ void *decrypted_key = NULL, *friendly = NULL; + size_t decrypted_key_size = 0; + + if (!key_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PKCS#11 mode selected but no key file specified, refusing."); + + friendly = friendly_disk_name(crypt_get_device_name(cd), name); + if (!friendly) + return log_oom(); + + r = acquire_pkcs11_key( + friendly, + arg_pkcs11_uri, + key_file, + arg_keyfile_size, arg_keyfile_offset, + until, + &decrypted_key, &decrypted_key_size); + if (r < 0) + return r; + + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + else + r = crypt_activate_by_passphrase(cd, name, arg_key_slot, decrypted_key, decrypted_key_size, flags); + if (r == -EPERM) { + log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)"); + return -EAGAIN; /* log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate with PKCS#11 acquired key: %m"); + + } else if (key_file) { r = crypt_activate_by_keyfile_offset(cd, name, arg_key_slot, key_file, arg_keyfile_size, arg_keyfile_offset, flags); if (r == -EPERM) { log_error_errno(r, "Failed to activate with key file '%s'. (Key data incorrect?)", key_file); @@ -739,7 +805,7 @@ static int run(int argc, char *argv[]) { for (tries = 0; arg_tries == 0 || tries < arg_tries; tries++) { _cleanup_strv_free_erase_ char **passwords = NULL; - if (!key_file) { + if (!key_file && !arg_pkcs11_uri) { r = get_password(argv[2], argv[3], until, tries == 0 && !arg_verify, &passwords); if (r == -EAGAIN) continue; @@ -750,7 +816,7 @@ static int run(int argc, char *argv[]) { if (streq_ptr(arg_type, CRYPT_TCRYPT)) r = attach_tcrypt(cd, argv[2], key_file, passwords, flags); else - r = attach_luks_or_plain(cd, argv[2], key_file, passwords, flags); + r = attach_luks_or_plain(cd, argv[2], key_file, passwords, flags, until); if (r >= 0) break; if (r != -EAGAIN) @@ -758,6 +824,7 @@ static int run(int argc, char *argv[]) { /* Passphrase not correct? Let's try again! */ key_file = NULL; + arg_pkcs11_uri = NULL; } if (arg_tries != 0 && tries >= arg_tries) From 1660e79d8febf79c58512d72d73899adf374db1e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 23 Aug 2019 13:19:55 +0200 Subject: [PATCH 097/109] man: add homectl(1) man page --- man/homectl.xml | 742 ++++++++++++++++++++++++++++++++++++++++++ man/rules/meson.build | 1 + 2 files changed, 743 insertions(+) create mode 100644 man/homectl.xml diff --git a/man/homectl.xml b/man/homectl.xml new file mode 100644 index 0000000000000..79a8e3fdd68d8 --- /dev/null +++ b/man/homectl.xml @@ -0,0 +1,742 @@ + + + + + + + + homectl + systemd + + + + homectl + 1 + + + + homectl + Create, remove, change or inspect home directories + + + + + homectl + OPTIONS + COMMAND + NAME + + + + + Description + + homectl may be used to create, remove, change or inspect a user's home + directory. It's primarily a command interfacing with + systemd-homed.service8 + which manages home directories of users. + + Home directories managed by systemd-homed.service are self-contained, and thus + include the user's full metadata record in the home's data storage itself, making them easily migratable + between machines. In particular a home directory in itself describes a matching user record, and every + user record managed by systemd-homed.service also implies existance and + encapsulation of a home directory. The user account and home directory hence become the same concept. The + following backing storage mechanisms are supported: + + + Individual LUKS2 encrypted loopback files for each user, located in + /home/*.home. At login the file systems contained in these files are mounted, + after the LUKS2 encrypted volume is attached. The user's password is identical to the encryption + passphrase of the LUKS2 volume. Access to data without preceeding user authentication is thus not + possible, not even for the systems administrator. This storage mechanism provides the strongest data + security and is thus recommended. + + Similar, but the LUKS2 encrypted file system is located on regular block device, such + as an USB storage stick. In this mode home directories and all data they include are nicely migratable + between machines, simply by plugging the USB stick into different systems at different + times. + + An encrypted directory using fscrypt on file systems that support it + (at the moment this is primarily ext4), located in + /home/*.homedir. This mechanism also provides encryption, but substantially + weaker than the two mechanisms described above, as most file system metadata is unprotected. Moreover + it currently does not support changing user passwords once the home directory has been + created. + + A btrfs subvolume for each user, also located in + /home/*.homedir. This provides no encryption, but good quota + support. + + A regular directory for each user, also located in + /home/*.homedir. This provides no encryption, but is a suitable fallback + available on all machines, even where LUKS2, fscrypt or btrfs + support is not available. + + An individual Windows file share (CIFS) for each user. + + + Note that systemd-homed.service and homectl will not manage + "classic" UNIX user accounts as created with useradd8 or + similar tools. In particular, this functionality is not suitable for managing system users (i.e. users + with a UID below 1000) but is exclusive to regular ("human") users. + + Note that users/home directories managed via systemd-homed.service do not show + up in /etc/passwd and similar files, they are synthesized via glibc NSS during + runtime. They are thus resolvable and may be enumerated via the getent1 + tool. + + This tool interfaces directly with systemd-homed.service, and may execute + specific commands on the home directories it manages. Since every home directory managed that way also + defines a JSON user and group record these home directories may also be inspected and enumerated via + userdbctl1. + + + + Options + + The following general options are understood (further options that control the various properties + of user records managed by systemd-homed.service are documented further + down): + + + + + FILE + + Read the user's JSON record from the specified file. If passed as + - reads the user record from standard input. The supplied JSON object must follow + the structure documented on JSON User + Records. This option may be used in conjunction with the create and + update commands (see below), where it allows configuring the user record in JSON + as-is, instead of setting the individual user record properties (see below). + + + + FORMAT + + + Controls whether to output the user record in JSON format, if the + inspect command (see below) is used. Takes one of pretty, + short or off. If pretty human-friendly + whitespace and newlines are inserted in the output to make the JSON data more readable. If + short all superfluous whitespace is suppressed. If off (the + default) the user information is not shown in JSON format but in a friendly human readable formatting + instead. The option picks pretty when run interactively and + short otherwise. + + + + FORMAT + + + + When used with the inspect verb in JSON mode (see above) may be + used to suppress certain aspects of the JSON user record on output. Specifically, if + stripped format is used the binding and runtime fields of the record are + removed. If minimal format is used the cryptographic signature is removed too. If + full format is used the full JSON record is shown (this is the default). This + option is useful for copying an existing user record to a different system in order to create a + similar user there with the same settings. Specifically: homectl inspect -EE | ssh + root@othersystem homectl create -i- may be used as simple command line for replicating a + user on another host. is equivalent to , + to . Note that when replicating user + accounts user records acquired in stripped mode will retain the original + cryptographic signatures and thus may only modified when the private key to update them is available + on the destination machine. When replicating users in minimal mode the signature + is remove during the replication and thus it is implicitly signed with the key of the destination + machine and thus may be updated there without any private key replication. + + + + + + + + + + + + + + + User Record Properties + + The following options control various properties of the user records/home directories that + systemd-homed.service manages. These switches may be used in conjunction with the + create and update commands for configuring various aspects of the + home directory and the user account: + + + + + NAME + NAME + + The real name for the user. This corresponds with the GECOS field on classic UNIX NSS + records. + + + + REALM + + The realm for the user. The realm associates a user with a specific organization or + installation, and allows distuingishing users of the same name defined in different contexts. The realm + can be any string that also qualifies as valid DNS domain name, and it is recommended to use the + organization's or installation's domain name for this purpose, but this is not enforced nor + required. On each system only a single user of the same name may exist, and if a user with the same + name and realm is seen it is assumed to refer to the same user while a user with the same name but + different realm is considered a different user. Assigning a realm to a user is + optional. + + + + EMAIL + + Takes an electronic mail address to associate with the user. On log-in the + $EMAIL environment variable is initialized from this value. + + + + TEXT + + Takes location specification for this user. This is free-form text, which might or + might not be usable by geo-location applications. Example: or + + + + TEXT + + Takes a password hint to store alongside the user record. This string is stored + accessible only to privileged users and the user itself and may not be queried by other users. + Example: + + + + ICON + + Takes an icon name to associate with the user, following the scheme defined by the Icon Naming + Specification. + + + + PATH + PATH + + Takes a path to use as home directory for the user. Note that this is the directory + the user's home directory is mounted to while the user is logged in. This is not where the user's + data is actually stored, see for that. If not specified defaults to + /home/$USER. + + + + UID + + Takes a preferred numeric UNIX UID to use for this user. Note that + systemd-homed may assign a user a different UID if needed if the UID is already in + use. The specified UID must be outside of the system user range. It is recommended to use the + 60001…60513 UID range for this purpose. If not specified the UID is automatically picked. When + logging in and the home directory is found to be owned by a UID not matching the user's assigned one + the home directory and all files and directories inside it will have their ownership + changed automatically before login completes. + + Note that users managed by systemd-homed always have a matching group + associated with the same name as well as a GID matching the UID of the user. Thus, configuring the + GID separately is not permitted. + + + + GROUP + GROUP + + Takes a comma-separated list of auxiliary UNIX groups this user shall belong + to. Example: to provide the user with administrator + privileges. Note that systemd-homed does not manage any groups besides a group + matching the user in name and numeric UID/GID. Thus any groups listed here must be registered + independently, for example with + groupadd8. If + non-existant groups are listed they are ignored. This option may be used more than once in case all + specified group lists are combined. + + + + PATH + + Takes a file system path to a directory. Specifies the skeleton directory to + initialize the home directory with. All files and directories in the specified are copied into any + newly create home directory. If not specified defaults to + /etc/skel/. + + + + SHELL + + Takes a file system path. Specifies the shell binary to execute on terminal + logins. If not specified defaults to /bin/bash. + + + + VARIABLE=VALUE + + Takes an environment variable assignment to set for all user processes. Note that a + number of other settings also result in environment variables to be set for the user, including + , and . May be used + multiple times to set multiple environment variables. + + + + TIMEZONE + + Takes a timezone specification as string that sets the timezone for the specified + user. When the user logs in the $TZ environment variable is initialized from this + setting. Example: . + + + + LANG + + Takes a specifier indicating the preferred language of the user. The + $LANG environment variable is initialized from this value on login, and thus a + value suitable for this environment variable is accepted here, for example + + + + + KEYS + Either takes a SSH authorized key line to associate with the user record or a + @ character followed by a path to a file to read one or more such lines from. SSH + keys configured this way are made available to SSH to permit access to this home directory and user + record. This option may be used more than once to configure multiple SSH keys. + + + + BOOLEAN + + Takes a boolean argument. Specifies whether this user account shall be locked. If + true logins into this account are prohibited, if false (the default) they are permitted (of course, + only if authorization otherwise succeeds). + + + + TIMESTAMP + TIMESTAMP + + These options take a timestamp string, in the format documented in + systemd.time7 and + configures points in time before and after logins into this account are not + permitted. + + + + SECS + NUMBER + + Configures a rate limit on authentication attempts for this user. If the user + attempts to authenticate more often than the specified number, on a specific system, within the + specified time interval authentication is refused until the time interval passes. Defaults to 10 + times per 1min. + + + + BOOL + + + Takes a boolean argument. Configures whether to enforce the system's password policy + for this user, regarding quality and strength of selected passwords. Defaults to + on. is short for + . + + + + BYTES + Either takes a size in bytes as argument (possibly using the usual K, M, G, … + suffixes for 1024 base values), or a percentage value and configures the disk space to assign to the + user. If a percentage value is specified (i.e. the argument suffixed with %) it is + taken relative to the available disk space of the backing file system. If the LUKS2 backend is used + this configures the size of the loopback file and file system contained therein. For the other + storage backends configures disk quota using the filesystem's native quota logic, if available. If + not specified, defaults to 85% of the available disk space for the LUKS2 backend and to no quota for + the others. + + + + MODE + + Takes a UNIX file access mode written in octal. Configures the access mode of the + home directory itself. Note that this is only used when the directory is first created, and the user + may change this any time afterwards. Example: + + + + + MASK + + Takes the access mode mask (in octal syntax) to apply to newly created files and + directories of the user ("umask"). If set this controls the initial umask set for all login sessions of + the user, possibly overriding the system's defaults. + + + + NICE + + Takes the numeric scheduling priority ("nice level") to apply to the processes of the user at login + time. Takes a numeric value in the range -20 (highest priority) to 19 (lowest priority). + + + + LIMIT=VALUE:VALUE + + Allows configuration of resource limits for processes of this user, see getrlimit2 + for details. Takes a resource limit name (e.g. LIMIT_NOFILE) followed by an equal + sign, followed by a numeric limit. Optionally, separated by colon a second numeric limit may be + specified. If two are specified this refers to the soft and hard limits, respectively. If only one + limit is specified the setting sets both limits in one. + + + + TASKS + + Takes a non-zero unsigned integer as argument. Configures the maximum numer of tasks + (i.e. processes and threads) the user may have at any given time. This limit applies to all tasks + forked off the user's sessions, even if they change user identity via su1 or a + similar tool. Use to place a limit on the tasks actually + running under the UID of the user, thus excluding any child processes that might have changed user + identity. This controls the TasksMax= settting of the per-user systemd slice unit + user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + BYTES + BYTES + + Set a limit on the memory a user may take up on a system at any given time in bytes + (the usual K, M, G, … suffixes are supported, to the base of 1024). This includes all memory used by + the user itself and all processes they forked off that changed user credentials. This controls the + MemoryHigh= and MemoryMax= settings of the per-user systemd + slice unit user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + WEIGHT + WEIGHT + + Set a CPU and IO scheduling weights of the processes of the user, including those of + processes forked off by the user that changed user credentials. Takes a numeric value in the range + 1…10000. This controls the CPUWeight= and IOWeight= settings of + the per-user systemd slice unit user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + STORAGE + + Selects the storage mechanism to use for this home directory. Takes one of + luks, fscrypt, directory, + subvolume, cifs. For details about these mechanisms, see + above. If a new home directory is created and the storage type is not specifically specified defaults + to luks if supported, subvolume as first fallback if supported, + and directory if not. + + + + PATH + + Takes a file system path. Configures where to place the user's home directory. When + LUKS2 storage is used refers to the path to the loopback file, otherwise to the path to the home + directory. When unspecified defaults to /home/$USER.home when LUKS storage is + used and /home/$USER.homedir for the other storage mechanisms. Not defined for + the cifs storage mechanism. To use LUKS2 storage on a regular block device (for + example a USB stick) pass the path to the block device here. + + + + TYPE + + When LUKS2 storage is used configures the file system type to use inside the home + directory LUKS2 container. One of ext4, xfs, + btrfs. If not specified defaults to ext4. Note that + xfs is not recommended as its support for file system resizing is too + limited. + + + + BOOL + + When LUKS2 storage is used configures whether to enable the + discard feature of the file system. If enabled the file system on top of the LUKS2 + volume will report empty block information to LUKS2 and the loopback file below, ensuring that empty + space in the home directory is returned to the backing file system below the LUKS2 volume, resulting + in a "sparse" loopback file. This option mostly defaults to off, since this permits over-committing + home directories which results in I/O errors if the underlying file system runs full while the upper + file system wants to allocate a block. Such I/O errors are generally not handled well by file systems + nor applications. When LUKS2 storage is used on top of regular block devices (instead of on top a + loopback file) the discard logic defaults to on. + + + + CIPHER + MODE + BITS + TYPE + ALGORITHM + SECONDS + BYTES + THREADS + + Configures various cryptographic parameters for the LUKS2 storage mechanism. See + cryptsetup8 + for details on the specific attributes. + + + + BOOL + BOOL + BOOL + + Configures the nosuid, nodev and + noexec mount options for the home directories. By default nodev + and nosuid are on, while noexec is off. For details about these + mount options see mount8. + + + + DOMAIN + USER + SERVICE + + Configures the Windows File Sharing (CIFS) domain and user to associate with the home + directory/user account, as well as the file share ("service") to mount as directory. The latter is used when + cifs storage is selected. + + + + SECS + + Configures the time the per-user service manager shall continue to run after the all + sessions of the user ended. The default is configured in + logind.conf5 (for + home directories of LUKS2 storage located on removable media this defaults to 0 though). A longer + time makes sure quick, repetitive logins are more efficient as the user's service manager doesn't + have to be started every time. + + + + BOOL + + Configures whether to kill all processes of the user on logout. The default is + configured in + logind.conf5. + + + + BOOL + + Takes a boolean argument. Configures whether the graphical UI of the system should + automatically log this user in if possible. Defaults to off. If less or more than one user is marked + this way automatic login is disabled. + + + + + + Commands + + The following commands are understood: + + + + + list + + List all home directories (along with brief details) currently managed by + systemd-homed.service. This command is also executed if none is specified on the + command line. + + + + activate USER [USER…] + + Activate one or more home directories. The home directories of each listed user will + be activated and made available under their mount points (typically in + /home/$USER). Note that any home activated this way stays active indefinitely, + until it is explicitly deactivated again (with deactivate, see below), or the user + logs in and out again and it thus is deactivated due to the automatic deactivation-on-logout + logic. + + Activation of a home directory involves various operations that depend on the selected storage + mechanism. If the LUKS2 mechanism is used, this generally involves: setting up a loopback device, + validating and activating the LUKS2 volume, checking the file system, mounting the file system, and + potentiatlly changing the ownership of all included files to the correct UID/GID. + + + + deactivate USER [USER…] + + Deactivate one or more home directories. This undoes the effect of + activate. + + + + inspect USER [USER…] + + Show various details about the specified home directories. This shows various + information about the home directory and its user account, including runtime data such as current + state, disk use and similar. Combine with to show the detailed JSON user + record instead, possibly combined with to suppress certain aspects + of the output. + + + + authenticate USER [USER…] + + Validate authentication credentials of a home directory. This queries the caller for + a password (or similar) and checks that it correctly unlocks the home directory. + + + + create USER + create PATH USER + + Create a new home directory/user account of the specified name. Use the various + user record property options (as documented above) to control various aspects of the home directory + and its user accounts. + + + + remove USER + + Remove a home directory/user account. + + + + update USER + update PATH USER + + Update a home directory/user account. Use the various user record property options + (as documented above) to make changes to the account, or alternatively provide a full, updated JSON + user record via the option. + + Note that changes to user records not signed by a cryptographic private key available locally + are not permitted, unless is used with a user record that is already + correctly signed by a recognized private key. + + + + passwd USER + + Change the password of the specified home direcory/user account. + + + + resize USER BYTES + + Change the disk space assigned to the specified home directory. If the LUKS2 storage + mechanism is used this will automatically resize the loopback file and the file system contained + within. Note that if ext4 is used inside of the LUKS2 volume the home directory + while activated may only be grown, and for being shrunk needs to be deactivated first (i.e. the user + has to log out). If xfs is used inside of the LUKS2 volume the home directory may + not be shrunk whatsoever. On all three of ext4, xfs and + btrfs the home directory may be grown while the user is logged in, and on the + latter also shrunk while the user is logged in. If the subvolume, + directory, fscrypt storage mechanisms are used, resizing will + change file system quota. + + + + lock USER + + Temporarily suspend access to the user's home directory and remove any associated + cryptographic keys from memory. Any attempts to access the user's home directory will stall until the + home directory is unlocked again (while re-authenticating). This functionality is primarily intended + to be used during system suspend to make sure the user's data cannot be accessed until the user + re-authenticates on resume. This operation is only defined for home directories that use the LUKS2 + storage mechanism. + + + + unlock USER + + Resume access to the user's home directory again, undoing the effect of + lock above. This requires authentication of the user, as the cryptographic keys + required for access to the home directory need to be reacquired. + + + + lock-all + + Execute the lock command on all suitable home directories at + once. This operation is generally execute on system suspend, to ensure all active user's + cryptographic keys for accessing their home directories are removed from memory. + + + + with USER COMMAND… + + Activate the specified user's home directory, run the specified command (under the + caller's identity, not the specified user's) and deactivate the home directory afterwards again + (unless the user is logged in otherwise). This command is useful for running privileged backup + scripts and such, but requires authentication with the user's credentials in order to be able to + unlock the user's home directory. + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + + + Examples + + + Create a user <literal>waldo</literal> in the administrator group <literal>wheel</literal>, and + assign 500 MiB disk space to them. + + homectl create waldo --real-name="Waldo McWaldo" -G wheel --disk-size=500M + + + + Create a user <literal>wally</literal> on a USB stick, and assign a maximum of 500 concurrent + tasks to them. + + homectl create wally --real-name="Wally McWally" --image-path=/dev/disk/by-id/usb-SanDisk_Ultra_Fit_476fff954b2b5c44-0:0 --tasks-max=500 + + + + Change nice level of user <literal>odlaw</literal> to +5 and make sure the environment variable + <varname>$SOME</varname> is set to the string <literal>THING</literal> for them on login. + + homectl update odlaw --nice=5 --setenv=SOME=THING + + + + + See Also + + systemd1, + systemd-homed.service8, + userdbctl1, + useradd8, + cryptsetup8 + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index 3b63311d7b52b..d566eb827e98a 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -17,6 +17,7 @@ manpages = [ ['environment.d', '5', [], 'ENABLE_ENVIRONMENT_D'], ['file-hierarchy', '7', [], ''], ['halt', '8', ['poweroff', 'reboot'], ''], + ['homectl', '1', [], 'ENABLE_HOMED'], ['hostname', '5', [], ''], ['hostnamectl', '1', [], 'ENABLE_HOSTNAMED'], ['hwdb', '7', [], 'ENABLE_HWDB'], From 4dc65ee12bec4927bfeffd9be0bcc313e14bee03 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 12:36:10 +0200 Subject: [PATCH 098/109] man: add systemd-homed man page --- man/rules/meson.build | 1 + man/systemd-homed.service.xml | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 man/systemd-homed.service.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index d566eb827e98a..de395b5df8a20 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -698,6 +698,7 @@ manpages = [ '8', ['systemd-hibernate-resume'], 'ENABLE_HIBERNATE'], + ['systemd-homed.service', '8', ['systemd-homed'], 'ENABLE_HOMED'], ['systemd-hostnamed.service', '8', ['systemd-hostnamed'], 'ENABLE_HOSTNAMED'], ['systemd-hwdb', '8', [], 'ENABLE_HWDB'], ['systemd-id128', '1', [], ''], diff --git a/man/systemd-homed.service.xml b/man/systemd-homed.service.xml new file mode 100644 index 0000000000000..9313ea03141a5 --- /dev/null +++ b/man/systemd-homed.service.xml @@ -0,0 +1,57 @@ + + + + + + + + systemd-homed.service + systemd + + + + systemd-homed.service + 8 + + + + systemd-homed.service + systemd-homed + Home Directory/User Account Manager + + + + systemd-homed.service + /usr/lib/systemd/systemd-homed + + + + Description + + systemd-homed is a system service that may be used to create, remove, change or + inspect home directories. + + Most of systemd-homed's functionality is accessible through the + homectl1 command. + + See the Home Directories documentation for + details about the format and design of home directories managed by + systemd-homed.service. + + Each home directory managed by systemd-homed.service synthesizes a local user + and group. These are made available to the system using the Varlink User/Group Record Lookup API, and thus may be browsed + with + userdbctl1. + + + + See Also + + systemd1, + homectl1, + userdbctl1 + + + From 5c9eb170152cf6de939baa0268ac99fd212e8677 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 12:06:53 +0200 Subject: [PATCH 099/109] man: add man page for sd_bus_message_sensitive() --- man/rules/meson.build | 1 + man/sd_bus_message_sensitive.xml | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 man/sd_bus_message_sensitive.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index de395b5df8a20..3d5e9028d7004 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -272,6 +272,7 @@ manpages = [ ['sd_bus_message_read_array', '3', [], ''], ['sd_bus_message_read_basic', '3', [], ''], ['sd_bus_message_rewind', '3', [], ''], + ['sd_bus_message_sensitive', '3', [], ''], ['sd_bus_message_set_destination', '3', ['sd_bus_message_get_destination', diff --git a/man/sd_bus_message_sensitive.xml b/man/sd_bus_message_sensitive.xml new file mode 100644 index 0000000000000..a4a732cfd152c --- /dev/null +++ b/man/sd_bus_message_sensitive.xml @@ -0,0 +1,85 @@ + + + + + + + + sd_bus_message_sensitive + systemd + + + + sd_bus_message_sensitive + 3 + + + + sd_bus_message_sensitive + + Mark a message object as containing sensitive data + + + + + #include <systemd/sd-bus.h> + + + int sd_bus_message_sensitive + sd_bus_message *message + + + + + + Description + + sd_bus_message_sensitive() marks an allocated bus message as containing + sensitive data. This ensures that the message data is carefully removed from memory (specifically, + overwritten with zero bytes) when released. It is recommended to mark all incoming and outgoing messages + like this that contain security credentials and similar data that should be dealt with carefully. Note + that it is not possible to unmark messages like this, it's a one way operation. If a message is already + marked sensitive and then marked sensitive a second time the message remains marked so and no further + operation is executed. + + As a safety precaution all messages that are created as reply to messages that are marked sensitive + are also implicitly marked so. + + + + Return Value + + On success, theis functions return 0 or a positive integer. On failure, it returns a + negative errno-style error code. + + + Errors + + Returned errors may indicate the following problems: + + + + -EINVAL + + The message parameter is + NULL. + + + + + + + + + + See Also + + + systemd1, + sd-bus3, + sd_bus_message_new_method_call3 + + + + From ae298b0d63c9c69d90e20734ff685647d93048dc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 14:10:41 +0200 Subject: [PATCH 100/109] man: document pam_systemd_home --- man/pam_systemd_home.xml | 126 +++++++++++++++++++++++++++++++++++++++ man/rules/meson.build | 1 + 2 files changed, 127 insertions(+) create mode 100644 man/pam_systemd_home.xml diff --git a/man/pam_systemd_home.xml b/man/pam_systemd_home.xml new file mode 100644 index 0000000000000..f0a58bda15cfb --- /dev/null +++ b/man/pam_systemd_home.xml @@ -0,0 +1,126 @@ + + + + + + + + pam_systemd_home + systemd + + + + pam_systemd_home + 8 + + + + pam_systemd_home + Automatically mount home directories managed by systemd-homed.service on logout, and unmount them on logout + + + + pam_systemd_home.so + + + + Description + + pam_systemd_home ensures that home directories managed by + systemd-homed.service8 + are automatically activated (mounted) on user login, and are deactivated (unmounted) when the last + session of the user ends. + + + + Options + + The following options are understood: + + + + + suspend= + + Takes a boolean argument. If true, the home directory of the user will be suspended + automatically during system suspend; if false it will remain active. Automatic suspending of the home + directory improves security substantially as secret key material is automatically removed from memory + before the system is put into sleep and must be re-acquired (by user re-authentication) when coming + back from suspend. It is recommended to set this parameter for all PAM applications that have support + for automatically re-authenticating via PAM on system resume. If this behaviour is enabled without + the PAM application supporting this, access to the home directory will sooner or later stall and will + resume only after the user re-authenticated via any PAM session. If multiple sessions of the same + user are open in parallel the user's home directory will be left unsuspended on system suspend as + soon as at least one of the sessions does not set this parameter. Defaults to off. + + + + debug= + + Takes an optional boolean argument. If yes or without the argument, the module will log + debugging information as it operates. + + + + + + Module Types Provided + + The module provides all four management operations: , , + , . + + + + Environment + + The following environment variables are initialized by the module and available to the processes of the + user's session: + + + + $SYSTEMD_HOME=1 + + Indicates that the user's home directory is managed by systemd-homed.service. + + + + + + + Example + + #%PAM-1.0 +auth sufficient pam_unix.so nullok try_first_pass +auth sufficient pam_systemd_home.so +auth required pam_deny.so + +account required pam_nologin.so +account sufficient pam_unix.so +account sufficient pam_systemd_home.so + +password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok +password sufficient pam_systemd_home.so +password required pam_deny.so + +-session optional pam_keyinit.so revoke +-session optional pam_loginuid.so +-session optional pam_systemd_home.so +-session optional pam_systemd.so +session required pam_unix.so + + + + See Also + + systemd1, + systemd-homed.service8, + homed.conf5, + homectl1, + pam.conf5, + pam.d5, + pam8 + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index 3d5e9028d7004..587602dc19b9c 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -46,6 +46,7 @@ manpages = [ ['nss-systemd', '8', ['libnss_systemd.so.2'], 'ENABLE_NSS_SYSTEMD'], ['os-release', '5', [], ''], ['pam_systemd', '8', [], 'HAVE_PAM'], + ['pam_systemd_home', '8', [], 'HAVE_PAM'], ['portablectl', '1', [], 'ENABLE_PORTABLED'], ['pstore.conf', '5', ['pstore.conf.d'], 'ENABLE_PSTORE'], ['resolvectl', '1', ['resolvconf'], 'ENABLE_RESOLVE'], From 7b50b617ded97898dfe262b311719e7905d637ba Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 28 Aug 2019 12:40:23 +0200 Subject: [PATCH 101/109] man: document userdbctl(1) --- man/rules/meson.build | 1 + man/userdbctl.xml | 233 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 man/userdbctl.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index 587602dc19b9c..9d36057ef285d 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -946,6 +946,7 @@ manpages = [ ['udev_new', '3', ['udev_ref', 'udev_unref'], ''], ['udevadm', '8', [], ''], ['user@.service', '5', ['user-runtime-dir@.service'], ''], + ['userdbctl', '1', [], 'ENABLE_USERDB'], ['vconsole.conf', '5', [], 'ENABLE_VCONSOLE'] ] # Really, do not edit. diff --git a/man/userdbctl.xml b/man/userdbctl.xml new file mode 100644 index 0000000000000..72043a423aedb --- /dev/null +++ b/man/userdbctl.xml @@ -0,0 +1,233 @@ + + + + + + + + userdbctl + systemd + + + + userdbctl + 1 + + + + userdbctl + Inspect users, groups and group memberships + + + + + userdbctl + OPTIONS + COMMAND + NAME + + + + + Description + + userdbctl may be used to inspect user and groups (as well as group memberships) + of the system. This client utility inquires user/group information provided by various system services, + both operating on advanced JSON user/group records (as defined by the JSON User Record and JSON Group Record definitions), and classic UNIX NSS/glibc + user and group records. This tool is primarily a client to the Varlink User/Group Lookup API. + + + + Options + + The following options are understood: + + + + + MODE + + Choose the display mode, takes one of classic, + friendly, table, json. If + classic an output very close to the format of /etc/passwd or + /etc/group is generated. If friendly a more comprehensive and + user friendly, human readable output is generated; if table a minimal, tabular + output is generated; if json a JSON formatted output is generated. Defaults to + friendly if a user/group is specified on the command line, + table otherwise. + + + + SERVICE:SERVICE… + SERVICE:SERVICE… + + Controls which services to enquire for looking up or enumerating users/groups. Takes + a list of one or more service names, separated by :. See below for a list of + well-known service names. If not specified all available services are enquired at + once. + + + + BOOL + + Controls whether to include classic glibc/NSS user/group lookups in the output. If + is used any attempts to resolve or enumerate users/groups provided + only via glibc NSS is suppressed. If is specified such users/groups + are included in the output (which is the default). + + + + BOOL + + Controls whether to synthesize records for the root and nobody users/groups if they + aren't defined otherwise. By default (or yes) such records are implicitly + synthesized if otherwise missing since they have special significance to the OS. When + no this synthesizing is turned off. + + + + + + This option is short for + . Use this option to show only records that are natively defined as + JSON user or group records, with all NSS/glibc compatibility and all implicit synthesis turned + off. + + + + + + + + + + + Commands + + The following commands are understood: + + + + + user USER + + List all known users records or show details of one or more specified user + records. Use to tweak display mode. + + + + group GROUP + + List all known group records or show details of one or more specified group + records. Use to tweak display mode. + + + + users-in-group GROUP + + List users that are members of the specified groups. If no groups are specified list + all user/group memberships defined. Use to tweak display + mode. + + + + groups-of-user USER + + List groups that the specified users are members of. If no users are specified list + all user/group memberships defined (in this case groups-of-user and + users-in-group are equivalent). Use to tweak display + mode. + + + + services + + List all services currently providing user/group definitions to the system. See below + for a list of well-known services providing user information. + + + + + + Well-Known Services + + The userdbctl services command will list all currently running services that + provide user or group definitions to the system. The following are well-known services are shown among + this list. + + + + + io.systemd.DynamicUser + + This service is provided by the system service manager itself (i.e. PID 1) and + makes all users (and their groups) synthesized through the DynamicUser= setting in + service unit files available to the system (see + systemd.exec5 for + details about this setting). + + + + io.systemd.Home + + This service is provided by + systemd-homed.service8 + and makes all users (and their groups) belonging to home directories managed by that service + available to the system. + + + + io.systemd.Multiplexer + + This service is provided by + systemd-userdbd.service8 + and multiplexes user/group look-ups to all other running lookup services. This is the primary entry point + for user/group record clients, as it simplifies client side implementation substantially since they + can ask a single service for lookups instead of asking all running services in parallel. + userdbctl uses this service preferably, too, unless + or are used, in which case finer control over the services to talk to is + required. + + + + io.systemd.NameSeviceSwitch + + This service is (also) provided by + systemd-userdbd.service8 + and converts classic NSS/glibc user and group records to JSON user/group records, providing full + backwards compatibility. Use to disable this compatibility, see + above. Note that compatibility is actually provided in both directions: + nss-systemd8 will + automatically synthesize classic NSS/glibc user/group records from all JSON user/group records + provided to the system, thus using both APIs is mostly equivalent and provides access to the same + data, however the NSS/glibc APIs necessarily expose a more reduced set of fields + only. + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + + + See Also + + systemd1, + systemd-userdbd.service8, + systemd-homed.service8, + nss-systemd8, + getent1 + + + + From 8ccd3486c052ae19095128ae7940764e6bd664ee Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 28 Aug 2019 13:33:13 +0200 Subject: [PATCH 102/109] man: document systemd-userdbd.service --- man/rules/meson.build | 1 + man/systemd-userdbd.service.xml | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 man/systemd-userdbd.service.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index 9d36057ef285d..6efe2b6045c5f 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -815,6 +815,7 @@ manpages = [ ['systemd-update-utmp', 'systemd-update-utmp-runlevel.service'], 'ENABLE_UTMP'], ['systemd-user-sessions.service', '8', ['systemd-user-sessions'], 'HAVE_PAM'], + ['systemd-userdbd.service', '8', ['systemd-userdbd'], 'ENABLE_USERDB'], ['systemd-vconsole-setup.service', '8', ['systemd-vconsole-setup'], diff --git a/man/systemd-userdbd.service.xml b/man/systemd-userdbd.service.xml new file mode 100644 index 0000000000000..b73004f7d29f4 --- /dev/null +++ b/man/systemd-userdbd.service.xml @@ -0,0 +1,68 @@ + + + + + + + + systemd-userdbd.service + systemd + + + + systemd-userdbd.service + 8 + + + + systemd-userdbd.service + systemd-userdbd + JSON User/Group Record Query Multiplexer/NSS Compatibility + + + + systemd-userdbd.service + /usr/lib/systemd/systemd-userdbd + + + + Description + + systemd-userdbd is a system service that multiplexes user/group lookups to all + local services that provide JSON user/group record definitions to the system. In addition it synthesizes + JSON user/group records from classic UNIX/glibc NSS user/group records in order to provide full backwards + compatibility. + + Most of systemd-userdbd's functionality is accessible through the + userdbctl1 + command. + + The user and group records this service provides access to follow the JSON User Record and JSON Group Record definitions. This service implements the + Varlink User/Group Record Lookup API, and + multiplexes access other services implementing this API, too. It is thus both server and client of this + API. + + This service provides two distinct Varlink services: + io.systemd.Multiplexer provides a single, unified API for querying JSON user and + group records. Internally it talks to all other user/group record services running on the system in + parallel and forwards any information discovered. This simplifies clients substantially since they need + to talk to a single service only instead of all of them in + parallel. io.systemd.NameSeviceSwitch provides compatibility with classic UNIX/glibc + NSS user records, i.e. converts struct passwd and struct group records as + acquired with APIs such as getpwnam1 to JSON + user/group records, thus hiding the differences between the services as much as possible. + + + + See Also + + systemd1, + nss-systemd8, + userdbctl1 + + + From b16d05a421f028cdad24ab0af84af6aac1273fbd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 15:31:17 +0200 Subject: [PATCH 103/109] docs: document homed UID range --- docs/UIDS-GIDS.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/UIDS-GIDS.md b/docs/UIDS-GIDS.md index 480ee231e7431..6cb0e144fbebc 100644 --- a/docs/UIDS-GIDS.md +++ b/docs/UIDS-GIDS.md @@ -94,7 +94,15 @@ but downstreams are strongly advised against doing that.) `systemd` defines a number of special UID ranges: -1. 61184…65519 → UIDs for dynamic users are allocated from this range (see the +1. 60001…60513 → UIDs for home directories managed by + [`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html). UIDs + from this range are automatically assigned to any home directory discovered, + and persisted locally on first login. On different systems the same user + might get different UIDs assigned in case of conflict, though it is + attempted to make UID assignments stable, by hashing them from the user + name. + +2. 61184…65519 → UIDs for dynamic users are allocated from this range (see the `DynamicUser=` documentation in [`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)). This range has been chosen so that it is below the 16bit boundary (i.e. below @@ -109,7 +117,7 @@ but downstreams are strongly advised against doing that.) user record resolving works correctly without those users being in `/etc/passwd`. -2. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of +3. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of per-container UID ranges. When the `--private-users=pick` switch is used (or `-U`) then it will automatically find a so far unused 16bit subrange of this range and assign it to the container. The range is picked so that the upper @@ -230,7 +238,8 @@ the artifacts the container manager persistently leaves in the system. | 5 | `tty` group | `systemd` | `/etc/passwd` | | 6…999 | System users | Distributions | `/etc/passwd` | | 1000…60000 | Regular users | Distributions | `/etc/passwd` + LDAP/NIS/… | -| 60001…61183 | Unused | | | +| 60001…60513 | Human Users (homed) | `systemd` | `nss-systemd` +| 60514…61183 | Unused | | | | 61184…65519 | Dynamic service users | `systemd` | `nss-systemd` | | 65520…65533 | Unused | | | | 65534 | `nobody` user | Linux | `/etc/passwd` + `nss-systemd` | From 2df59ec75b6f3775eda7e806c2c064b7c6e08d55 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 26 Aug 2019 18:15:40 +0200 Subject: [PATCH 104/109] docs: document the home directory format --- docs/HOME_DIRECTORY.md | 163 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/HOME_DIRECTORY.md diff --git a/docs/HOME_DIRECTORY.md b/docs/HOME_DIRECTORY.md new file mode 100644 index 0000000000000..b07e0c4886f64 --- /dev/null +++ b/docs/HOME_DIRECTORY.md @@ -0,0 +1,163 @@ +--- +title: Home Directories +--- + +# Home Directories + +[`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html) +manages home directories of regular ("human") users. Each directory it manages +encapsulates both the data store and the user record of the user so that it +comprehensively describes the user account, and is thus naturally portable +between systems without any further, external metadata. This document describes +the format used by these home directories, in context of the storage mechanism +used. + +## General Structure + +Inside of the home directory a file `~/.identity` contains the JSON formatted +user record of the user. It follows the format defined in [`JSON User +Records`](https://systemd.io/USER_RECORDS). It is recommended to bring the +record into 'normalized' form (i.e. all objects should contain their fields +sorted alphabetically by their key) before storing it there, though this is not +required nor enforced. Since the user record is cryptographically signed the +user cannot make modifications to the file on their own (at least not without +corrupting it, or knowing the private key used for signing the record). Note +that user records are stored here without their `binding`, `status` and +`secret` sections, i.e. only with the sections included in the signature plus +the signature section itself. + +## Storage Mechanism: Plain Directory/`btrfs` Subvolume + +If the plain directory or `btrfs` subvolume storage mechanism of +`systemd-homed` is used (i.e. `--storage=directory` or `--storage=subvolume` on +the +[`homectl(1)`](https://www.freedesktop.org/software/systemd/man/homectl.html) +command line) the home directory requires no special set-up besides including +the user record in the `~/.identity` file. + +It is recommended to name home directories managed this way by +`systemd-homed.service` by the user name, suffixed with `.homedir` (example: +`lennart.homedir` for a user `lennart`) but this is not enforced. When the user +is logged in the directory is generally mounted to `/home/$USER` (in our +example: `/home/lennart`), thus dropping the suffix while the home directory is +active. `systemd-homed` will automatically discover home directories named this +way in `/home/*.homedir` and synthesize NSS user records for them as they show +up. + +## Storage Mechanism: `fscrypt` Directories + +This storage mechanism is mostly identical to the plain directory storage +mechanism, except that the home directory is encrypted using `fscrypt`. (Use +`--storage=fscrypt` on the `homectl` command line.) There is currently no key +management in place for this, the `fscrypt` encryption key is directly derived +from the supplied user password using PBKDF2. Due to this password changes are +currently not supported if this storage mechanism is used. The salt value for +the key derivation function is stored in the `trusted.fscrypt_salt` extended +attribute, encoded in base64. + +## Storage Mechanism: `cifs` Home Directories + +In this storage mechanism the home directory is mounted from a CIFS server and +service at login, configured inside the user record. (Use `--storage=cifs` on +the `homectl` command line.) The local password of the user is used to log into +the CIFS service. The directory share needs to contain the user record in +`~/.identity` as well. Note that this means that the user record needs to be +registered locally before it can be mounted for the first time, since CIFS +domain and server information needs to be known *before* the mount. Note that +for all other storage mechanisms it is entirely sufficient if the directories +or storage artifacts are placed at the right locations — all information to +activate them can be derived automatically from their mere availability. + +## Storage Mechanism: `luks` Home Directories + +This is the most advanced and most secure storage mechanism and consists of a +Linux file system inside a LUKS2 volume inside a loopback file (or on removable +media). (Use `--storage=luks` on the `homectl` command line.) Specifically: + +* The image contains a GPT partition table. For now it should only contain a + single partition, and that partition must have the type UUID + `773f91ef-66d4-49b5-bd83-d683bf40ad16`. It's partition label must be the + user name. + +* This partition must contain a LUKS2 volume, whose label must be the user + name. The LUKS2 volume must contain a LUKS2 token field of type + `systemd-homed`. The JSON data of this token must have a `record` field, + containing a string with base64-encoded data. This data is the JSON user + record, in the same serialization as in `~/.identity`, though encrypted. The + JSON data of this token must also have an `iv` field, which contains a + base64-encoded binary initialization vector for the encryption. The + encryption used is the same as the LUKS2 volume itself uses, unlocked by the + same volume key, but based on its own IV. + +* Inside of this LUKS2 volume must be a Linux file system, one of `ext4`, + `btrfs` and `xfs`. The file system label must be the user name. + +* This file system should contain a single directory named after the user. This + directory will become the home directory of the user when activated. It + contains a second copy of the user record in the `~/.identity` file, like in + the other storage mechanisms. + +The image file should either reside in a directory `/home/` on the system, +named after the user, suffixed with `.home`. When activated the container home +directory is mounted to the same path, though with the `.home` suffix +dropped. (e.g.: the loopback file `/home/waldo.home` is mounted to +`/home/waldo` while activated.) When the image is stored on removable media +(such as a USB stick) the image file can be directly `dd`'ed onto it, the +format is unchanged. The GPT envelope should ensure the image is properly +recognizable as a home directory both when used in a loopback file and on a +removable USB stick. + +Rationale for the GPT partition table envelope: this way the image is nicely +discoverable and recognizable already by partition managers as a home +directory. Moreover, when copied onto a USB stick the GPT envelope makes sure +the stick is properly recognizable as a portable home directory +medium. (Moreover it allows to embed additional partitions later on, for +example for allowing a multi-purpose USB stick that contains both a home +directory and a generic storage volume.) + +Rationale for including the encrypted user record in the the LUKS2 header: +Linux kernel file system implementations are generally not robust towards +maliciously formatted file systems; there's a good chance that file system +images can be used as attack vectors, exploiting the kernel. Thus it is +necessary to validate the home directory image *before* mounting it and +establishing a minimal level of trust. Since the user record data is +cryptographically signed and user records not signed with a recognized private +key are not accepted a minimal level of trust between the system and the home +directory image is established. + +Rationale for storing the home directory one level below to root directory of +the contained file system: this way special directories such as `lost+found/` +do not show up in the user's home directory. + +## Algorithm + +Regardless of the storage mechanism used, an activated home directory +necessarily involves a mount point to be established. In case of the +directory-based storage mechanisms (`directory`, `subvolume` and `fscrypt`) +this is a bind mount, in case of `cifs` this is a CIFS network mount, and in +case of the LUKS2 backend a regular block device mount of the file system +contained in the LUKS2 image. By requiring a mount for all cases (even for +those that already are a directory) a clear logic is defined to distuingish +active and inactive home directories, so that the directories become +inaccessible under their regular path the instant they are +deactivated. Moreover, the `nosuid`, `nodev` and `noexec` flags configured in +the user record are applied when the bind mount is established. + +During activation, the user records retained on the host, the user record +stored in the LUKS2 header (in case of the LUKS2 storage mechanism) and the +user record stored inside the home directory in `~/.identity` are +compared. Activation is only permitted if they match the same user and are +signed by a recognized key. When the three instances differ in `lastChangeUSec` +field, the newest record wins, and is propagated to the other two locations. + +During activation the file system checker (`fsck`) appropriate for the +selected file system is automatically invoked, ensuring the file system is in a +healthy state before it is mounted. + +If the UID assigned to a user does not match the owner of the home directory in +the file system, the home directory is automatically and recursively `chown()`ed +to the correct UID. + +Depending on the `discard` setting of the user record either the backing +loopback file is `fallocate()`ed during activation, or the mounted file system +is `FITRIM`ed after mounting, to ensure the setting is correctly enforced. From 137187c9b762db514e5c4b9e0bf166c73f69463c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 27 Aug 2019 15:57:33 +0200 Subject: [PATCH 105/109] docs: add documentation for JSON user records --- docs/USER_RECORD.md | 902 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 902 insertions(+) create mode 100644 docs/USER_RECORD.md diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md new file mode 100644 index 0000000000000..b7338e96f6c34 --- /dev/null +++ b/docs/USER_RECORD.md @@ -0,0 +1,902 @@ +--- +title: JSON User Records +--- + +# JSON User Records + +systemd optionally processes user records that go beyond the classic UNIX (or +glibc NSS) `struct passwd`. Various components of systemd are able to provide +and consume records in a more advanced JSON-based format. Specifically: + +1. [`systemd-homed.service`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html) + manages `human` user home directories and embeds these JSON records + directly in the home directory images (see [Home + Directories](https://systemd.io/HOME_DIRECTORY)) for details. + +2. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) + processes these JSON records for users that log in, and applies various + settings to the activated session, including environment variables, nice + levels and more. + +3. [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html) + processes these JSON records users that log in, and applies various resource + management settings to the per-user slice units it manages. This allows + setting global limits on resource consumption by a specific user. + +4. [`nss-systemd`](https://www.freedesktop.org/software/systemd/man/nss-systemd.html) + is a glibc NSS module that synthesizes classic NSS records from these JSON + records, providing full backwards compatibility with the classic UNIX APIs + both for look-up and enumeration. + +5. The service manager (PID 1) exposes dynamic users (i.e. users synthesized as + effect of `DynamicUser=` in service unit files) as these advanced JSON + records, making them discoverable to the rest of the system. + +6. [`systemd-userdbd.service`](https://www.freedesktop.org/software/systemd/man/systemd-userdbd.service.html) + is a small service that can translate UNIX/glibc NSS records to these JSON + user records. It also provides a unified [Varlink](https://varlink.org/) API + for querying and enumerating records of this type, optionally acquiring them + from various other services. + +JSON user records may contain various fields that are not available in `struct +passwd`, and are extensible for other applications. For example, the record may +contain information about: + +1. Additional security credentials (PKCS#11 security token information, + biometrical authentication information, SSH public key information) + +2. Additional user metadata, such as a picture, email address, location string, + preferred language or timezone + +3. Resource Management settings (such as CPU/IO weights, memory and tasks + limits, classic UNIX resource limits or nice levels) + +4. Runtime parameters such as environment variables or the `nodev`, `noexec`, + `nosuid` flags to use for the home directory + +5. Information about where to mount the home directory from + +And various other things. The record is intended to be extensible, for example +the following extensions are thinkable: + +1. Windows network credential information + +2. Information about default IMAP, SMTP servers to use for this user + +3. Parental control information to enforce on this user + +4. Default parameters for backup applications and similar + +Similar to JSON User Records there are also [JSON Group +Records](https://systemd.io/GROUP_RECORD.md) that encapsulate UNIX groups. + +JSON User Records may be transferred or written to disk in various protocols +and formats. To inquire about such records defined on the local system use the +[Varlink User/Group Lookup API](https://systemd.io/USER_GROUP_API.md). + +## Why JSON? + +JSON is nicely extensible and widely used. In particular it's easy to +synthesize and process is with numerous programming languages. It's +particularly popular in the web communities, which hopefully should make it +easy to link user credential data from the web and from local systems more +closely together. + +## General Structure + +The JSON user records generated and processed by systemd follow a general +structure, consisting of seven distinct "sections". Specifically: + +1. Various fields are placed at the top-level of user record (the `regular` + section). These are generally fields that shall apply unconditionally to the + user in all contexts, are portable and not security sensitive. + +2. A number of fields are located in the `privileged` section (a sub-object of + the user record). Fields contained in this object are security sensitive, + i.e. contain information that the user and the administrator should be able + to see, but other users should not. In many ways this matches the data + stored in `/etc/shadow` in classic Linux user accounts, i.e. includes + password hashes and more. Algorithmically, when a user record is passed to + an untrusted client, by monopolizing such sensitive records in a single + object field we can easily remove it from view. + +3. A number of fields are located in objects inside the `perMachine` section + (an array field of the user record). Primarily these are resource + management-related fields, as those tend to make sense on a specific system + only, e.g. limiting a user's memory use to 1G only makes sense on a specific + system that has more than 1G of memory. Each object inside the `perMachine` + array comes with a `matchMachineId` or `matchHostname` field which indicate + which systems to apply the listed settings to. Note that many fields + accepted in the `perMachine` section can also be set at the top level (the + `regular` section), where they define the fallback if no matching object in + `perMachine` is found. + +4. Various fields are located in the `binding` section (a sub-sub-object of the + user record; an intermediary object is insert which is keyed by the machine + ID of the host). Fields included in this section "bind" the object to a + specific system. They generally include non-portable information about paths + or UID assignments, that are true on a specific system, but not necessarily + on others, and which are managed automatically by some user record manager + (such as `systemd-homed`). Data in this section is considered part of the + user record only in the local context, and is generally not ported to other + systems. Due to that it is not included in the reduced user record the + cryptographic signature defined in the `signature` section is calculated + on. In `systemd-homed` this section is also removed when the user's record + is stored in the `~.identity` file in the home directory, so that every + system with access to the home directory can manage these `binding` fields + individually. Typically, the binding section is persisted to the local disk. + +5. Various fields are located in the `status` section (a sub-sub-object of the + user record, also with an intermediary object between that is keyed by the + machine ID, similar to the way the `binding` section is organized). This + section is augmented during runtime only, and never persisted to disk. The + idea is that this section contains information about current runtime + resource usage (for example: currently used disk space of the user), that + changes dynamically but is otherwise immediately associated with the user + record and for many purposes should be considered to be part of the user + record. + +6. The `signature` section contains one or more cryptographic signatures of a + reduced version of the user record. This is used to ensure that only user + records defined by a specific source are accepted on a system, by validating + the signature against the set of locally accepted signature public keys. The + signature is calculated from the JSON user record with all sections removed, + except for `regular`, `privileged`, `perMachine`. Specifically, `binding`, + `status`, `signature` itself and `secret` are removed first and thus not + covered by the signature. This section is optional, and is only used when + cryptographic validation of user records is required (as it is by + `systemd-homed.service` for example). + +7. The `secret` section contains secret user credentials, such as password or + PIN information. This data is never persisted, and never returned when user + records are inquired by a client, privileged or not. This data should only + be included in a user record very briefly, for example when certain very + specific operations are executed. For example, in tools such as + `systemd-homed` this section may be included in user records, when creating + a new home directory, as passwords and similar credentials need to be + provided to encrypt the home directory with. + +Here's a tabular overview of the sections and their properties: + +| Section | Included in Signature | Persisted | Security Sensitive | Contains Host-Specific Data | +|------------|-----------------------|-----------|--------------------|-----------------------------| +| regular | yes | yes | no | no | +| privileged | yes | yes | yes | no | +| perMachine | yes | yes | no | yes | +| binding | no | yes | no | yes | +| status | no | no | no | yes | +| signature | no | yes | no | no | +| secret | no | no | yes | no | + +## Fields in the `regular` section + +As mentioned, the `regular` section's fields are placed at the top level +object. The following fields are currently defined: + +`userName` → The UNIX user name for this record. Takes a string with a valid +UNIX user name. This field is the only mandatory field, all others are +optional. Corresponds with the `pw_name` field of of `struct passwd` and the +`sp_namp` field of `struct spwd` (i.e. the shadow user record stored in +`/etc/shadow`). + +`realm` → The "realm" a user is defined in. This concept allows distinguishing +users with the same name that originate in different organizations or +installations. This should take a string in DNS domain syntax, but doesn't have +to refer to an actual DNS domain (though it is recommended to use one for +this). The idea is that the user `lpoetter` in the `redhat.com` realm might be +distinct from the same user in the `poettering.hq` realm. User records for the +same user name that have different realm fields are considered referring to +different users. When updating a user record it is required that any new +version has to match in both `userName` and `realm` field. This field is +optional, when unset the user should not be considered part of any realm. A +user record with a realm set is never compatible (for the purpose of updates, +see above) with a user record without one set, even if the `userName` field matches. + +`realName` → The real name of the user, a string. This should contain the user's +real ("human") name, and corresponds loosely to the GECOS field of classic UNIX +user records. When converting a `struct passwd` to a JSON user record this +field is initialized from GECOS (i.e. the `pw_gecos` field), and vice versa +when converting back. That said, unlike GECOS this field is supposed to contain +only the real name and no other information. + +`emailAddress` → The email address of the user, formatted as +string. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +initializes the `$EMAIL` environment variable from this value for all login +sessions. + +`iconName` → The name of an icon picked by the user, for example for the +purpose of an avatar. This must be a string, and should follow the semantics +defined in the [Icon Naming +Specification](https://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html). + +`location` → A free-form location string describing the location of the user, +if that is applicable. It's probably wise to use a location string processable +by geo-location subsystems, but this is not enforced nor required. Example: +`Berlin, Germany` or `Basement, Room 3a`. + +`disposition` → A string, one of `intrinsic`, `system`, `dynamic`, `regular`, +`container`, `reserved`. If specified clarifies the disposition of the user, +i.e. the context it is defined in. For regular, "human" users this should be +`regular`, for system users (i.e. users that system services run under, and +similar) this should be `system`. The `intrinsic` disposition should be used +only for the two users that have special meaning to the OS kernel itself, +i.e. the `root` and `nobody` users. The `container` string should be used for +users that are used by an OS container, and hence will show up in `ps` listings +and such, but are only defined in container context. Finally `reserved` should +be used for any users outside of these use-cases. Note that this property is +entirely optional and applications are assumed to be able to derive the +disposition of a user automatically from a record even in absence of this +field, based on other fields, for example the numeric UID. By setting this +field explicitly applications can override this default determination. + +`lastChangeUSec` → An unsigned 64bit integer value, referring to a timestamp in µs +since the epoch 1970, indicating when the user record (specifically, any of the +`regular`, `privileged`, `perMachine` sections) was last changed. This field is +used when comparing two records of the same user to identify the newer one, and +is used for example for automatic updating of user records, where appropriate. + +`lastPasswordChangeUSec` → Similar, also an unsigned 64bit integer value, +indicating the point in time the password (or any authentication token) of the +user was last changed. This corresponds to the `sp_lstchg` field of `struct +spwd`, i.e. the matching field in the user shadow database `/etc/shadow`, +though provides finer resolution. + +`shell` → A string, referring to the shell binary to use for terminal logins of +this user. This corresponds with the `pw_shell` field of `struct passwd`, and +should contain an absolute file system path. For system users not suitable for +terminal log-in this field should not be set. + +`umask` → The `umask` to set for the user's login sessions. Takes an +integer. Note that usually the umask is noted in octal, but JSON's integers are +generally written in decimal, hence that's what this is too. The specified +value should be in the valid range for umasks, i.e. 0000…0777 (in octal), or +0…511 (in decimal). This `umask` is automatically set by +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +for all login sessions of the user. + +`environment` → An array of strings, each containing an environment variable +and its value to set for the user's login session, in a format compatible with +[`putenv()`](http://man7.org/linux/man-pages/man3/putenv.3.html). Any +environment variable listed here is automatically set by +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +for all login sessions of the user. + +`timeZone` → A string indicating a preferred timezone to use for the user. When +logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialized the `$TZ` environment variable from this +string. The string hence should be in a format compatible with this environment +variable, for example: `Europe/Berlin`. + +`preferredLanguage` → A string indicating the preferred language/locale for the +user. When logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialized the `$LANG` environment variable from this +string. The string hence should be in a format compatible with this environment +variable, for example: `de_DE.UTF8`. + +`niceLevel` → An integer value in the range -20…19. When logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialize the login process' nice level to this value with +this, which is then inherited by all the user's processes, see +[`setpriority()`](http://man7.org/linux/man-pages/man2/setpriority.2.html) for +more information. + +`resourceLimits` → An object, where each key refers to a Linux resource limit +(such as `RLIMIT_NOFILE` and similar). Their values should be an object with +two keys `cur` and `max` for the soft and hard resource limit. When logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialize the login process' resource limits to these +values, which is then inherited by all the user's processes, see +[`setrlimit()`](http://man7.org/linux/man-pages/man2/setrlimit.2.html) for more +information. + +`locked` → A boolean value. If true the user account is locked, the user may +not log in. If this field is missing it should be assumed to be false, +i.e. logins are permitted. This field corresponds with the `sp_expire` field of +`struct spwd` (i.e. the `/etc/shadow` data for a user) being set to zero or +one. + +`notBeforeUSec` → An unsigned 64bit integer value, indicating a time in µs since +the UNIX epoch (1970) before which the record should be considered invalid for +the purpose of logging in. + +`notAfterUSec` → Similar, but indicates the point in time *after* which logins +shall not be permitted anymore. This corresponds to the `sp_expire` field of +`struct spwd`, when it is set to a value larger than one, but provides finer +granularity. + +`storage` → A string, one of `classic`, `luks`, `directory`, `subvolume`, +`fscrypt`, `cifs`. Indicates the storage mechanism for the user's home +directory. If `classic` the home directory is a plain directory as in classic +UNIX. When `directory`, the home directory is a regular directory, but the +`~.identity` file in it contains the user's user record, so that the directory +is self-contained. Similar, `subvolume` is a `btrfs` subvolume that also +contains a `~/.identity` use record; `fscrypt` is an `fscrypt`-encrypted +directory, also containing the `~/.identity` user record; `luks` is a per-user +LUKS volume that is mounted as home directory, and `cifs` a home directory +mounted from a Windows File Share. The five latter types are primarily used by +`systemd-homed` when managing home directories, but may be used if other +managers are used too. If this is not set `classic` is the implied default. + +`diskSize` → An unsigned 64bit integer, indicating the intended home directory +disk space in bytes to assign to the user. Depending to the selected storage +type this might be implement differently: for `luks` this is the intended size +of the file system and LUKS volume, while for the others this likely translates +to classic file system quota settings. + +`diskSizeRelative` → Similar to `diskSize` but takes a relative value, which is +taken relative to the available disk space on the selected storage medium. This +unsigned integer value is normalized to 2^32 = 100%. + +`skeletonDirectory` → Takes a string with the absolute path to the skeleton +directory to populate a new home directory from. This is only used when a home +directory is first created, and defaults to `/etc/skel` if not defined. + +`accessMode` → Takes an unsigned integer in the range 0…511 indicating the UNIX +access mask for the home directory when it is first created. + +`tasksMax` → Takes an unsigned 64bit integer indicating the maximum number of +tasks the user may start in parallel during system runtime. This value is +enforced on all tasks (i.e. processes and threads) the user starts or that are +forked off these processes regardless if the change user identity (for example +by setuid binaries/`su`/`sudo` and +similar). [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html) +enforces this by setting the `TasksMax` slice property for the user's slice +`user-$UID.slice`. + +`memoryHigh`/`memoryMax` → These take unsigned 64bit integers indicating upper +memory limits for all processes of the user (plus all processes forked off them +that might have changed user identity), in bytes. Enforced by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html), +similar to `tasksMax`. + +`cpuWeight`/`ioWeight` → These take unsigned integers in the range 100…10000 +and configure the CPU and IO scheduling weights for the user's processes as a +whole. Also enforced by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html), +similar to `tasksMax`, `memoryHigh` and `memoryMax`. + +`mountNoDevices`/`mountNoSUID`/`mountNoExecute` → Three booleans that control +the `nodev`, `nosuid`, `noexec` mount flags of the user's home +directories. Note that these booleans are only honored if the home directory +is managed by a subsystem such as `systemd-homed.service` that automatically +mounts home directories on login. + +`fscryptSalt` → A string containing base64 encoded data. Selects the the salt +bytes to use for the PBKDF2 key derivation function when using `fscrypt` as +storage mechanism for the home directory. + +`cifsDomain` → A string indicating the Windows File Sharing domain (CIFS) to +use. This is generally useful, but particularly when `cifs` is used as storage +mechanism for the user's home directory, see above. + +`cifsUserName` → A string indicating the Windows File Sharing user name (CIFS) +to associate this user record with. This is generally useful, but particularly +useful when `cifs` is used as storage mechanism for the user's home directory, +see above. + +`cifsService` → A string indicating the Windows File Share service (CIFS) to +mount as home directory of the user on login. + +`imagePath` → A string with an absolute file system path to the file, directory +or block device to use for storage backing the home directory. If the `luks` +storage is used this refers to the loopback file or block device node to store +the LUKS volume on. For `fscrypt`, `directory`, `subvolume` this refers to the +directory to bind mount as home directory on login. Not defined for `classic` +or `cifs`. + +`homeDirectory` → A string with an absolute file system path to the home +directory. This is where the image indicated in `imagePath` is mounted to on +login and thus indicates the application facing home directory while the home +directory is active, and is what the user's `$HOME` environment variable is set +to during log-in. It corresponds to the `pw_dir` field of `struct passwd`. + +`uid` → An unsigned integer in the range 0…4294967295: the numeric UNIX user ID (UID) to +use for the user. This corresponds to the `pw_uid` field of `struct passwd`. + +`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group +ID (GID) to use for the user. This corresponds to the `pw_gid` field of +`struct passwd`. + +`memberOf` → An array of strings, each indicating a UNIX group this user shall +be a member of. The listed strings must be valid group names, but it is not +required that all groups listed exist in all contexts: any entry for which no +group exists should be silently ignored. + +`fileSystemType` → A string, one of `ext4`, `xfs`, `btrfs` (possibly others) to +use as file system for the user's home directory. This is primarily relevant +when the storage mechanism used is `luks` as a file system to use inside the +LUKS container must be selected. + +`partitionUUID` → A string containing a lower-case, text-formatted UUID, referencing +the GPT partition UUID the home directory is located in. This is primarily +relevant when the storage mechanism used is `luks`. + +`luksUUID` → A string containing a lower-case, text-formatted UUID, referencing +the LUKS volume UUID the home directory is located in. This is primarily +relevant when the storage mechanism used is `luks`. + +`fileSystemUUID` → A string containing a lower-case, text-formatted UUID, +referencing the file system UUID the home directory is located in. This is +primarily relevant when the storage mechanism used is `luks`. + +`luksDiscard` → A boolean. If true and `luks` storage is used controls whether +the loopback block devices, LUKS and the file system on top shall be used in +`discard` mode, i.e. erased sectors should always be returned to the underlying +storage. If false and `luks` storage is used turns this behavior off. In +addition, depending on this setting an `FITRIM` or `fallocate()` operation is +executed to make sure the image matches the selected option. + +`luksCipher` → A string, indicating the cipher to use for the LUKS storage mechanism. + +`luksCipherMode` → A string, selecting the cipher mode to use for the LUKS storage mechanism. + +`luksVolumeKeySize` → An unsigned integer, indicating the volume key length in +bytes to use for the LUKS storage mechanism. + +`luksPbkdfHashAlgorithm` → A string, selecting the hash algorithm to use for +the PBKDF operation for the LUKS storage mechanism. + +`luksPbkdfType` → A string, indicating the PBKDF type to use for the LUKS storage mechanism. + +`luksPbkdfTimeCostUSec` → An unsigned 64bit integer, indicating the intended +time cost for the PBKDF operation, when the LUKS storage mechanism is used, in +µs. + +`luksPbkdfMemoryCost` → An unsigned 64bit integer, indicating the intended +memory cost for the PBKDF operation, when LUKS storage is used, in bytes. + +`luksPbkdfParallelThreads` → An unsigned 64bit integer, indicating the intended +required parallel threads for the PBKDF operation, when LUKS storage is used. + +`service` → A string declaring the service that defines or manages this user +record. It is recommended to use reverse domain name notation for this. For +example, if `systemd-homed` manages a user a string of `io.systemd.Home` is +used for this. + +`rateLimitIntervalUSec` → An unsigned 64bit integer that configures the +authentication rate limiting enforced on the user account. This specifies a +timer interval (in µs) within which to count authentication attempts. When the +counter goes above the value configured n `rateLimitIntervalBurst` log-ins are +temporarily refused until the interval passes. + +`rateLimitIntervalBurst` → An unsigned 64bit integer, closely related to +`rateLimitIntervalUSec`, that puts a limit on authentication attempts within +the configured time interval. + +`enforcePasswordPolicy` → A boolean. Configures whether to enforce the system's +password policy when creating the home directory for the user or changing the +user's password. By default the policy is enforced, but if this field is false +it is bypassed. + +`autoLogin` → A boolean. If true the user record is marked as suitable for +auto-login. Systems are supposed to automatically log in a user marked this way +during boot, if there's exactly one user on it defined this way. + +`stopDelayUSec` → An unsigned 64bit integer, indicating the time in µs the +per-user service manager is kept around after the user fully logged out. This +value is honored by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If +set to zero the per-user service manager is immediately terminated when the +user logs out, and longer values optimize high-frequency log-ins as the +necessary work to set up and tear down a log-in is reduced if the service +manager stays running. + +`killProcesses` → A boolean. If true all processes of the user are +automatically killed when the user logs out. This is enforced by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If +false any processes left around when the user logs out are left running. + +`passwordChangeMinUSec`/`passwordChangeMaxUSec` → An unsigned 64bit integer, +encoding how much time has to pass at least/at most between password changes of +the user. This corresponds with the `sp_min` and `sp_max` fields of `struct +spwd` (i.e. the `/etc/shadow` entries of the user), but offers finer +granularity. + +`passwordChangeWarnUSec` → An unsigned 64bit integer, encoding how much time to +warn the user before their password expires, in µs. This corresponds with the +`sp_warn` field of `struct spwd`. + +`passwordChangeInactiveUSec` → An unsigned 64bit integer, encoding how much +time has to pass after the password expired that the account is +deactivated. This corresponds with the `sp_inact` field of `struct spwd`. + +`passwordChangeNow` → A boolean. If true the user has to change their password +on next login. This corresponds with the `sp_lstchg` field of `struct spwd` +being set to zero. + +`privileged` → An object, which contains the fields of he `privileged` section +of the user record, see below. + +`perMachine` → An array of objects, which contain the `perMachine` section of +the user record, and thus fields to apply on specific systems only, see below. + +`binding` → An object, keyed by machine IDs formatted as strings, pointing +to objects that contain the `binding` section of the user record, +i.e. additional fields that bind the user record to a specific machine, see +below. + +`status` → An object, keyed by machine IDs formatted as strings, pointing to +objects that contain the `status` section of the user record, i.e. additional +runtime fields that expose the current status of the user record on a specific +system, see below. + +`signature` → An array of objects, which contain cryptographic signatures of +the user record, i.e. the fields of the `signature` section of the user record, +see below. + +`secret` → An object, which contains the fields of the `secret` section of the +user record, see below. + +## Fields in the `privileged` section + +As mentioned, the `privileged` section is encoded in a sub-object of the user +record top-level object, in the `privileged` field. Any data included in this +object shall only be visible to the administrator and the user themselves, and +be suppressed implicitly when other users get access to a user record. It thus +takes the role of the `/etc/shadow` records for each user, which has similarly +restrictive access semantics. The following fields are currently defined: + +`passwordHint` → A user-selected password hint in free-form text. This should +be a string like "What's the name of your first pet?", but is entirely for the +user to choose. + +`hashPassword` → An array of strings, each containing a hashed UNIX password +string, in the format +[`crypt(3)`](http://man7.org/linux/man-pages/man3/crypt.3.html) generates. This +corresponds with `sp_pwdp` field of `struct spwd` (and in a way the `pw_passwd` +field of `struct passwd`). + +`sshAuthorizedKeys` → An array of strings, each listing an SSH public key that +is authorized to access the account. The strings should follow the same format +as the lines in the traditional `~./.ssh/authorized_key` file. + +## Fields in the `perMachine` section + +As mentioned, the `perMachine` section contains settings that shall apply to +specific systems only. This is primarily interesting for resource management +properties as they tend to require a per-system focus, however they may be used +for other purposes too. + +The `perMachine` field in the top-level object is an array of objects. When +processing the user record first the various fields on the top-level object +should be used. Then this array should be iterated in order, and the various +settings be applied that match either the indicated machine ID or host +name. There may be multiple array entries that match a specific system, in +which case all the object's setting should be applied. If the same option is +set in the top-level object as in a per-machine object the latter wins and +entirely undoes the setting in the top-level object (i.e. no merging of +properties that are arrays themselves is done). If the same option is set in +multiple per-machine objects the one specified later in the array wins (and +here too no merging of individual fields is done, the later field always wins +in full). + +The following fields are defined in this section: + +`matchMachineId` → An array of strings with each a formatted 128bit ID in +hex. If any of the specified IDs match the system's local machine ID +(i.e. matches `/etc/machine-id`) the fields in this object are honored. + +`matchHostname` → An array of string with a each a valid hostname. If any of +the specified hostnames match the system's local hostname, the fields in this +object are honored. If both `matchHostname` and `matchMachineId` are used +within the same array entry, the object is honored when either match succeeds, +i.e. the two match types are combined in OR, not in AND. + +These two are the only two fields specific to this section. All other fields +that may be used in this section are identical to the equally named ones in the +`regular` section (i.e. at the top-level object). Specifically, these are: + +`iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`, +`preferredLanguage`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`, +`notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`, +`accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`, +`mountNoDevices`, `mountNoSUID`, `mountNoExecute`, `fscryptSalt`, `cifsDomain`, +`cifsUserName`, `cifsService`, `imagePath`, `uid`, `gid`, `memberOf`, +`fileSystemType`, `partitionUUID`, `luksUUID`, `fileSystemUUID`, `luksDiscard`, +`luksCipher`, `luksCipherMode`, `luksVolumeKeySize`, `luksPbkdfHashAlgorithm`, +`luksPbkdfType`, `luksPbkdfTimeCostUSec`, `luksPbkdfMemoryCost`, +`luksPbkdfParallelThreads`, `rateLimitIntervalUSec`, `rateLimitBurst`, +`enforcePasswordPolicy`, `autoLogin`, `stopDelayUSec`, `killProcesses`, +`passwordChangeMinUSec`, `passwordChangeMaxUSec`, `passwordChangeWarnUSec`, +`passwordChangeInactiveUSec`, `passwordChangeNow`. + +## Fields in the `binding` section + +As mentioned, the `binding` section contains additional fields about the user +record, that bind it to the local system. These fields are generally used by a +local user manager (such as `systemd-homed.service`) to add in fields that make +sense in a local context but not necessarily in a global one. For example, a +user record that contains no `uid` field in the regular section is likely +extended with one in the `binding` section to assign a local UID if no global +UID is defined. + +All fields in the `binding` section only make sense in a local context and are +suppressed when the use record is ported between systems. The `binding` section +is generally persisted on the system but not in the home directories themselves +and the home directory is supposed to be fully portable and thus not contain +the information that `binding` is supposed to contain that binds the portable +record to a specific system. + +The `binding` sub-object on the top-level user record object is keyed by the +machine ID the binding is intended for, which point to an object with the +fields of the bindings. These fields generally match fields that may also be +defined in the `regular` and `perMachine` sections, however override +both. Usually, the `binding` value should not contain settings different from +those set via `regular` or `perMachine`, however this might happen if some +settings are not supported locally (think: `fscrypt` is recorded as intended +storage mechanism in the `regular` section, but the local kernel does not +support `fscrypt`, hence `directory` was chosen as implicit fallback), or have +been changed in the `regular` section through updates (e.g. a home directory +was created with `luks` as storage mechanism but later the user record was +updated to prefer `subvolume`, which however doesn't change the actual storage +used already which is pinned in the `binding` section). + +The following fields are defined in the `binding` section. They all have an +identical format and override their equally named counterparts in the `regular` +and `perMachine` sections: + +`imagePath`, `homeDirectory`, `partitionUUID`, `luksUUID`, `fileSystemUUID`, +`uid`, `gid`, `storage`, `fileSystemType`, `fscryptSalt`, `luksCipher`, +`luksCipherMode`, `luksVolumeKeySize`. + +## Fields in the `status` section + +As mentioned, the `status` section contains additional fields about the user +record that are exclusively acquired during runtime, and that expose runtime +metrics of the user and similar metadata that shall not be persisted but are +only acquired "on-the-fly" when requested. + +This section is arranged similarly to the `binding` section: the `status` +sub-object of the top-level user record object is keyed by the machine ID, +which points to the object with the fields defined here. The following fields +are defined: + +`diskUsage` → An unsigned 64bit integer. The currently used disk space of the +home directory in bytes. This value might be determined in different ways, +depending on the selected storage mechanism. For LUKS storage this is the file +size of the loopback file or block device size. For the +directory/subvolume/fscrypt storage this is the current disk space used as +reported by the file system quota subsystem. + +`diskFree` → An unsigned 64bit integer, denoting the number of "free" bytes in +the disk space allotment, i.e. usually the difference between the disk size as +reported by `diskSize` and the used already as reported in `diskFree`, but +possibly skewed by metadata sizes, disk compression and similar. + +`diskSize` → An unsigned 64bit integer, denoting the disk space currently +allotted to the user, in bytes. Depending on the storage mechanism this can mean +different things (see above). In contrast to the top-level field of the same +(or the one in the `perMachine` section), this field reports the current size +allotted to the user, not the intended one. The values may differ when user +records are updated without the home directory being re-sized. + +`diskCeiling`/`diskFloor` → Unsigned 64bit integers indicating upper and lower +bounds when changing the `diskSize` value, in bytes. These values are typically +derived from the underlying data storage, and indicate in which range the home +directory may be re-sized in, i.e. in which sensible range the `diskSize` value +should be kept. + +`state` → A string indicating the current state of the home directory. The +precise set of values exposed here are up to the service managing the home +directory to define (i.e. are up to the service identified with the `service` +field below). However, it is recommended to stick to a basic vocabulary here: +`inactive` for a home directory currently not mounted, `absent` for a home +directory that cannot be mounted currently because it does not exist on the +local system, `active` for a home directory that is currently mounted and +accessible. + +`service` → A string identifying the service that manages this user record. For +example `systemd-homed.service` sets this to `io.systemd.Home` to all user +records it manages. This is particularly relevant to define clearly the context +in which `state` lives, see above. Note that this field also exists on the +top-level object (i.e. in the `regular` section), which it overrides. The +`regular` field should be used if conceptually the user record can only be +managed by the specified service, and this `status` field if it can +conceptually be managed by different managers, but currently is managed by the +specified one. + +`signedLocally` → A boolean. If true indicates that the user record is signed +by a public key for which the private key is available locally. This indicates +that indicates that the user record may be modified locally as it can be +re-signed with the private key. If false indicates that the user record is +signed by a public key recognized by the local manager but whose private key is +not available locally. This means the user record cannot be modified locally as +it couldn't be signed afterwards. + +`goodAuthenticationCounter` → An unsigned 64bit integer. This counter is +increased by one on every successful authentication attempt, i.e. an +authentication attempt where a security token of some form was presented and it +was correct. + +`badAuthenticationCounter` → An unsigned 64bit integer. This counter is +increased by one on every unsuccessfully authentication attempt, i.e. an +authentication attempt where a security token of some form was presented and it +was incorrect. + +`lastGoodAuthenticationUSec` → An unsigned 64bit integer, indicating the time +of the last successful authentication attempt in µs since the UNIX epoch (1970). + +`lastBadAuthenticationUSec` → Similar, but the timestamp of the last +unsuccessfully authentication attempt. + +`rateLimitBeginUSec` → An unsigned 64bit integer: the µs timestamp since the +UNIX epoch (1970) where the most recent rate limiting interval has been +started, as configured with `rateLimitIntervalUSec`. + +`rateLimitCount` → An unsigned 64bit integer, counting the authentication +attempts in the current rate limiting interval, see above. If this counter +grows beyond the value configured in `rateLimitBurst` authentication attempts +are temporarily refused. + +`removable` → A boolean value. If true the manager of this user record +determined the home directory being on removable media. If false it was +determined the home directory is in internal built-in media. (This is used by +`systemd-logind.service` to automatically pick the right default value for +`stopDelayUSec` if the field is not explicitly specified: for home directories +on removable media the delay is selected very low to minimize the chance the +home directory remains in unclean state if the storage device is removed from +the system by the user). + +## Fields in the `signature` section + +As mentioned, the `signature` section of the user record may contain one or +more cryptographic signatures of the user record. Like all others, this section +is optional, and only used when cryptographic validation of user records shall +be used. Specifically, all user records managed by `systemd-homed.service` will +carry such signatures and the service refuses managing user records that come +without signature or with signatures not recognized by any locally defined +public key. + +The `signature` field in the top-level user record object is an array of +objects. Each object encapsulates one signature and has two fields: `data` and +`key` (both are strings). The `data` field contains the actual signature, +encoded in base64, the `key` field contains a copy of the public key whose +private key was used to make the signature, in PEM format. Currently only +signatures with Ed25519 keys are defined. + +Before signing the user record should be brought into "normalized" form, +i.e. the keys in all objects should be sorted alphabetically. All redundant +white-space and newlines should be removed and the JSON text then signed. + +The signatures only cover the `regular`, `perMachine` and `privileged` sections +of the user records, all other sections (include `signature` itself), are +removed before the signature is calculated. + +## Fields in the `secret` section + +As mentioned, the `secret` section of the user record should never be persisted +nor transferred across machines. It is only defined in short-lived operations, +for example when a user record is first created or registered, as the secret +key data needs to be available to derive encryption keys from and similar. + +The `secret` field of the top-level user record contains the following fields: + +`password` → an array of strings, each containing a plain text password. + +## Mapping to `struct passwd` and `struct spwd` + +When mapping classic UNIX user records (i.e. `struct passwd` and `struct spwd`) +to JSON user records the following mappings should be applied: + +| Structure | Field | Section | Field | Condition | +|-----------------|-------------|--------------|------------------------------|----------------------------| +| `struct passwd` | `pw_name` | `regular` | `userName` | | +| `struct passwd` | `pw_passwd` | `privileged` | `password` | (See notes below) | +| `struct passwd` | `pw_uid` | `regular` | `uid` | | +| `struct passwd` | `pw_gid` | `regular` | `gid` | | +| `struct passwd` | `pw_gecos` | `regular` | `realName` | | +| `struct passwd` | `pw_dir` | `regular` | `homeDirectory` | | +| `struct passwd` | `pw_shell` | `regular` | `shell` | | +| `struct spwd` | `sp_namp` | `regular` | `userName` | | +| `struct spwd` | `sp_pwdp` | `privileged` | `password` | (See notes below) | +| `struct spwd` | `sp_lstchg` | `regular` | `lastPasswordChangeUSec` | (if `sp_lstchg` > 0) | +| `struct spwd` | `sp_lstchg` | `regular` | `passwordChangeNow` | (if `sp_lstchg` == 0) | +| `struct spwd` | `sp_min` | `regular` | `passwordChangeMinUSec` | | +| `struct spwd` | `sp_max` | `regular` | `passwordChangeMaxUSec` | | +| `struct spwd` | `sp_warn` | `regular` | `passwordChangeWarnUSec` | | +| `struct spwd` | `sp_inact` | `regular` | `passwordChangeInactiveUSec` | | +| `struct spwd` | `sp_expire` | `regular` | `locked` | (if `sp_expire` in [0, 1]) | +| `struct spwd` | `sp_expire` | `regular` | `notAfterUSec` | (if `sp_expire` > 1) | + +At this time almost all Linux machines employ shadow passwords, thus the +`pw_passwd` field in `struct passwd` is set to `"x"`, and the actual password +is stored in the shadow entry `struct spwd`'s field `sp_pwdp`. + +## Extending These Records + +User records following this specifications are supposed to be extendable for +various applications. In general, subsystems are free to introduce their own +keys, as long as: + +* Care should be taken to place the keys in the right section, i.e. the most + appropriate for the data field. + +* Care should be taken to avoid namespace clashes. Please prefix your fields + with a short identifier of your project to avoid ambiguities and + incompatibilities. + +* This specification is supposed to be a living specification. If you need + additional fields, please consider submitting them upstream for inclusion in + this specification. If they are reasonably universally useful, it would be + best to list them here. + +## Examples + +The shortest valid user record looks like this: + +```json +{ + "userName" : "u" +} +``` + +A reasonable user record for a system user might look like this: + +```json +{ + "userName" : "httpd", + "uid" : 473, + "gid" : 473, + "disposition" : "system", + "locked" : true +} +``` + +A fully featured user record associated with a home directory managed by +`systemd-homed.service` might look like this: + +```json +{ + "autoLogin" : true, + "binding" : { + "15e19cf24e004b949ddaac60c74aa165" : { + "fileSystemType" : "ext4", + "fileSystemUUID" : "758e88c8-5851-4a2a-b88f-e7474279c111", + "gid" : 60232, + "homeDirectory" : "/home/grobie", + "imagePath" : "/home/grobie.home", + "luksCipher" : "aes", + "luksCipherMode" : "xts-plain64", + "luksUUID" : "e63581ba-79fb-4226-b9de-1888393f7573", + "luksVolumeKeySize" : 32, + "partitionUUID" : "41f9ce04-c827-4b74-a981-c669f93eb4dc", + "storage" : "luks", + "uid" : 60232 + } + }, + "disposition" : "regular", + "enforcePasswordPolicy" : false, + "lastChangeUSec" : 1565950024279735, + "memberOf" : [ + "wheel" + ], + "privileged" : { + "hashedPassword" : [ + "$6$WHBKvAFFT9jKPA4k$OPY4D4TczKN/jOnJzy54DDuOOagCcvxxybrwMbe1SVdm.Bbr.zOmBdATp.QrwZmvqyr8/SafbbQu.QZ2rRvDs/" + ] + }, + "signature" : [ + { + "data" : "LU/HeVrPZSzi3MJ0PVHwD5m/xf51XDYCrSpbDRNBdtF4fDVhrN0t2I2OqH/1yXiBidXlV0ptMuQVq8KVICdEDw==", + "key" : "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA/QT6kQWOAMhDJf56jBmszEQQpJHqDsGDMZOdiptBgRk=\n-----END PUBLIC KEY-----\n" + } + ], + "userName" : "grobie", + "status" : { + "15e19cf24e004b949ddaac60c74aa165" : { + "goodAuthenticationCounter" : 16, + "lastGoodAuthenticationUSec" : 1566309343044322, + "rateLimitBeginUSec" : 1566309342340723, + "rateLimitCount" : 1, + "state" : "inactive", + "service" : "io.systemd.Home", + "diskSize" : 161118667776, + "diskCeiling" : 190371729408, + "diskFloor" : 5242880, + "signedLocally" : true + } + } +} +``` From dbdf681bcc7fe17c048acaea94f5c0e2b8344569 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 27 Aug 2019 19:45:41 +0200 Subject: [PATCH 106/109] docs: add documentation for JSON group records, too --- docs/GROUP_RECORD.md | 156 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/GROUP_RECORD.md diff --git a/docs/GROUP_RECORD.md b/docs/GROUP_RECORD.md new file mode 100644 index 0000000000000..55cb2f064974a --- /dev/null +++ b/docs/GROUP_RECORD.md @@ -0,0 +1,156 @@ +--- +title: JSON Group Records +--- + +# JSON Group Records + +Long story short: JSON Group Records are to `struct group` what [JSON User +Records](https://systemd.io/USER_RECORD.md) are to `struct passwd`. + +Conceptually, much of what applies to JSON user records also applies to JSON +group records. They also consist of seven sections, with similar properties and +they carry some identical (or at least very similar) fields. + +## Fields in the `regular` section + +`groupName` → A string with the UNIX group name. Matches the `gr_name` field of +UNIX/glibc NSS `struct group`, or the shadow structure `struct sgrp`'s +`sg_namp` field. + +`realm` → The "realm" the group belongs to, conceptually identical to the same +field of user records. A string in DNS domain name syntax. + +`disposition` → The disposition of the group, conceptually identical to the +same field of user records. A string. + +`service` → A string, an identifier for the service managing this group record +(this field is typically in reverse domain name syntax.) + +`lastChangeUSec` → An unsigned 64bit integer, a timestamp (in µs since the UNIX +epoch 1970) of the last time the group record has been modified. (Covers only +the `regular`, `perMachine` and `privileged` sections). + +`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group +ID (GID) to use for the group. This corresponds to the `gr_gid` field of +`struct group`. + +`members` → An array of strings, listing user names that are members of this +group. Note that JSON user records also contain a `memberOf` field, or in other +words a group membership can either be denoted in the JSON user record or in +the JSON record, or in both. The list of memberships should be determined as +the combination of both lists (plus optionally others). If a user is listed as +member of a group and doesn't exist it should be ignored. This field +corresponds to the `gr_mem` field of `struct group` and the `sg_mem` field of +`struct sgrp`. + +`administrators` → Similarly, an array of strings, listing user names that +shall be considered "administrators" of this group. This field corresponds to +the `sg_adm` field of `struct sgrp`. + +`privileged`/`perMachine`/`binding`/`status`/`signature`/`secret` → The +objects/arrays for the other six group record sections. These are organized the +same way as for the JSON user records, and have the same semantics. + +## Fields in the `privileged` section + +The following fields are defined: + +`hashedPassword` → An array of strings with UNIX hashed passwords; see the +matching field for user records for details. This field corresponds to the +`sg_passwd` field of `struct sgrp` (and `gr_passwd` of `struct group` in a +way). + +## Fields in the `perMachine` section + +`matchMachineId`/`matchHostname` → Strings, match expressions similar as for +user records, see the user record documentation for details. + +The following fields are defined for the `perMachine` section and are defined +equivalent to the fields of the same name in the `regular` section, and +override those: + +`gid`, `members`, `administrators` + +## Fields in the `binding` section + +The following fields are defined for the `binding` section, and are equivalent +to the fields of the same name in the `regular` and `perMachine` sections: + +`gid` + +## Fields in the `status` section + +The following fields are defined in the `status` section, and are mostly +equivalent to the fields of the same name in the `regular` section, though with +slightly different conceptual semantics, see the same fields in the user record +documentation: + +`service` + +## Fields in the `signature` section + +The fields in this section are defined identically to those in the matching +section in the user record. + +## Fields in the `secret` section + +Currently no fields are defined in this section for group records. + +## Mapping to `struct group` and `struct sgrp` + +When mapping classic UNIX group records (i.e. `struct group` and `struct sgrp`) +to JSON group records the following mappings should be applied: + +| Structure | Field | Section | Field | Condition | +|----------------|-------------|--------------|------------------|----------------------------| +| `struct group` | `gr_name` | `regular` | `groupName` | | +| `struct group` | `gr_passwd` | `privileged` | `password` | (See notes below) | +| `struct group` | `gr_gid` | `regular` | `gid` | | +| `struct group` | `gr_mem` | `regular` | `members` | | +| `struct sgrp` | `sg_namp` | `regular` | `groupName` | | +| `struct sgrp` | `sg_passwd` | `privileged` | `password` | (See notes below) | +| `struct sgrp` | `sg_adm` | `regular` | `administrators` | | +| `struct sgrp` | `sg_mem` | `regular` | `members` | | + +At this time almost all Linux machines employ shadow passwords, thus the +`gr_passwd` field in `struct group` is set to `"x"`, and the actual password +is stored in the shadow entry `struct sgrp`'s field `sg_passwd`. + +## Extending These Records + +The same logic and recommendations apply as for JSON user records + +## Examples + +A reasonable group record for a system group might look like this: + +```json +{ + "groupName" : "systemd-resolve", + "gid" : 193, + "status" : { + "6b18704270e94aa896b003b4340978f1" : { + "service" : "io.systemd.NameServiceSwitch" + } + } +} +``` + +And here's a more complete one for a regular group: + +```json +{ + "groupName" : "grobie", + "binding" : { + "6b18704270e94aa896b003b4340978f1" : { + "gid" : 60232 + } + }, + "disposition" : "regular", + "status" : { + "6b18704270e94aa896b003b4340978f1" : { + "service" : "io.systemd.Home" + } + } +} +``` From c2004b310a42d45282d38e9a66998e23c6026181 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 28 Aug 2019 19:36:01 +0200 Subject: [PATCH 107/109] docs: add documentation for the varlink user/group APIs --- docs/USER_GROUP_API.md | 235 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/USER_GROUP_API.md diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md new file mode 100644 index 0000000000000..b9ac9c02c0c92 --- /dev/null +++ b/docs/USER_GROUP_API.md @@ -0,0 +1,235 @@ +--- +title: Varlink User/Group Lookup API +--- + +# Varlink User/Group Lookup API + +JSON User/Group Records (as described in the [JSON User +Records](https://systemd.io/USER_RECORD) and [JSON Group +Records](https://systemd.io/GROUP_RECORD) documents) that are defined on the +local system may be queried with a [Varlink](https://varlink.org/) API. This +API takes both the role of what +[`getpwnam(3)`](http://man7.org/linux/man-pages/man3/getpwnam.3.html) and +related calls are for `struct passwd`, as well as the interfaces modules +implementing the [glibc Name Service Switch +(NSS)](https://www.gnu.org/software/libc/manual/html_node/Name-Service-Switch.html) +expose. Or in other words, it both allows applications to efficiently query +user/group records from local services, and allows local subsystems to provide +user/group records efficiently to local applications. + +This simple API only exposes only three method calls, and requires only a small +subset of the Varlink functionality. + +## Why Varlink? + +The API described in this document is based on a simple subset of the +mechanisms described by [Varlink](https://varlink.org/). The choice of +preferring Varlink over D-Bus and other IPCs in this context was made for three +reasons: + +1. User/Group record resolution should work during early boot and late shutdown + without special handling. This is very hard to do with D-Bus, as the broker + service for D-Bus generally runs as regular system daemon and is hence only + available at the latest boot stage. + +2. The JSON user/group records are native JSON data, hence picking an IPC + system that natively operates with JSON data is natural and clean. + +3. IPC systems such as D-Bus do not provide flow control and are thus unusable + for streaming data. They are useful to pass around short control messages, + but as soon as potentially many and large objects shall be transferred D-Bus + is not suitable, as any such streaming of messages would be considered + flooding in D-Bus' logic, and thus possibly result in termination of + communication. Since the APIs defined in this document need to support + enumerating potentially large numbers of users and groups, D-Bus is simply + not an appropriate option. + +## Concepts + +Each subsystem that needs to define users and groups on the local system is +supposed to implement this API, and offer its interfaces on a Varlink +`AF_UNIX`/`SOCK_STREAM` file system socket bound into the +`/run/systemd/userdb/` directory. When a client wants to look up a user or +group record, it contacts all sockets bound in this directory in parallel, and +enqueues the same query to each. The first positive reply is then returned to +the application, or if all fail the last seen error is returned instead. + +Unlike with glibc NSS there's no order or programmatic expression language +defined in which queries are issued to the various services. Instead, all +queries are always enqueued in parallel to all defined services, in order to +make look-ups efficient, and the simple rule of "first successful lookup wins" +is unconditionally followed for user and group look-ups (though not for +membership lookups, see below). + +This simple scheme only works safely as long as every service providing +user/group records carefully makes sure not to answer with conflicting +records. This API does not define any mechanisms for dealing with user/group +name/ID collisions during look-up nor during record registration. It assumes +the various subsystems that want to offer user and group records to the rest of +the system have made sufficiently sure in advance that their definitions do not +collide with those of other services. Clients are not expected to merge +multiple definitions for the same user or group, and will also not be able to +detect conflicts and suppress such conflicting records. + +It is recommended to name the sockets in the directory in reverse domain name +notation, but this is neither required nor enforced. + +## Well-Known Services + +Any subsystem that wants to provide user/group records can do so, simply by +binding a socket in the aforementioned directory. By default two +services are listening there, that have special relevance: + +1. `io.systemd.NameServiceSwitch` → This service makes the classic UNIX/glibc + NSS user/group records available as JSON User/Group records. Any such + records are automatically converted as needed, and possibly augmented with + information from the shadow databases. + +2. `io.systemd.Multiplexer` → This service multiplexes client queries to all + other running services. It's supposed to simplify client development: in + order to look up or enumerate user/group records it's sufficient to talk to + one service instead of all of them in parallel. + +Both these services are implemented by the same daemon +`systemd-userdbd.service`. + +Note that these services currently implement a subset of Varlink only. For +example, introspection is not available, and the resolver logic is not used. + +## Compatibility with NSS + +Two-way compatibility with classic UNIX/glibc NSS user/group records is +provided. When using the Varlink API, lookups into databases provided only via +NSS (and not natively via Varlink) are handled by the +`io.systemd.NameServiceSwitch` service (see above). When using the NSS API +(i.e. `getpwnam()` and friends) the `nss-systemd` module will automatically +synthesize NSS records for users/groups natively defined via a Varlink +API. Special care is taken to avoid recursion between these two compatibility +mechanisms. + +Subsystems that shall provide user/group records to the system may choose +between offering them via an NSS module or via a this Varlink API, either way +all records are accessible via both APIs, due to the two-way compatibility. It +is also possible to provide the same records via both APIs directly, but in +that case the compatibility logic must be turned off. There are mechanisms in +place for this, please contact the systemd project for details, as these are +currently not documented. + +## Caching of User Records + +This API defines no concepts for caching records. If caching is desired it +should be implemented in the subsystems that provide the user records, not in +the clients consuming them. + +## Method Calls + +``` +interface io.systemd.UserDatabase + +method GetUserRecord( + uid : ?int, + userName : ?string, + service : string +) -> ( + record : object, + incomplete : boolean +) + +method GetGroupRecord( + gid : ?int, + groupName : ?string, + service : string +) -> ( + record : object, + incomplete : boolean +) + +method GetMemberships( + userName : ?string, + groupName : ?string, + service : string +) -> ( + userName : string, + groupName : string +) + +error NoRecordFound() +error BadService() +error ServiceNotAvailable() +error ConflictingRecordFound() +``` + +The `GetUserRecord` method looks up or enumerates a user record. If the `uid` +parameter is set it specifies the numeric UNIX UID to search for. If the +`userName` parameter is set is specifies the name of the user to search +for. Typically, only one of the two parameters are set, depending whether a +look-up by UID or by name is desired. However, clients may also specify both +parameters, in which case a record matching both will be returned, and if only +one exists that matches one of the two parameters but not the other an error of +`ConflictingRecordFound` is returned. If neither of the two parameters are set +the whole user database is enumerated. In this case the method call needs to be +made with `more` set, so that multiple method call replies may be generated as +effect, each carrying one user record. + +The `service` parameter is mandatory and should be set to the service name +being talked to (i.e. to the same name as the `AF_UNIX` socket path, with the +`/run/systemd/userdb/` prefix removed). This is useful to allow implementation +of multiple services on the same socket (which is used by +`systemd-userdbd.service`). + +The method call returns one or more user records, depending which type of query is +used (see above). The record is returned in the `record` field. The +`incomplete` field indicates whether the record is complete. Services providing +user record lookup should only pass the `privileged` section of user records to +clients that either match the user the record is about or to sufficiently +privileged clients, for all others the section must be removed so that no +sensitive data is leaked this way. The `incomplete` parameter should indicate +whether the record has been modified like this or not (i.e. it is `true` if a +`privileged` section existed in the user record and was removed, and `false` if +no `privileged` section existed or one existed but hasn't been removed). + +If no user record matching the specified UID or name is known the error +`NoRecordFound` is returned (this is also returned if neither UID nor name are +specified, and hence enumeration requested but the subsystem currently has no +users defined). + +If a method call with an incorrectly set `service` field is received +(i.e. either not set at all, or not to the service's own name) a `BadService` +error is generated. Finally, `ServiceNotAvailable` should be returned when the +backing subsystem is not operational for some reason and hence no information +about existence or non-existence of a record can be returned nor any user +record at all. + +The `GetGroupRecord` method call works analogously but for groups. + +The `GetMemberships` method call may be used to inquire about group +memberships. The `userName` and `groupName` arguments take what the name +suggests. If one of the two is specified all matching memberships are returned, +if neither is specified all known memberships of any user and any group are +returned. The return value is a pair of user name and group name, where the +user is a member of the group. If both arguments are specified the specified +membership will be tested for, but no others. Unless both arguments are +specified the method call needs to be made with `more` set, so that multiple +replies can be returned (since typically there are are multiple members per +group and also multiple groups a user is member of). As with `GetUserRecord` +and `GetGroupRecord` the `service` parameter needs to contain the name of the +service being talked to, in order to allow implementation of multiple service +within the same IPC socket. In case no matching membership is known +`NoRecordFound` is returned. The other two errors are also generated in the +same cases as for `GetUserRecord` and `GetGroupRecord`. + +Unlike with `GetUserRecord` and `GetGroupRecord` the lists of memberships +returned by services are always combined. Thus unlike the other two calls a +membership lookup query has to wait for the last parallel query to complete +before the complete list is acquired. + +Note that only the `GetMemberships` call is authoritative about memberships of +users in groups. i.e. it should not be considered sufficient to check the +`memberOf` field of user records and the `members` field of group records to +acquire the full list of memberships. The full list can only bet determined by +`GetMemberships`, and as mentioned requires merging of these lists of all local +services. Result of this is that it can be one service that defines a user A, +and another service that defines a group B, and a third service that declares +that A is a member of B. + +And that's really all there is to it. From 749f6a7e870e0b942ca7f154716cc174b237336b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:16:30 +0200 Subject: [PATCH 108/109] TODO-HOME --- TODO | 17 ++++++++--------- TODO-HOME | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 TODO-HOME diff --git a/TODO b/TODO index c5b5b86057ba3..1538e15d10d49 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,14 @@ Janitorial Clean-ups: Features: +* socket units: allow creating a udev monitor socket with ListenDevices= or so, + with matches, then actviate app thorugh that passing socket oveer + +* cryptsetup: allow encoding key directly in /etc/crypttab, maybe with a + "base64:" prefix. Useful in particular for pkcs11 mode. + +* cryptsetup: add logic to wait for pkcs11 token + * coredump: maybe when coredumping read a new xattr from /proc/$PID/exe that may be used to mark a whole binary as non-coredumpable. Would fix: https://bugs.freedesktop.org/show_bug.cgi?id=69447 @@ -395,15 +403,6 @@ Features: * maybe introduce gpt auto discovery for /var/tmp? -* maybe add gpt-partition-based user management: each user gets his own - LUKS-encrypted GPT partition with a new GPT type. A small nss module - enumerates users via udev partition enumeration. UIDs are assigned in a fixed - way: the partition index is added as offset to some fixed base uid. User name - is stored in GPT partition name. A PAM module authenticates the user via the - LUKS partition password. Benefits: strong per-user security, compatibility - with stateless/read-only/verity-enabled root. (other idea: do this based on - loopback files in /home, without GPT involvement) - * gpt-auto logic: introduce support for discovering /var matching an image. For that, use a partition type UUID that is hashed from the OS name (as encoded in /etc/os-release), the architecture, and 4 new bits from the gpt flags diff --git a/TODO-HOME b/TODO-HOME new file mode 100644 index 0000000000000..4263f637e0826 --- /dev/null +++ b/TODO-HOME @@ -0,0 +1,51 @@ +PRIMARILY: + +- pkcs11 smart card support: pin certificate in record, then for auth, generate random data, ask smartcard to sign it, then verify signature. For LUKS key store encrypted key in record, decrypt it with smartcard. +- write fscrypt key into client's keyring +- don't use argon2 with pkcs#11 cryptsetup mode +- move the ssh authorized keys stuff from homectl to userdbctl +- honour passwordChangeNow, and unset it in homectl passwd +- fix borked yubikey cryptsetup stuff? + +Before first release: +- unit file lockdown (caps, …) +- extend test case to cover more +- man pages: + cryptsetup pkcs11 + pam_systemd update + nss-systemd update +- blog story +- fix userwork and homework paths +- drop LOG_DEBUG being forced in userdbd and homed +- drop debug=true being forced in pam_systemd_home +- performance data + +Later: +- when user tries to log into record signed by unrecognized key, automatically add key to our chain after polkit auth +- hook up machined/nspawn users with a varlink user query interface +- rollback when resize fails mid-operation +- forget key on suspend (requires gnome rework so that lock screen runs outside of uid) +- resize on login? +- update LUKS password on login if we find there's a password that unlocks the JSON record but not the LUKS device. +- always fstrim on logout? +- compare with accounts daemon +- create on activate? +- properties: icon url?, preferred session type?, administrator bool (which translates to 'wheel' membership)?, address?, telephone?, vcard?, samba stuff?, parental controls? +- communicate clearly when usb stick is safe to remove. probably involves + beefing up logind to make pam session close hook synchronous and wait until + systemd --user is shut down. +- logind: maybe keep a "busy fd" as long as there's a non-released session around or the user@.service +- fscrypt key mgmt (maybe in xattr?) +- shrink fs on logout? +- maybe make automatic, read-only, time-based reflink-copies of LUKS disk images (think: time machine) +- distuingish destroy / remove (i.e. currently we can unregister a user, unregister+remove their home directory, but not just remove their home directory) +- in systemd's PAMName= logic: query passwords with ssh-askpassword, so that we can make "loginctl set-linger" mode work +- fingerprint authentication, pattern authentication, … +- make sure "classic" user records can also be managed by homed +- description field for groups +- make size of $XDG_RUNTIME_DIR configurable in user record +- reuse pwquality magic in firstboot +- query password from kernel keyring first +- update in absence +- add a "access mode" + "fstype" field to the "status" section of json identity records reflecting the actually used access mode and fstype, even on non-luks backends +- move acct mgmt stuff from pam_systemd_home to pam_systemd? From 29550895b206d26d50c9307eab65fcc76d17f26a Mon Sep 17 00:00:00 2001 From: Thomas Hindoe Paaboel Andersen Date: Sun, 15 Sep 2019 23:53:15 +0200 Subject: [PATCH 109/109] USER_GROUP_API: typo fixes --- docs/USER_GROUP_API.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md index b9ac9c02c0c92..9d56085a8f9a2 100644 --- a/docs/USER_GROUP_API.md +++ b/docs/USER_GROUP_API.md @@ -17,7 +17,7 @@ expose. Or in other words, it both allows applications to efficiently query user/group records from local services, and allows local subsystems to provide user/group records efficiently to local applications. -This simple API only exposes only three method calls, and requires only a small +This simple API exposes only three method calls, and requires only a small subset of the Varlink functionality. ## Why Varlink? @@ -29,7 +29,7 @@ reasons: 1. User/Group record resolution should work during early boot and late shutdown without special handling. This is very hard to do with D-Bus, as the broker - service for D-Bus generally runs as regular system daemon and is hence only + service for D-Bus generally runs as a regular system daemon and is hence only available at the latest boot stage. 2. The JSON user/group records are native JSON data, hence picking an IPC @@ -108,7 +108,7 @@ API. Special care is taken to avoid recursion between these two compatibility mechanisms. Subsystems that shall provide user/group records to the system may choose -between offering them via an NSS module or via a this Varlink API, either way +between offering them via an NSS module or via this Varlink API, either way all records are accessible via both APIs, due to the two-way compatibility. It is also possible to provide the same records via both APIs directly, but in that case the compatibility logic must be turned off. There are mechanisms in @@ -161,8 +161,8 @@ error ConflictingRecordFound() The `GetUserRecord` method looks up or enumerates a user record. If the `uid` parameter is set it specifies the numeric UNIX UID to search for. If the -`userName` parameter is set is specifies the name of the user to search -for. Typically, only one of the two parameters are set, depending whether a +`userName` parameter is set it specifies the name of the user to search +for. Typically, only one of the two parameters are set, depending on whether a look-up by UID or by name is desired. However, clients may also specify both parameters, in which case a record matching both will be returned, and if only one exists that matches one of the two parameters but not the other an error of @@ -177,8 +177,8 @@ being talked to (i.e. to the same name as the `AF_UNIX` socket path, with the of multiple services on the same socket (which is used by `systemd-userdbd.service`). -The method call returns one or more user records, depending which type of query is -used (see above). The record is returned in the `record` field. The +The method call returns one or more user records, depending on which type of +query is used (see above). The record is returned in the `record` field. The `incomplete` field indicates whether the record is complete. Services providing user record lookup should only pass the `privileged` section of user records to clients that either match the user the record is about or to sufficiently @@ -197,7 +197,7 @@ If a method call with an incorrectly set `service` field is received (i.e. either not set at all, or not to the service's own name) a `BadService` error is generated. Finally, `ServiceNotAvailable` should be returned when the backing subsystem is not operational for some reason and hence no information -about existence or non-existence of a record can be returned nor any user +about the existence or non-existence of a record can be returned nor any user record at all. The `GetGroupRecord` method call works analogously but for groups. @@ -210,10 +210,10 @@ returned. The return value is a pair of user name and group name, where the user is a member of the group. If both arguments are specified the specified membership will be tested for, but no others. Unless both arguments are specified the method call needs to be made with `more` set, so that multiple -replies can be returned (since typically there are are multiple members per +replies can be returned (since typically there are multiple members per group and also multiple groups a user is member of). As with `GetUserRecord` and `GetGroupRecord` the `service` parameter needs to contain the name of the -service being talked to, in order to allow implementation of multiple service +service being talked to, in order to allow implementation of multiple services within the same IPC socket. In case no matching membership is known `NoRecordFound` is returned. The other two errors are also generated in the same cases as for `GetUserRecord` and `GetGroupRecord`. @@ -226,7 +226,7 @@ before the complete list is acquired. Note that only the `GetMemberships` call is authoritative about memberships of users in groups. i.e. it should not be considered sufficient to check the `memberOf` field of user records and the `members` field of group records to -acquire the full list of memberships. The full list can only bet determined by +acquire the full list of memberships. The full list can only be determined by `GetMemberships`, and as mentioned requires merging of these lists of all local services. Result of this is that it can be one service that defines a user A, and another service that defines a group B, and a third service that declares