diff --git a/0575-test-journal-dump-dump-the-headers-of-journal-files.patch b/0575-test-journal-dump-dump-the-headers-of-journal-files.patch new file mode 100644 index 0000000..b081ec0 --- /dev/null +++ b/0575-test-journal-dump-dump-the-headers-of-journal-files.patch @@ -0,0 +1,84 @@ +From 53118a0bc2f4855cb90ba24c674c3c6044ea6e3d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= +Date: Wed, 30 Jul 2025 11:33:35 +0200 +Subject: [PATCH] test-journal-dump: dump the headers of journal files + +We have journal_file_print_header(), but it's not exposed anywhere in +a way that it is easy to call. + +(cherry picked from commit 58ecb72bcf5b2f25057d58fe52ce9717c0d07382) + +Resolves: RHEL-106795 +--- + src/libsystemd/meson.build | 4 ++ + src/libsystemd/sd-journal/test-journal-dump.c | 46 +++++++++++++++++++ + 2 files changed, 50 insertions(+) + create mode 100644 src/libsystemd/sd-journal/test-journal-dump.c + +diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build +index fa28aa09fb..e5c8275fb0 100644 +--- a/src/libsystemd/meson.build ++++ b/src/libsystemd/meson.build +@@ -291,6 +291,10 @@ libsystemd_tests += [ + 'sources' : files('sd-journal/test-journal-append.c'), + 'type' : 'manual', + }, ++ { ++ 'sources' : files('sd-journal/test-journal-dump.c'), ++ 'type' : 'manual', ++ }, + { + 'sources' : files('sd-journal/test-journal-verify.c'), + 'timeout' : 90, +diff --git a/src/libsystemd/sd-journal/test-journal-dump.c b/src/libsystemd/sd-journal/test-journal-dump.c +new file mode 100644 +index 0000000000..bac2fb9a66 +--- /dev/null ++++ b/src/libsystemd/sd-journal/test-journal-dump.c +@@ -0,0 +1,46 @@ ++/* SPDX-License-Identifier: LGPL-2.1-or-later */ ++ ++#include "errno-util.h" ++#include "journal-file.h" ++#include "log.h" ++#include "main-func.h" ++#include "pager.h" ++#include "strv.h" ++ ++static int run(int argc, char *argv[]) { ++ int r = 0; ++ unsigned n = 0; ++ ++ _cleanup_(mmap_cache_unrefp) MMapCache *m = mmap_cache_new(); ++ assert_se(m); ++ ++ pager_open(/* flags= */ 0); ++ ++ STRV_FOREACH(s, strv_skip(argv, 1)) { ++ JournalFile *f = NULL; ++ ++ int k = journal_file_open( ++ /* fd= */ -EBADF, ++ *s, ++ O_RDONLY, ++ /* file_flags= */ 0, ++ 0666, ++ /* compress_threshold_bytes= */ UINT64_MAX, ++ /* metrics= */ NULL, ++ m, ++ /* template= */ NULL, ++ &f); ++ if (k < 0) ++ RET_GATHER(r, log_error_errno(k, "Failed to open %s, continuing: %m", *s)); ++ ++ if (n++ > 0) ++ puts(""); ++ ++ journal_file_print_header(f); ++ journal_file_close(f); ++ } ++ ++ return r; ++} ++ ++DEFINE_MAIN_FUNCTION(run); diff --git a/0576-journal-store-counts-not-byte-sizes-in-table-size-co.patch b/0576-journal-store-counts-not-byte-sizes-in-table-size-co.patch new file mode 100644 index 0000000..ec8acbe --- /dev/null +++ b/0576-journal-store-counts-not-byte-sizes-in-table-size-co.patch @@ -0,0 +1,94 @@ +From f9594f04c3a37ad8dab8f62bbeff67adc2e5ca44 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= +Date: Wed, 30 Jul 2025 11:39:35 +0200 +Subject: [PATCH] journal: store counts, not byte sizes, in table size + constants + +It's easier to think about the size in "objects", not bytes. Let's convert to +bytes at the last moment. + +Also drop some of the pointless size suffixes. In general, it's the size of the +variable that matters, not the constant that is written to it. + +No functional change. + +(cherry picked from commit fbbcd0edefd2921b117eebfeab8eefc3d26bd0c2) + +Resolves: RHEL-106795 +--- + src/libsystemd/sd-journal/journal-file.c | 27 ++++++++++++------------ + 1 file changed, 13 insertions(+), 14 deletions(-) + +diff --git a/src/libsystemd/sd-journal/journal-file.c b/src/libsystemd/sd-journal/journal-file.c +index 7e941edb19..791c1e8eaf 100644 +--- a/src/libsystemd/sd-journal/journal-file.c ++++ b/src/libsystemd/sd-journal/journal-file.c +@@ -42,11 +42,11 @@ + #include "user-util.h" + #include "xattr-util.h" + +-#define DEFAULT_DATA_HASH_TABLE_SIZE (2047ULL*sizeof(HashItem)) +-#define DEFAULT_FIELD_HASH_TABLE_SIZE (333ULL*sizeof(HashItem)) ++#define DEFAULT_DATA_HASH_TABLE_SIZE 2047U ++#define DEFAULT_FIELD_HASH_TABLE_SIZE 333U + +-#define DEFAULT_COMPRESS_THRESHOLD (512ULL) +-#define MIN_COMPRESS_THRESHOLD (8ULL) ++#define DEFAULT_COMPRESS_THRESHOLD 512U ++#define MIN_COMPRESS_THRESHOLD 8U + + /* This is the minimum journal file size */ + #define JOURNAL_FILE_SIZE_MIN (512 * U64_KB) /* 512 KiB */ +@@ -1281,15 +1281,14 @@ static int journal_file_setup_data_hash_table(JournalFile *f) { + beyond 75% fill level. Calculate the hash table size for + the maximum file size based on these metrics. */ + +- s = (f->metrics.max_size * 4 / 768 / 3) * sizeof(HashItem); +- if (s < DEFAULT_DATA_HASH_TABLE_SIZE) +- s = DEFAULT_DATA_HASH_TABLE_SIZE; ++ s = MAX(f->metrics.max_size * 4 / 768 / 3, ++ DEFAULT_DATA_HASH_TABLE_SIZE); + +- log_debug("Reserving %"PRIu64" entries in data hash table.", s / sizeof(HashItem)); ++ log_debug("Reserving %"PRIu64" entries in data hash table.", s); + + r = journal_file_append_object(f, + OBJECT_DATA_HASH_TABLE, +- offsetof(Object, hash_table.items) + s, ++ offsetof(Object, hash_table.items) + s * sizeof(HashItem), + &o, &p); + if (r < 0) + return r; +@@ -1297,7 +1296,7 @@ static int journal_file_setup_data_hash_table(JournalFile *f) { + memzero(o->hash_table.items, s); + + f->header->data_hash_table_offset = htole64(p + offsetof(Object, hash_table.items)); +- f->header->data_hash_table_size = htole64(s); ++ f->header->data_hash_table_size = htole64(s * sizeof(HashItem)); + + return 0; + } +@@ -1314,19 +1313,19 @@ static int journal_file_setup_field_hash_table(JournalFile *f) { + * number should grow very slowly only */ + + s = DEFAULT_FIELD_HASH_TABLE_SIZE; +- log_debug("Reserving %"PRIu64" entries in field hash table.", s / sizeof(HashItem)); ++ log_debug("Reserving %"PRIu64" entries in field hash table.", s); + + r = journal_file_append_object(f, + OBJECT_FIELD_HASH_TABLE, +- offsetof(Object, hash_table.items) + s, ++ offsetof(Object, hash_table.items) + s * sizeof(HashItem), + &o, &p); + if (r < 0) + return r; + +- memzero(o->hash_table.items, s); ++ memzero(o->hash_table.items, s * sizeof(HashItem)); + + f->header->field_hash_table_offset = htole64(p + offsetof(Object, hash_table.items)); +- f->header->field_hash_table_size = htole64(s); ++ f->header->field_hash_table_size = htole64(s * sizeof(HashItem)); + + return 0; + } diff --git a/0577-journal-treble-field-hash-table-size.patch b/0577-journal-treble-field-hash-table-size.patch new file mode 100644 index 0000000..b9351c3 --- /dev/null +++ b/0577-journal-treble-field-hash-table-size.patch @@ -0,0 +1,243 @@ +From 1aea0cf1757ce56a47f915aa89043fd5415d1f58 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= +Date: Wed, 30 Jul 2025 11:52:26 +0200 +Subject: [PATCH] journal: treble field hash table size + +As discussed in https://github.com/systemd/systemd/issues/38399, "ordinary" +systems can have the field table with a large number of values, causing journal +rotation to occur early. For example, audit generates a log of fields: +$ journalctl --fields | rg -c '^_?AUDIT' +114 + +It seems that the "structured log" capabilities of the journal are being use +more than in the past. Looking at some journal files on my system, it seems +the field hash table field is quite high in many cases: +$ build/test-journal-dump /var/log/journal/*/* | rg 'table fill' +Data hash table fill: 15.1% +Field hash table fill: 69.1% +Data hash table fill: 4.9% +Field hash table fill: 32.4% +Data hash table fill: 10.2% +Field hash table fill: 34.2% +Data hash table fill: 9.9% +Field hash table fill: 37.2% +Data hash table fill: 26.8% +Field hash table fill: 21.9% +Data hash table fill: 35.6% +Field hash table fill: 22.8% +Data hash table fill: 25.5% +Field hash table fill: 54.1% +Data hash table fill: 3.4% +Field hash table fill: 43.8% +Data hash table fill: 75.0% +Field hash table fill: 70.3% +Data hash table fill: 75.0% +Field hash table fill: 63.1% +Data hash table fill: 75.0% +Field hash table fill: 74.2% +Data hash table fill: 35.6% +Field hash table fill: 43.2% +Data hash table fill: 35.5% +Field hash table fill: 75.4% +Data hash table fill: 75.0% +Field hash table fill: 59.8% +Data hash table fill: 75.0% +Field hash table fill: 56.5% +Data hash table fill: 16.9% +Field hash table fill: 76.3% +Data hash table fill: 18.1% +Field hash table fill: 76.9% +Data hash table fill: 75.0% +Field hash table fill: 42.0% +Data hash table fill: 75.0% +Field hash table fill: 22.8% +Data hash table fill: 75.0% +Field hash table fill: 22.8% +Data hash table fill: 75.0% +Field hash table fill: 22.8% +Data hash table fill: 75.0% +Field hash table fill: 22.8% +Data hash table fill: 75.0% +Field hash table fill: 32.1% +Data hash table fill: 75.0% +Field hash table fill: 21.9% +Data hash table fill: 75.0% +Field hash table fill: 21.9% +Data hash table fill: 75.0% +Field hash table fill: 21.9% +Data hash table fill: 75.0% +Field hash table fill: 22.8% +Data hash table fill: 75.0% +Field hash table fill: 22.8% +Data hash table fill: 75.0% +Field hash table fill: 21.9% +Data hash table fill: 75.0% +Field hash table fill: 22.5% +Data hash table fill: 9.6% +Field hash table fill: 53.8% +Data hash table fill: 75.0% +Field hash table fill: 22.2% +Data hash table fill: 75.0% +Field hash table fill: 22.2% +Data hash table fill: 75.0% +Field hash table fill: 22.2% +Data hash table fill: 35.6% +Field hash table fill: 75.1% +Data hash table fill: 33.6% +Field hash table fill: 50.2% +Data hash table fill: 75.0% +Field hash table fill: 26.7% +Data hash table fill: 75.0% +Field hash table fill: 25.8% +Data hash table fill: 75.0% +Field hash table fill: 29.1% +Data hash table fill: 75.0% +Field hash table fill: 25.8% +Data hash table fill: 75.0% +Field hash table fill: 31.8% +Data hash table fill: 75.0% +Field hash table fill: 18.9% +Data hash table fill: 75.0% +Field hash table fill: 22.2% +Data hash table fill: 75.0% +Field hash table fill: 20.1% +Data hash table fill: 75.0% +Field hash table fill: 29.1% +Data hash table fill: 75.0% +Field hash table fill: 30.9% +Data hash table fill: 75.0% +Field hash table fill: 28.5% +Data hash table fill: 75.0% +Field hash table fill: 28.5% +Data hash table fill: 75.0% +Field hash table fill: 25.8% +Data hash table fill: 75.0% +Field hash table fill: 25.2% +Data hash table fill: 75.0% +Field hash table fill: 39.3% +Data hash table fill: 50.2% +Field hash table fill: 75.1% +Data hash table fill: 75.0% +Field hash table fill: 61.9% +Data hash table fill: 75.0% +Field hash table fill: 56.5% +Data hash table fill: 75.0% +Field hash table fill: 58.6% +Data hash table fill: 48.9% +Field hash table fill: 79.6% +Data hash table fill: 75.0% +Field hash table fill: 71.5% +Data hash table fill: 75.0% +Field hash table fill: 60.1% +Data hash table fill: 31.4% +Field hash table fill: 75.7% +Data hash table fill: 27.0% +Field hash table fill: 69.4% +Data hash table fill: 28.9% +Field hash table fill: 76.6% +Data hash table fill: 60.2% +Field hash table fill: 79.9% +Data hash table fill: 8.8% +Field hash table fill: 78.7% +Data hash table fill: 5.8% +Field hash table fill: 61.3% +Data hash table fill: 75.0% +Field hash table fill: 64.0% +Data hash table fill: 61.4% +Field hash table fill: 63.4% +Data hash table fill: 29.7% +Field hash table fill: 61.9% +Data hash table fill: 18.9% +Field hash table fill: 30.9% +Data hash table fill: 1.4% +Field hash table fill: 22.2% +Data hash table fill: 0.4% +Field hash table fill: 13.5% +Data hash table fill: 2.6% +Field hash table fill: 37.5% +Data hash table fill: 1.3% +Field hash table fill: 23.4% +Data hash table fill: 0.6% +Field hash table fill: 15.3% +Data hash table fill: 18.7% +Field hash table fill: 33.9% +Data hash table fill: 7.4% +Field hash table fill: 37.5% +Data hash table fill: 20.2% +Field hash table fill: 44.1% +Data hash table fill: 1.3% +Field hash table fill: 33.0% +Data hash table fill: 75.0% +Field hash table fill: 19.2% +Data hash table fill: 42.2% +Field hash table fill: 23.4% +Data hash table fill: 1.6% +Field hash table fill: 87.1% +Data hash table fill: 0.1% +Field hash table fill: 98.8% +Data hash table fill: 0.2% +Field hash table fill: 128.8% +Data hash table fill: 15.4% +Field hash table fill: 31.2% +Data hash table fill: 7.4% +Field hash table fill: 22.5% +Data hash table fill: 10.5% +Field hash table fill: 38.7% +Data hash table fill: 2.8% +Field hash table fill: 18.0% +Data hash table fill: 1.5% +Field hash table fill: 15.9% +Data hash table fill: 0.0% +Field hash table fill: 7.5% +Data hash table fill: 0.1% +Field hash table fill: 12.0% +Data hash table fill: 0.2% +Field hash table fill: 10.8% +Data hash table fill: 0.2% +Field hash table fill: 15.6% +Data hash table fill: 0.1% +Field hash table fill: 11.7% +Data hash table fill: 0.1% +Field hash table fill: 12.0% +Data hash table fill: 0.0% +Field hash table fill: 6.6% +Data hash table fill: 1.4% +Field hash table fill: 18.0% +Data hash table fill: 0.7% +Field hash table fill: 16.8% +Data hash table fill: 1.1% +Field hash table fill: 18.0% +Data hash table fill: 0.2% +Field hash table fill: 10.8% +Data hash table fill: 0.1% +Field hash table fill: 10.8% +Data hash table fill: 0.4% +Field hash table fill: 11.1% + +Since filling of the field hash table to 75% normally causes file rotation, +let's double the default to make rotation happen less often. +We'll use 11kB more for the hash table, which should be fine, considering +that journal files are usually at least 8 MB. + +Closes https://github.com/systemd/systemd/issues/38399. + +(cherry picked from commit e8962d77ac5a10926d0216246cfb5ad5dc25a533) + +Resolves: RHEL-106795 +--- + src/libsystemd/sd-journal/journal-file.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/libsystemd/sd-journal/journal-file.c b/src/libsystemd/sd-journal/journal-file.c +index 791c1e8eaf..290b8af73e 100644 +--- a/src/libsystemd/sd-journal/journal-file.c ++++ b/src/libsystemd/sd-journal/journal-file.c +@@ -43,7 +43,7 @@ + #include "xattr-util.h" + + #define DEFAULT_DATA_HASH_TABLE_SIZE 2047U +-#define DEFAULT_FIELD_HASH_TABLE_SIZE 333U ++#define DEFAULT_FIELD_HASH_TABLE_SIZE 1023U + + #define DEFAULT_COMPRESS_THRESHOLD 512U + #define MIN_COMPRESS_THRESHOLD 8U diff --git a/0578-nspawn-move-uid-shift-chown-code-into-shared.patch b/0578-nspawn-move-uid-shift-chown-code-into-shared.patch new file mode 100644 index 0000000..6765924 --- /dev/null +++ b/0578-nspawn-move-uid-shift-chown-code-into-shared.patch @@ -0,0 +1,375 @@ +From f9eb81c4c764d4d0c5a0a9265f805be6f575f02d Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Mon, 11 Nov 2024 20:58:30 +0100 +Subject: [PATCH] nspawn: move uid shift/chown() code into shared/ + +(cherry picked from commit b1fb2d971c810e0bdf9ff0ae567a1c6c230e4e5d) + +Related: RHEL-143036 +--- + src/nspawn/meson.build | 7 - + src/nspawn/nspawn-def.h | 9 -- + src/nspawn/nspawn.c | 2 +- + src/shared/meson.build | 1 + + .../nspawn-patch-uid.c => shared/shift-uid.c} | 141 ++++++------------ + .../nspawn-patch-uid.h => shared/shift-uid.h} | 0 + src/test/meson.build | 5 + + .../test-shift-uid.c} | 2 +- + 8 files changed, 52 insertions(+), 115 deletions(-) + delete mode 100644 src/nspawn/nspawn-def.h + rename src/{nspawn/nspawn-patch-uid.c => shared/shift-uid.c} (82%) + rename src/{nspawn/nspawn-patch-uid.h => shared/shift-uid.h} (100%) + rename src/{nspawn/test-patch-uid.c => test/test-shift-uid.c} (97%) + +diff --git a/src/nspawn/meson.build b/src/nspawn/meson.build +index 2a913b156d..26a7e7bfb9 100644 +--- a/src/nspawn/meson.build ++++ b/src/nspawn/meson.build +@@ -7,7 +7,6 @@ libnspawn_core_sources = files( + 'nspawn-mount.c', + 'nspawn-network.c', + 'nspawn-oci.c', +- 'nspawn-patch-uid.c', + 'nspawn-register.c', + 'nspawn-seccomp.c', + 'nspawn-settings.c', +@@ -63,12 +62,6 @@ executables += [ + nspawn_test_template + { + 'sources' : files('test-nspawn-util.c'), + }, +- test_template + { +- 'sources' : files('test-patch-uid.c'), +- 'link_with' : nspawn_libs, +- 'dependencies' : libacl, +- 'type' : 'manual', +- }, + nspawn_fuzz_template + { + 'sources' : files('fuzz-nspawn-settings.c'), + }, +diff --git a/src/nspawn/nspawn-def.h b/src/nspawn/nspawn-def.h +deleted file mode 100644 +index 32a20aabd5..0000000000 +--- a/src/nspawn/nspawn-def.h ++++ /dev/null +@@ -1,9 +0,0 @@ +-/* SPDX-License-Identifier: LGPL-2.1-or-later */ +-#pragma once +- +-#include +- +-/* While we are chmod()ing a directory tree, we set the top-level UID base to this "busy" base, so that we can always +- * recognize trees we are were chmod()ing recursively and got interrupted in */ +-#define UID_BUSY_BASE ((uid_t) UINT32_C(0xFFFE0000)) +-#define UID_BUSY_MASK ((uid_t) UINT32_C(0xFFFF0000)) +diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c +index 6f90f2f418..724639df5c 100644 +--- a/src/nspawn/nspawn.c ++++ b/src/nspawn/nspawn.c +@@ -73,7 +73,6 @@ + #include "nspawn-mount.h" + #include "nspawn-network.h" + #include "nspawn-oci.h" +-#include "nspawn-patch-uid.h" + #include "nspawn-register.h" + #include "nspawn-seccomp.h" + #include "nspawn-settings.h" +@@ -96,6 +95,7 @@ + #include "rlimit-util.h" + #include "rm-rf.h" + #include "seccomp-util.h" ++#include "shift-uid.h" + #include "signal-util.h" + #include "socket-util.h" + #include "stat-util.h" +diff --git a/src/shared/meson.build b/src/shared/meson.build +index af9ef74b32..4684b7709d 100644 +--- a/src/shared/meson.build ++++ b/src/shared/meson.build +@@ -158,6 +158,7 @@ shared_sources = files( + 'selinux-util.c', + 'serialize.c', + 'service-util.c', ++ 'shift-uid.c', + 'sleep-config.c', + 'smack-util.c', + 'smbios11.c', +diff --git a/src/nspawn/nspawn-patch-uid.c b/src/shared/shift-uid.c +similarity index 82% +rename from src/nspawn/nspawn-patch-uid.c +rename to src/shared/shift-uid.c +index b8918a2315..435ba46c8e 100644 +--- a/src/nspawn/nspawn-patch-uid.c ++++ b/src/shared/shift-uid.c +@@ -1,24 +1,21 @@ + /* SPDX-License-Identifier: LGPL-2.1-or-later */ + +-#include + #include +-#include +-#include + + #include "acl-util.h" + #include "dirent-util.h" + #include "fd-util.h" + #include "fileio.h" +-#include "fs-util.h" + #include "missing_magic.h" +-#include "nspawn-def.h" +-#include "nspawn-patch-uid.h" ++#include "shift-uid.h" + #include "stat-util.h" +-#include "stdio-util.h" +-#include "string-util.h" +-#include "strv.h" + #include "user-util.h" + ++/* While we are chmod()ing a directory tree, we set the top-level UID base to this "busy" base, so that we can always ++ * recognize trees we are were chmod()ing recursively and got interrupted in */ ++#define UID_BUSY_BASE ((uid_t) UINT32_C(0xFFFE0000)) ++#define UID_BUSY_MASK ((uid_t) UINT32_C(0xFFFF0000)) ++ + #if HAVE_ACL + + static int get_acl(int fd, const char *name, acl_type_t type, acl_t *ret) { +@@ -286,7 +283,7 @@ static int is_fs_fully_userns_compatible(const struct statfs *sfs) { + F_TYPE_EQUAL(sfs->f_type, SYSFS_MAGIC); + } + +-static int recurse_fd(int fd, bool donate_fd, const struct stat *st, uid_t shift, bool is_toplevel) { ++static int recurse_fd(int fd, const struct stat *st, uid_t shift, bool is_toplevel) { + _cleanup_closedir_ DIR *d = NULL; + bool changed = false; + struct statfs sfs; +@@ -303,10 +300,10 @@ static int recurse_fd(int fd, bool donate_fd, const struct stat *st, uid_t shift + + r = is_fs_fully_userns_compatible(&sfs); + if (r < 0) +- goto finish; ++ return r; + if (r > 0) { + r = 0; /* don't recurse */ +- goto finish; ++ return r; + } + + /* Also, if we hit a read-only file system, then don't bother, skip the whole subtree */ +@@ -315,56 +312,36 @@ static int recurse_fd(int fd, bool donate_fd, const struct stat *st, uid_t shift + goto read_only; + + if (S_ISDIR(st->st_mode)) { +- if (!donate_fd) { +- int copy; +- +- copy = fcntl(fd, F_DUPFD_CLOEXEC, 3); +- if (copy < 0) { +- r = -errno; +- goto finish; +- } +- +- fd = copy; +- donate_fd = true; +- } +- + d = take_fdopendir(&fd); +- if (!d) { +- r = -errno; +- goto finish; +- } ++ if (!d) ++ return -errno; + +- FOREACH_DIRENT_ALL(de, d, r = -errno; goto finish) { ++ FOREACH_DIRENT_ALL(de, d, return -errno) { + struct stat fst; + + if (dot_or_dot_dot(de->d_name)) + continue; + +- if (fstatat(dirfd(d), de->d_name, &fst, AT_SYMLINK_NOFOLLOW) < 0) { +- r = -errno; +- goto finish; +- } ++ if (fstatat(dirfd(d), de->d_name, &fst, AT_SYMLINK_NOFOLLOW) < 0) ++ return -errno; + + if (S_ISDIR(fst.st_mode)) { + int subdir_fd; + + subdir_fd = openat(dirfd(d), de->d_name, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME); +- if (subdir_fd < 0) { +- r = -errno; +- goto finish; +- +- } ++ if (subdir_fd < 0) ++ return -errno; + +- r = recurse_fd(subdir_fd, true, &fst, shift, false); ++ r = recurse_fd(subdir_fd, &fst, shift, false); + if (r < 0) +- goto finish; ++ return r; + if (r > 0) + changed = true; + + } else { + r = patch_fd(dirfd(d), de->d_name, &fst, shift); + if (r < 0) +- goto finish; ++ return r; + if (r > 0) + changed = true; + } +@@ -380,8 +357,7 @@ static int recurse_fd(int fd, bool donate_fd, const struct stat *st, uid_t shift + if (r > 0) + changed = true; + +- r = changed; +- goto finish; ++ return changed; + + read_only: + if (!is_toplevel) { +@@ -393,51 +369,41 @@ read_only: + r = changed; + } + +-finish: +- if (donate_fd) +- safe_close(fd); +- + return r; + } + +-static int fd_patch_uid_internal(int fd, bool donate_fd, uid_t shift, uid_t range) { ++int path_patch_uid(const char *path, uid_t shift, uid_t range) { ++ _cleanup_close_ int fd = -EBADF; + struct stat st; +- int r; + +- assert(fd >= 0); ++ assert(path); ++ ++ fd = open(path, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME); ++ if (fd < 0) ++ return -errno; + + /* Recursively adjusts the UID/GIDs of all files of a directory tree. This is used to automatically fix up an + * OS tree to the used user namespace UID range. Note that this automatic adjustment only works for UID ranges + * following the concept that the upper 16-bit of a UID identify the container, and the lower 16-bit are the actual + * UID within the container. */ + +- if ((shift & 0xFFFF) != 0) { +- /* We only support containers where the shift starts at a 2^16 boundary */ +- r = -EOPNOTSUPP; +- goto finish; +- } ++ /* We only support containers where the shift starts at a 2^16 boundary */ ++ if ((shift & 0xFFFF) != 0) ++ return -EOPNOTSUPP; + +- if (shift == UID_BUSY_BASE) { +- r = -EINVAL; +- goto finish; +- } ++ if (shift == UID_BUSY_BASE) ++ return -EINVAL; + +- if (range != 0x10000) { +- /* We only support containers with 16-bit UID ranges for the patching logic */ +- r = -EOPNOTSUPP; +- goto finish; +- } ++ /* We only support containers with 16-bit UID ranges for the patching logic */ ++ if (range != 0x10000) ++ return -EOPNOTSUPP; + +- if (fstat(fd, &st) < 0) { +- r = -errno; +- goto finish; +- } ++ if (fstat(fd, &st) < 0) ++ return -errno; + +- if ((uint32_t) st.st_uid >> 16 != (uint32_t) st.st_gid >> 16) { +- /* We only support containers where the uid/gid container ID match */ +- r = -EBADE; +- goto finish; +- } ++ /* We only support containers where the uid/gid container ID match */ ++ if ((uint32_t) st.st_uid >> 16 != (uint32_t) st.st_gid >> 16) ++ return -EBADE; + + /* Try to detect if the range is already right. Of course, this a pretty drastic optimization, as we assume + * that if the top-level dir has the right upper 16-bit assigned, then everything below will have too... */ +@@ -448,30 +414,11 @@ static int fd_patch_uid_internal(int fd, bool donate_fd, uid_t shift, uid_t rang + * range. Should we be interrupted in the middle of our work, we'll see it owned by this user and will start + * chown()ing it again, unconditionally, as the busy UID is not a valid UID we'd everpick for ourselves. */ + +- if ((st.st_uid & UID_BUSY_MASK) != UID_BUSY_BASE) { ++ if ((st.st_uid & UID_BUSY_MASK) != UID_BUSY_BASE) + if (fchown(fd, + UID_BUSY_BASE | (st.st_uid & ~UID_BUSY_MASK), +- (gid_t) UID_BUSY_BASE | (st.st_gid & ~(gid_t) UID_BUSY_MASK)) < 0) { +- r = -errno; +- goto finish; +- } +- } +- +- return recurse_fd(fd, donate_fd, &st, shift, true); +- +-finish: +- if (donate_fd) +- safe_close(fd); +- +- return r; +-} +- +-int path_patch_uid(const char *path, uid_t shift, uid_t range) { +- int fd; +- +- fd = open(path, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW|O_NOATIME); +- if (fd < 0) +- return -errno; ++ (gid_t) UID_BUSY_BASE | (st.st_gid & ~(gid_t) UID_BUSY_MASK)) < 0) ++ return -errno; + +- return fd_patch_uid_internal(fd, true, shift, range); ++ return recurse_fd(TAKE_FD(fd), &st, shift, true); + } +diff --git a/src/nspawn/nspawn-patch-uid.h b/src/shared/shift-uid.h +similarity index 100% +rename from src/nspawn/nspawn-patch-uid.h +rename to src/shared/shift-uid.h +diff --git a/src/test/meson.build b/src/test/meson.build +index d8135c226c..5dd9e3b8bd 100644 +--- a/src/test/meson.build ++++ b/src/test/meson.build +@@ -386,6 +386,11 @@ executables += [ + 'sources' : files('test-parse-util.c'), + 'dependencies' : libm, + }, ++ test_template + { ++ 'sources' : files('test-shift-uid.c'), ++ 'dependencies' : libacl, ++ 'type' : 'manual', ++ }, + test_template + { + 'sources' : files('test-process-util.c'), + 'dependencies' : threads, +diff --git a/src/nspawn/test-patch-uid.c b/src/test/test-shift-uid.c +similarity index 97% +rename from src/nspawn/test-patch-uid.c +rename to src/test/test-shift-uid.c +index f8f44b0b0b..b1e272c79f 100644 +--- a/src/nspawn/test-patch-uid.c ++++ b/src/test/test-shift-uid.c +@@ -3,7 +3,7 @@ + #include + + #include "log.h" +-#include "nspawn-patch-uid.h" ++#include "shift-uid.h" + #include "user-util.h" + #include "string-util.h" + #include "tests.h" diff --git a/0579-user-classification-add-new-foreign-UID-range.patch b/0579-user-classification-add-new-foreign-UID-range.patch new file mode 100644 index 0000000..355e248 --- /dev/null +++ b/0579-user-classification-add-new-foreign-UID-range.patch @@ -0,0 +1,319 @@ +From 69b8c163b4785b80195d109333776a416b9c4318 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Fri, 8 Nov 2024 12:14:16 +0100 +Subject: [PATCH] user-classification: add new "foreign" UID range + +This makes the UID range configurable via build time options, but of +course it really shouldn't be changed. The default range I picked is +outside even of IPAs current (ridiculously large) allocation ranges, +hence hopefully minimizes conflicts. + +(cherry picked from commit ec0c10fc9db6459d78f0b3970a0f7a34c88e6db3) + +Related: RHEL-143036 +--- + docs/UIDS-GIDS.md | 60 ++++++++++++++++++++-------------- + docs/USER_RECORD.md | 7 ++-- + meson.build | 4 +++ + meson_options.txt | 2 ++ + src/basic/uid-classification.c | 2 +- + src/basic/uid-classification.h | 12 +++++++ + src/core/systemd.pc.in | 2 ++ + src/dissect/dissect.c | 2 +- + src/shared/group-record.c | 3 ++ + src/shared/user-record.c | 4 +++ + src/shared/user-record.h | 1 + + src/userdb/userdbctl.c | 7 ++++ + 12 files changed, 78 insertions(+), 28 deletions(-) + +diff --git a/docs/UIDS-GIDS.md b/docs/UIDS-GIDS.md +index 09488e2a78..35018b77a4 100644 +--- a/docs/UIDS-GIDS.md ++++ b/docs/UIDS-GIDS.md +@@ -129,10 +129,18 @@ possible. + erroneously considers UIDs signed integers, and hence can't deal with values above 2^31. + The `systemd-machined.service` service will synthesize user database records for all UIDs assigned to a running container from this range. + +-Note for both allocation ranges: when a UID allocation takes place NSS is +-checked for collisions first, and a different UID is picked if an entry is found. +-Thus, the user database is used as synchronization mechanism to ensure +-exclusive ownership of UIDs and UID ranges. ++4. 2147352576…2147418111 → UID range used for foreign OS images. For various ++ usecases (primarily: containers) it makes sense to make foreign OS images ++ available locally whose UID/GID ownerships do not make sense in the local ++ context but only within the OS image itself. This 64K UID range can be used ++ to have a clearly defined ownership even on the host, that can be mapped via ++ idmapped mount to a dynamic runtime UID range as needed. (These numbers in ++ hexadecimal are 0x7FFE0000…0x7FFEFFFF.) ++ ++Note for the `DynamicUser=` and the `systemd-nspawn` allocation ranges: when a ++UID allocation takes place NSS is checked for collisions first, and a different ++UID is picked if an entry is found. Thus, the user database is used as ++synchronization mechanism to ensure exclusive ownership of UIDs and UID ranges. + To ensure compatibility with other subsystems allocating from the same ranges it is hence essential that they + ensure that whatever they pick shows up in the user/group databases, either by + providing an NSS module, or by adding entries directly to `/etc/passwd` and `/etc/group`. +@@ -157,6 +165,8 @@ $ pkg-config --variable=container_uid_base_min systemd + 524288 + $ pkg-config --variable=container_uid_base_max systemd + 1878982656 ++$ pkg-config --variable=foreign_uid_base systemd ++2147352576 + ``` + + (Note that the latter encodes the maximum UID *base* `systemd-nspawn` might +@@ -164,7 +174,7 @@ pick — given that 64K UIDs are assigned to each container according to this + allocation logic, the maximum UID used for this range is hence + 1878982656+65535=1879048191.) + +-Systemd has compile-time default for these boundaries. ++systemd has compile-time default for these boundaries. + Using those defaults is recommended. + It will nevertheless query `/etc/login.defs` at runtime, when compiled with `-Dcompat-mutable-uid-boundaries=true` and that file is present. + Support for this is considered only a compatibility feature and should not be +@@ -244,25 +254,27 @@ i.e. somewhere below `/var/` or similar. + + ## Summary + +-| UID/GID | Purpose | Defined By | Listed in | +-|-----------------------|-----------------------|---------------|-------------------------------| +-| 0 | `root` user | Linux | `/etc/passwd` + `nss-systemd` | +-| 1…4 | System users | Distributions | `/etc/passwd` | +-| 5 | `tty` group | `systemd` | `/etc/passwd` | +-| 6…999 | System users | Distributions | `/etc/passwd` | +-| 1000…60000 | Regular users | Distributions | `/etc/passwd` + LDAP/NIS/… | +-| 60001…60513 | Human users (homed) | `systemd` | `nss-systemd` | +-| 60514…60577 | Host users mapped into containers | `systemd` | `systemd-nspawn` | +-| 60578…61183 | Unused | | | +-| 61184…65519 | Dynamic service users | `systemd` | `nss-systemd` | +-| 65520…65533 | Unused | | | +-| 65534 | `nobody` user | Linux | `/etc/passwd` + `nss-systemd` | +-| 65535 | 16-bit `(uid_t) -1` | Linux | | +-| 65536…524287 | Unused | | | +-| 524288…1879048191 | Container UID ranges | `systemd` | `nss-systemd` | +-| 1879048192…2147483647 | Unused | | | +-| 2147483648…4294967294 | HIC SVNT LEONES | | | +-| 4294967295 | 32-bit `(uid_t) -1` | Linux | | ++| UID/GID | Same in Hexadecimal | How Many | Purpose | Defined By | Listed in | ++|----------------------:|----------------------:|-----------:|:----------------------------------|:--------------|:------------------------------| ++| 0 | 0x00000000 | 1 | `root` user | Linux | `/etc/passwd` + `nss-systemd` | ++| 1…4 | 0x00000001…0x00000004 | 4 | System users | Distributions | `/etc/passwd` | ++| 5 | 0x00000005 | 1 | `tty` group | `systemd` | `/etc/passwd` | ++| 6…999 | 0x00000006…0x000003E7 | 994 | System users | Distributions | `/etc/passwd` | ++| 1000…60000 | 0x000003E8…0x00001770 | 59000 | Regular users | Distributions | `/etc/passwd` + LDAP/NIS/… | ++| 60001…60513 | 0x0000EA61…0x0000EC61 | 513 | Human users (homed) | `systemd` | `nss-systemd` | ++| 60514…60577 | 0x0000EC62…0x0000ECA1 | 64 | Host users mapped into containers | `systemd` | `systemd-nspawn` | ++| 60578…61183 | 0x0000ECA2…0x0000EEFF | 606 | *unused* | | | ++| 61184…65519 | 0x0000EF00…0x0000FFEF | 4336 | Dynamic service users | `systemd` | `nss-systemd` | ++| 65520…65533 | 0x0000FFF0…0x0000FFFD | 13 | *unused* | | | ++| 65534 | 0x0000FFFE | 1 | `nobody` user | Linux | `/etc/passwd` + `nss-systemd` | ++| 65535 | 0x0000FFFF | 1 | 16-bit `(uid_t) -1` | Linux | | ++| 65536…524287 | 0x00010000…0x0007FFFF | 458752 | *unused* | | | ++| 524288…1879048191 | 0x00080000…0x6FFFFFFF | 1878523904 | Container UID ranges | `systemd` | `nss-systemd` | ++| 1879048192…2147352575 | 0x70000000…0x7FFDFFFF | 1879048192 | *unused* | | | ++| 2147352576…2147418111 | 0x7FFE0000…0x7FFEFFFF | 65536 | Foreign UID range | `systemd` | `nss-systemd` | ++| 2147418112…2147483647 | 0x7FFF0000…0x7FFFFFFF | 65536 | *unused* | | | ++| 2147483648…4294967294 | 0x80000000…0xFFFFFFFE | 2147483647 | *HIC SVNT LEONES* | | | ++| 4294967295 | 0xFFFFFFFF | 1 | 32-bit `(uid_t) -1` | Linux | | + + Note that "Unused" in the table above doesn't mean that these ranges are really unused. + It just means that these ranges have no well-established +diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md +index 5babc70f65..a8e02b2c5e 100644 +--- a/docs/USER_RECORD.md ++++ b/docs/USER_RECORD.md +@@ -267,14 +267,17 @@ It's probably wise to use a location string processable by geo-location subsyste + 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, ++`container`, `foreign`, `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. ++The `foreign` string should be used for users from UID ranges which are used ++for OS images from foreign systems, i.e. where local resolution would not make ++sense. ++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 +diff --git a/meson.build b/meson.build +index 873d70f8d3..fa39da2d38 100644 +--- a/meson.build ++++ b/meson.build +@@ -887,6 +887,9 @@ container_uid_base_max = get_option('container-uid-base-max') + conf.set('CONTAINER_UID_BASE_MIN', container_uid_base_min) + conf.set('CONTAINER_UID_BASE_MAX', container_uid_base_max) + ++foreign_uid_base = get_option('foreign-uid-base') ++conf.set('FOREIGN_UID_BASE', foreign_uid_base) ++ + nobody_user = get_option('nobody-user') + nobody_group = get_option('nobody-group') + +@@ -3011,6 +3014,7 @@ summary({ + conf.get('SYSTEM_ALLOC_GID_MIN')), + 'dynamic UIDs' : '@0@…@1@'.format(dynamic_uid_min, dynamic_uid_max), + 'container UID bases' : '@0@…@1@'.format(container_uid_base_min, container_uid_base_max), ++ 'foreign UID base' : '@0@'.format(foreign_uid_base), + 'static UID/GID allocations' : ' '.join(static_ugids), + '/dev/kvm access mode' : get_option('dev-kvm-mode'), + 'render group access mode' : get_option('group-render-mode'), +diff --git a/meson_options.txt b/meson_options.txt +index 78b7c5fe30..df2bacdd3d 100644 +--- a/meson_options.txt ++++ b/meson_options.txt +@@ -273,6 +273,8 @@ option('container-uid-base-min', type : 'integer', value : 0x00080000, + description : 'minimum container UID base') + option('container-uid-base-max', type : 'integer', value : 0x6FFF0000, + description : 'maximum container UID base') ++option('foreign-uid-base', type : 'integer', value : 0x7FFE0000, ++ description : 'foreign OS image UID base') + option('adm-group', type : 'boolean', + description : 'the ACL for adm group should be added') + option('wheel-group', type : 'boolean', +diff --git a/src/basic/uid-classification.c b/src/basic/uid-classification.c +index 2c8b06c0d3..1295db01ae 100644 +--- a/src/basic/uid-classification.c ++++ b/src/basic/uid-classification.c +@@ -127,5 +127,5 @@ bool uid_for_system_journal(uid_t uid) { + + /* Returns true if the specified UID shall get its data stored in the system journal. */ + +- return uid_is_system(uid) || uid_is_dynamic(uid) || uid == UID_NOBODY; ++ return uid_is_system(uid) || uid_is_dynamic(uid) || uid == UID_NOBODY || uid_is_foreign(uid); + } +diff --git a/src/basic/uid-classification.h b/src/basic/uid-classification.h +index 0932123d5c..2d76be5f04 100644 +--- a/src/basic/uid-classification.h ++++ b/src/basic/uid-classification.h +@@ -12,6 +12,10 @@ assert_cc((CONTAINER_UID_BASE_MAX & 0xFFFFU) == 0); + #define CONTAINER_UID_MIN (CONTAINER_UID_BASE_MIN) + #define CONTAINER_UID_MAX (CONTAINER_UID_BASE_MAX + 0xFFFFU) + ++assert_cc((FOREIGN_UID_BASE & 0xFFFFU) == 0); ++#define FOREIGN_UID_MIN (FOREIGN_UID_BASE) ++#define FOREIGN_UID_MAX (FOREIGN_UID_BASE + 0xFFFFU) ++ + bool uid_is_system(uid_t uid); + bool gid_is_system(gid_t gid); + +@@ -31,6 +35,14 @@ static inline bool gid_is_container(gid_t gid) { + return uid_is_container((uid_t) gid); + } + ++static inline bool uid_is_foreign(uid_t uid) { ++ return FOREIGN_UID_MIN <= uid && uid <= FOREIGN_UID_MAX; ++} ++ ++static inline bool gid_is_foreign(gid_t gid) { ++ return uid_is_foreign((uid_t) gid); ++} ++ + typedef struct UGIDAllocationRange { + uid_t system_alloc_uid_min; + uid_t system_uid_max; +diff --git a/src/core/systemd.pc.in b/src/core/systemd.pc.in +index f3b85b0190..8d044dd7ad 100644 +--- a/src/core/systemd.pc.in ++++ b/src/core/systemd.pc.in +@@ -102,6 +102,8 @@ containeruidbasemin=${container_uid_base_min} + container_uid_base_max={{CONTAINER_UID_BASE_MAX}} + containeruidbasemax=${container_uid_base_max} + ++foreign_uid_base={{FOREIGN_UID_BASE}} ++ + Name: systemd + Description: systemd System and Service Manager + URL: {{PROJECT_URL}} +diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c +index 78a830d574..84e836027a 100644 +--- a/src/dissect/dissect.c ++++ b/src/dissect/dissect.c +@@ -1172,7 +1172,7 @@ static const char *pick_color_for_uid_gid(uid_t uid) { + return ansi_normal(); /* files in disk images are typically owned by root and other system users, no issue there */ + if (uid_is_dynamic(uid)) + return ansi_highlight_red(); /* files should never be owned persistently by dynamic users, and there are just no excuses */ +- if (uid_is_container(uid)) ++ if (uid_is_container(uid) || uid_is_foreign(uid)) + return ansi_highlight_cyan(); + + return ansi_highlight(); +diff --git a/src/shared/group-record.c b/src/shared/group-record.c +index e4a4eca99c..dcabdf634f 100644 +--- a/src/shared/group-record.c ++++ b/src/shared/group-record.c +@@ -302,6 +302,9 @@ UserDisposition group_record_disposition(GroupRecord *h) { + if (gid_is_container(h->gid)) + return USER_CONTAINER; + ++ if (gid_is_foreign(h->gid)) ++ return USER_FOREIGN; ++ + if (h->gid > INT32_MAX) + return USER_RESERVED; + +diff --git a/src/shared/user-record.c b/src/shared/user-record.c +index 8617c70aef..75605f248b 100644 +--- a/src/shared/user-record.c ++++ b/src/shared/user-record.c +@@ -1994,6 +1994,9 @@ UserDisposition user_record_disposition(UserRecord *h) { + if (uid_is_container(h->uid)) + return USER_CONTAINER; + ++ if (uid_is_foreign(h->uid)) ++ return USER_FOREIGN; ++ + if (h->uid > INT32_MAX) + return USER_RESERVED; + +@@ -2736,6 +2739,7 @@ static const char* const user_disposition_table[_USER_DISPOSITION_MAX] = { + [USER_DYNAMIC] = "dynamic", + [USER_REGULAR] = "regular", + [USER_CONTAINER] = "container", ++ [USER_FOREIGN] = "foreign", + [USER_RESERVED] = "reserved", + }; + +diff --git a/src/shared/user-record.h b/src/shared/user-record.h +index f8c7454f21..d80a46130a 100644 +--- a/src/shared/user-record.h ++++ b/src/shared/user-record.h +@@ -17,6 +17,7 @@ typedef enum UserDisposition { + USER_DYNAMIC, /* dynamically allocated users for system services */ + USER_REGULAR, /* regular (typically human users) */ + USER_CONTAINER, /* UID ranges allocated for container uses */ ++ USER_FOREIGN, /* UID range allocated for foreign OS images */ + USER_RESERVED, /* Range above 2^31 */ + _USER_DISPOSITION_MAX, + _USER_DISPOSITION_INVALID = -EINVAL, +diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c +index 0bb458eb15..f029c801a1 100644 +--- a/src/userdb/userdbctl.c ++++ b/src/userdb/userdbctl.c +@@ -62,6 +62,7 @@ static const char *user_disposition_to_color(UserDisposition d) { + return ansi_green(); + + case USER_CONTAINER: ++ case USER_FOREIGN: + return ansi_cyan(); + + case USER_RESERVED: +@@ -171,6 +172,12 @@ static const struct { + .name = "container", + .disposition = USER_CONTAINER, + }, ++ { ++ .first = FOREIGN_UID_MIN, ++ .last = FOREIGN_UID_MAX, ++ .name = "foreign", ++ .disposition = USER_FOREIGN, ++ }, + #if ENABLE_HOMED + { + .first = HOME_UID_MIN, diff --git a/0580-userdb-synthesize-stub-user-records-for-the-foreign-.patch b/0580-userdb-synthesize-stub-user-records-for-the-foreign-.patch new file mode 100644 index 0000000..784b866 --- /dev/null +++ b/0580-userdb-synthesize-stub-user-records-for-the-foreign-.patch @@ -0,0 +1,388 @@ +From de19e6fd05fe14ef0cf661abc0a087318019ed6b Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Tue, 12 Nov 2024 17:04:11 +0100 +Subject: [PATCH] userdb: synthesize stub user records for the foreign UID + +(cherry picked from commit 44eb6b81db77216e4f34e1de37eda133d7db6945) + +Related: RHEL-143036 +--- + man/userdbctl.xml | 6 +- + src/nspawn/nspawn-bind-user.c | 4 +- + src/nss-systemd/nss-systemd.c | 12 ++-- + src/shared/userdb.c | 127 +++++++++++++++++++++++++++++++--- + src/shared/userdb.h | 19 ++--- + src/userdb/userdbctl.c | 4 +- + 6 files changed, 142 insertions(+), 30 deletions(-) + +diff --git a/man/userdbctl.xml b/man/userdbctl.xml +index fb66ee2c0a..22d7da4d12 100644 +--- a/man/userdbctl.xml ++++ b/man/userdbctl.xml +@@ -136,9 +136,9 @@ + + + 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. ++ are not defined otherwise, as well as the user/groups for the "foreign" UID range. By default (or with ++ yes), such records are implicitly synthesized if otherwise missing since they have ++ special significance to the OS. When no, this synthesizing is turned off. + + + +diff --git a/src/nspawn/nspawn-bind-user.c b/src/nspawn/nspawn-bind-user.c +index d64a89f161..749accdce8 100644 +--- a/src/nspawn/nspawn-bind-user.c ++++ b/src/nspawn/nspawn-bind-user.c +@@ -231,7 +231,7 @@ int bind_user_prepare( + _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL; + _cleanup_free_ char *sm = NULL, *sd = NULL; + +- r = userdb_by_name(*n, USERDB_DONT_SYNTHESIZE, &u); ++ r = userdb_by_name(*n, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &u); + if (r < 0) + return log_error_errno(r, "Failed to resolve user '%s': %m", *n); + +@@ -252,7 +252,7 @@ int bind_user_prepare( + if (u->uid >= uid_shift && u->uid < uid_shift + uid_range) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID of user '%s' to map is already in container UID range, refusing.", u->user_name); + +- r = groupdb_by_gid(u->gid, USERDB_DONT_SYNTHESIZE, &g); ++ r = groupdb_by_gid(u->gid, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &g); + if (r < 0) + return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name); + +diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c +index 8e8d4cf1cb..d686d920fc 100644 +--- a/src/nss-systemd/nss-systemd.c ++++ b/src/nss-systemd/nss-systemd.c +@@ -615,7 +615,7 @@ enum nss_status _nss_systemd_setpwent(int stayopen) { + * (think: LDAP/NIS type situations), and our synthesizing of root/nobody is a robustness fallback + * only, which matters for getpwnam()/getpwuid() primarily, which are the main NSS entrypoints to the + * user database. */ +- r = userdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE, &getpwent_data.iterator); ++ r = userdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getpwent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +@@ -634,8 +634,8 @@ enum nss_status _nss_systemd_setgrent(int stayopen) { + getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator); + getgrent_data.by_membership = false; + +- /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE here */ +- r = groupdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE, &getgrent_data.iterator); ++ /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE_INTRINSIC here */ ++ r = groupdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getgrent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +@@ -654,8 +654,8 @@ enum nss_status _nss_systemd_setspent(int stayopen) { + getspent_data.iterator = userdb_iterator_free(getspent_data.iterator); + getspent_data.by_membership = false; + +- /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE here */ +- r = userdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE, &getspent_data.iterator); ++ /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE_INTRINSIC here */ ++ r = userdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getspent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +@@ -675,7 +675,7 @@ enum nss_status _nss_systemd_setsgent(int stayopen) { + getsgent_data.by_membership = false; + + /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE here */ +- r = groupdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE, &getsgent_data.iterator); ++ r = groupdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getsgent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +diff --git a/src/shared/userdb.c b/src/shared/userdb.c +index 29acebae19..1154906fd5 100644 +--- a/src/shared/userdb.c ++++ b/src/shared/userdb.c +@@ -16,10 +16,11 @@ + #include "set.h" + #include "socket-util.h" + #include "strv.h" ++#include "uid-classification.h" + #include "user-record-nss.h" + #include "user-util.h" +-#include "userdb-dropin.h" + #include "userdb.h" ++#include "userdb-dropin.h" + + DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(link_hash_ops, void, trivial_hash_func, trivial_compare_func, sd_varlink, sd_varlink_unref); + +@@ -116,8 +117,8 @@ static UserDBIterator* userdb_iterator_new(LookupWhat what, UserDBFlags flags) { + *i = (UserDBIterator) { + .what = what, + .flags = flags, +- .synthesize_root = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE), +- .synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE), ++ .synthesize_root = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_INTRINSIC), ++ .synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_INTRINSIC), + }; + + return i; +@@ -434,7 +435,7 @@ static int userdb_start_query( + } + + /* First, let's talk to the multiplexer, if we can */ +- if ((flags & (USERDB_AVOID_MULTIPLEXER|USERDB_EXCLUDE_DYNAMIC_USER|USERDB_EXCLUDE_NSS|USERDB_EXCLUDE_DROPIN|USERDB_DONT_SYNTHESIZE)) == 0 && ++ if ((flags & (USERDB_AVOID_MULTIPLEXER|USERDB_EXCLUDE_DYNAMIC_USER|USERDB_EXCLUDE_NSS|USERDB_EXCLUDE_DROPIN|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN)) == 0 && + !strv_contains(except, "io.systemd.Multiplexer") && + (!only || strv_contains(only, "io.systemd.Multiplexer"))) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *patched_query = sd_json_variant_ref(query); +@@ -617,6 +618,63 @@ static int synthetic_nobody_user_build(UserRecord **ret) { + SD_JSON_BUILD_PAIR("disposition", JSON_BUILD_CONST_STRING("intrinsic")))); + } + ++static int synthetic_foreign_user_build(uid_t foreign_uid, UserRecord **ret) { ++ assert(ret); ++ ++ if (!uid_is_valid(foreign_uid)) ++ return -ESRCH; ++ if (foreign_uid > 0xFFFF) ++ return -ESRCH; ++ ++ _cleanup_free_ char *un = NULL; ++ if (asprintf(&un, "foreign-" UID_FMT, foreign_uid) < 0) ++ return -ENOMEM; ++ ++ _cleanup_free_ char *rn = NULL; ++ if (asprintf(&rn, "Foreign System Image UID " UID_FMT, foreign_uid) < 0) ++ return -ENOMEM; ++ ++ return user_record_build( ++ ret, ++ SD_JSON_BUILD_OBJECT( ++ SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(un)), ++ SD_JSON_BUILD_PAIR("realName", SD_JSON_BUILD_STRING(rn)), ++ SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_UNSIGNED(FOREIGN_UID_BASE + foreign_uid)), ++ SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(FOREIGN_UID_BASE + foreign_uid)), ++ SD_JSON_BUILD_PAIR("shell", JSON_BUILD_CONST_STRING(NOLOGIN)), ++ SD_JSON_BUILD_PAIR("locked", SD_JSON_BUILD_BOOLEAN(true)), ++ SD_JSON_BUILD_PAIR("disposition", JSON_BUILD_CONST_STRING("foreign")))); ++} ++ ++static int user_name_foreign_extract_uid(const char *name, uid_t *ret_uid) { ++ int r; ++ ++ assert(name); ++ assert(ret_uid); ++ ++ /* Parses the inner UID from a user name of the foreign UID range, in the form "foreign-NNN". Returns ++ * > 0 if that worked, 0 if it didn't. */ ++ ++ const char *e = startswith(name, "foreign-"); ++ if (!e) ++ goto nomatch; ++ ++ uid_t uid; ++ r = parse_uid(e, &uid); ++ if (r < 0) ++ goto nomatch; ++ ++ if (uid > 0xFFFF) ++ goto nomatch; ++ ++ *ret_uid = uid; ++ return 1; ++ ++nomatch: ++ *ret_uid = UID_INVALID; ++ return 0; ++} ++ + int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; +@@ -658,7 +716,7 @@ int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { + } + } + +- if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_INTRINSIC)) { + if (streq(name, "root")) + return synthetic_root_user_build(ret); + +@@ -666,6 +724,16 @@ int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { + return synthetic_nobody_user_build(ret); + } + ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_FOREIGN)) { ++ uid_t foreign_uid; ++ r = user_name_foreign_extract_uid(name, &foreign_uid); ++ if (r < 0) ++ return r; ++ if (r > 0) ++ return synthetic_foreign_user_build(foreign_uid, ret); ++ r = -ESRCH; ++ } ++ + return r; + } + +@@ -708,7 +776,7 @@ int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) { + } + } + +- if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_INTRINSIC)) { + if (uid == 0) + return synthetic_root_user_build(ret); + +@@ -716,6 +784,9 @@ int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) { + return synthetic_nobody_user_build(ret); + } + ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_FOREIGN) && uid_is_foreign(uid)) ++ return synthetic_foreign_user_build(uid - FOREIGN_UID_BASE, ret); ++ + return r; + } + +@@ -751,6 +822,8 @@ int userdb_all(UserDBFlags flags, UserDBIterator **ret) { + log_debug_errno(r, "Failed to find user drop-ins, ignoring: %m"); + } + ++ /* Note that we do not enumerate the foreign users, since those would be just 64K of noise */ ++ + /* propagate IPC error, but only if there are no drop-ins */ + if (qr < 0 && + !iterator->nss_iterating && +@@ -889,6 +962,31 @@ static int synthetic_nobody_group_build(GroupRecord **ret) { + SD_JSON_BUILD_PAIR("disposition", JSON_BUILD_CONST_STRING("intrinsic")))); + } + ++static int synthetic_foreign_group_build(gid_t foreign_gid, GroupRecord **ret) { ++ assert(ret); ++ ++ if (!gid_is_valid(foreign_gid)) ++ return -ESRCH; ++ if (foreign_gid > 0xFFFF) ++ return -ESRCH; ++ ++ _cleanup_free_ char *gn = NULL; ++ if (asprintf(&gn, "foreign-" GID_FMT, foreign_gid) < 0) ++ return -ENOMEM; ++ ++ _cleanup_free_ char *d = NULL; ++ if (asprintf(&d, "Foreign System Image GID " GID_FMT, foreign_gid) < 0) ++ return -ENOMEM; ++ ++ return group_record_build( ++ ret, ++ SD_JSON_BUILD_OBJECT( ++ SD_JSON_BUILD_PAIR("groupName", SD_JSON_BUILD_STRING(gn)), ++ SD_JSON_BUILD_PAIR("description", SD_JSON_BUILD_STRING(d)), ++ SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(FOREIGN_UID_BASE + foreign_gid)), ++ SD_JSON_BUILD_PAIR("disposition", JSON_BUILD_CONST_STRING("foreign")))); ++} ++ + int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; +@@ -927,7 +1025,7 @@ int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { + } + } + +- if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_INTRINSIC)) { + if (streq(name, "root")) + return synthetic_root_group_build(ret); + +@@ -935,6 +1033,16 @@ int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { + return synthetic_nobody_group_build(ret); + } + ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_FOREIGN)) { ++ uid_t foreign_gid; ++ r = user_name_foreign_extract_uid(name, &foreign_gid); /* Same for UID + GID */ ++ if (r < 0) ++ return r; ++ if (r > 0) ++ return synthetic_foreign_group_build(foreign_gid, ret); ++ r = -ESRCH; ++ } ++ + return r; + } + +@@ -976,7 +1084,7 @@ int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) { + } + } + +- if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_INTRINSIC)) { + if (gid == 0) + return synthetic_root_group_build(ret); + +@@ -984,6 +1092,9 @@ int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) { + return synthetic_nobody_group_build(ret); + } + ++ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_FOREIGN) && gid_is_foreign(gid)) ++ return synthetic_foreign_group_build(gid - FOREIGN_UID_BASE, ret); ++ + return r; + } + +diff --git a/src/shared/userdb.h b/src/shared/userdb.h +index 75eb4b2dce..daf87fb5cf 100644 +--- a/src/shared/userdb.h ++++ b/src/shared/userdb.h +@@ -16,19 +16,20 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(UserDBIterator*, userdb_iterator_free); + + typedef enum UserDBFlags { + /* The main sources */ +- USERDB_EXCLUDE_NSS = 1 << 0, /* don't do client-side nor server-side NSS */ +- USERDB_EXCLUDE_VARLINK = 1 << 1, /* don't talk to any varlink services */ +- USERDB_EXCLUDE_DROPIN = 1 << 2, /* don't load drop-in user/group definitions */ ++ USERDB_EXCLUDE_NSS = 1 << 0, /* don't do client-side nor server-side NSS */ ++ USERDB_EXCLUDE_VARLINK = 1 << 1, /* don't talk to any varlink services */ ++ USERDB_EXCLUDE_DROPIN = 1 << 2, /* don't load drop-in user/group definitions */ + + /* Modifications */ +- USERDB_SUPPRESS_SHADOW = 1 << 3, /* don't do client-side shadow calls (server side might happen though) */ +- USERDB_EXCLUDE_DYNAMIC_USER = 1 << 4, /* exclude looking up in io.systemd.DynamicUser */ +- USERDB_AVOID_MULTIPLEXER = 1 << 5, /* exclude looking up via io.systemd.Multiplexer */ +- USERDB_DONT_SYNTHESIZE = 1 << 6, /* don't synthesize root/nobody */ ++ USERDB_SUPPRESS_SHADOW = 1 << 3, /* don't do client-side shadow calls (server side might happen though) */ ++ USERDB_EXCLUDE_DYNAMIC_USER = 1 << 4, /* exclude looking up in io.systemd.DynamicUser */ ++ USERDB_AVOID_MULTIPLEXER = 1 << 5, /* exclude looking up via io.systemd.Multiplexer */ ++ USERDB_DONT_SYNTHESIZE_INTRINSIC = 1 << 6, /* don't synthesize root/nobody */ ++ USERDB_DONT_SYNTHESIZE_FOREIGN = 1 << 7, /* don't synthesize foreign UID records */ + + /* Combinations */ +- USERDB_NSS_ONLY = USERDB_EXCLUDE_VARLINK|USERDB_EXCLUDE_DROPIN|USERDB_DONT_SYNTHESIZE, +- USERDB_DROPIN_ONLY = USERDB_EXCLUDE_NSS|USERDB_EXCLUDE_VARLINK|USERDB_DONT_SYNTHESIZE, ++ USERDB_NSS_ONLY = USERDB_EXCLUDE_VARLINK|USERDB_EXCLUDE_DROPIN|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, ++ USERDB_DROPIN_ONLY = USERDB_EXCLUDE_NSS|USERDB_EXCLUDE_VARLINK|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, + } UserDBFlags; + + /* Well-known errors we'll return here: +diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c +index f029c801a1..eac7636ace 100644 +--- a/src/userdb/userdbctl.c ++++ b/src/userdb/userdbctl.c +@@ -1381,7 +1381,7 @@ static int parse_argv(int argc, char *argv[]) { + break; + + case 'N': +- arg_userdb_flags |= USERDB_EXCLUDE_NSS|USERDB_DONT_SYNTHESIZE; ++ arg_userdb_flags |= USERDB_EXCLUDE_NSS|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN; + break; + + case ARG_WITH_NSS: +@@ -1413,7 +1413,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return r; + +- SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE, !r); ++ SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, !r); + break; + + case ARG_MULTIPLEXER: diff --git a/0581-dissect-add-new-shift-command.patch b/0581-dissect-add-new-shift-command.patch new file mode 100644 index 0000000..3c37e24 --- /dev/null +++ b/0581-dissect-add-new-shift-command.patch @@ -0,0 +1,199 @@ +From 9bda47d0ed718f7f7fe928dc0098ca13f139834e Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Tue, 12 Nov 2024 09:44:48 +0100 +Subject: [PATCH] dissect: add new --shift command + +(cherry picked from commit 55e4946f9ca75c35e87ff7f0c0d871e0d80e8ca0) + +Related: RHEL-143036 +--- + man/systemd-dissect.xml | 24 ++++++++++++++++++++ + src/dissect/dissect.c | 49 +++++++++++++++++++++++++++++++++++++++-- + 2 files changed, 71 insertions(+), 2 deletions(-) + +diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml +index b65a2c39f1..7ff1be31bd 100644 +--- a/man/systemd-dissect.xml ++++ b/man/systemd-dissect.xml +@@ -62,6 +62,9 @@ + + systemd-dissect OPTIONS --validate IMAGE + ++ ++ systemd-dissect OPTIONS --shift IMAGE UIDBASE ++ + + + +@@ -350,6 +353,27 @@ + + + ++ ++ ++ ++ Recursively iterates through all inodes of the specified image and shifts the UIDs ++ and GIDs the inodes are owned by into the specified UID range. Takes an image path and a UID base as ++ parameter. The UID base can be specified numerically (in which case it must be a multiple of 65536, ++ and either 0 or within the container or foreign UID range, as per Users, Groups, UIDs and GIDs on systemd Systems), or as ++ the symbolic identifier foreign which is shorthand to the foreign UID base. This ++ command is useful for preparing directory container images for unprivileged use. Note that this ++ command is intended for images that use the 16bit UIDs/GIDs range only, and it always ignores the ++ upper 16bit of the current UID/GID ownership, combining the lower 16 bit with the target UID ++ base. ++ ++ Use systemd-dissect --shift /some/container/tree foreign to shift a ++ container image into the foreign UID range, or systemd-dissect --shift /some/container/tree ++ 0 to shift it to host UID range. ++ ++ ++ ++ + + + +diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c +index 84e836027a..d3d3b3b195 100644 +--- a/src/dissect/dissect.c ++++ b/src/dissect/dissect.c +@@ -45,6 +45,7 @@ + #include "process-util.h" + #include "recurse-dir.h" + #include "sha256.h" ++#include "shift-uid.h" + #include "stat-util.h" + #include "string-util.h" + #include "strv.h" +@@ -68,6 +69,7 @@ static enum { + ACTION_DISCOVER, + ACTION_VALIDATE, + ACTION_MAKE_ARCHIVE, ++ ACTION_SHIFT, + } arg_action = ACTION_DISSECT; + static char *arg_image = NULL; + static char *arg_root = NULL; +@@ -95,6 +97,7 @@ static char *arg_loop_ref = NULL; + static ImagePolicy *arg_image_policy = NULL; + static bool arg_mtree_hash = true; + static bool arg_via_service = false; ++static uid_t arg_uid_base = UID_INVALID; + + STATIC_DESTRUCTOR_REGISTER(arg_image, freep); + STATIC_DESTRUCTOR_REGISTER(arg_root, freep); +@@ -127,6 +130,7 @@ static int help(void) { + "%1$s [OPTIONS...] --make-archive IMAGE [TARGET]\n" + "%1$s [OPTIONS...] --discover\n" + "%1$s [OPTIONS...] --validate IMAGE\n" ++ "%1$s [OPTIONS...] --shift IMAGE UIDBASE\n" + "\n%5$sDissect a Discoverable Disk Image (DDI).%6$s\n\n" + "%3$sOptions:%4$s\n" + " --no-pager Do not pipe output into a pager\n" +@@ -169,6 +173,7 @@ static int help(void) { + " --make-archive Convert the DDI to an archive file\n" + " --discover Discover DDIs in well known directories\n" + " --validate Validate image and image policy\n" ++ " --shift Shift UID range to selected base\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, +@@ -274,6 +279,7 @@ static int parse_argv(int argc, char *argv[]) { + ARG_VALIDATE, + ARG_MTREE_HASH, + ARG_MAKE_ARCHIVE, ++ ARG_SHIFT, + }; + + static const struct option options[] = { +@@ -307,6 +313,7 @@ static int parse_argv(int argc, char *argv[]) { + { "validate", no_argument, NULL, ARG_VALIDATE }, + { "mtree-hash", required_argument, NULL, ARG_MTREE_HASH }, + { "make-archive", no_argument, NULL, ARG_MAKE_ARCHIVE }, ++ { "shift", no_argument, NULL, ARG_SHIFT }, + {} + }; + +@@ -539,6 +546,9 @@ static int parse_argv(int argc, char *argv[]) { + arg_action = ACTION_MAKE_ARCHIVE; + break; + ++ case ARG_SHIFT: ++ arg_action = ACTION_SHIFT; ++ break; + case '?': + return -EINVAL; + +@@ -704,6 +714,33 @@ static int parse_argv(int argc, char *argv[]) { + arg_flags &= ~(DISSECT_IMAGE_PIN_PARTITION_DEVICES|DISSECT_IMAGE_ADD_PARTITION_DEVICES); + break; + ++ case ACTION_SHIFT: ++ if (optind + 2 != argc) ++ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), ++ "Expected an image path and a UID base as only argument."); ++ ++ r = parse_image_path_argument(argv[optind], &arg_root, &arg_image); ++ if (r < 0) ++ return r; ++ ++ if (streq(argv[optind + 1], "foreign")) ++ arg_uid_base = FOREIGN_UID_BASE; ++ else { ++ r = parse_uid(argv[optind + 1], &arg_uid_base); ++ if (r < 0) ++ return log_error_errno(r, "Failed to parse UID base: %s", argv[optind + 1]); ++ ++ if ((arg_uid_base & 0xFFFF) != 0) ++ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected UID base not a multiple of 64K: " UID_FMT, arg_uid_base); ++ if (arg_uid_base != 0 && ++ !uid_is_container(arg_uid_base) && ++ !uid_is_foreign(arg_uid_base)) ++ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected UID range is not in the container range, nor the foreign one, refusing."); ++ } ++ ++ arg_flags |= DISSECT_IMAGE_REQUIRE_ROOT; ++ break; ++ + default: + assert_not_reached(); + } +@@ -1417,7 +1454,7 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD + const char *root; + int r; + +- assert(IN_SET(arg_action, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_MAKE_ARCHIVE)); ++ assert(IN_SET(arg_action, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_MAKE_ARCHIVE, ACTION_SHIFT)); + + if (arg_image) { + assert(m); +@@ -1673,6 +1710,13 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD + #endif + } + ++ case ACTION_SHIFT: ++ r = path_patch_uid(root, arg_uid_base, 0x10000); ++ if (r < 0) ++ return log_error_errno(r, "Failed to shift UID base: %m"); ++ ++ return 0; ++ + default: + assert_not_reached(); + } +@@ -2082,7 +2126,7 @@ static int run(int argc, char *argv[]) { + else + r = loop_device_make_by_path(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); + if (r < 0) { +- if (!ERRNO_IS_PRIVILEGE(r) || !IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO)) ++ if (!ERRNO_IS_PRIVILEGE(r) || !IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_SHIFT)) + return log_error_errno(r, "Failed to set up loopback device for %s: %m", arg_image); + + log_debug_errno(r, "Lacking permissions to set up loopback block device for %s, using service: %m", arg_image); +@@ -2167,6 +2211,7 @@ static int run(int argc, char *argv[]) { + case ACTION_COPY_FROM: + case ACTION_COPY_TO: + case ACTION_MAKE_ARCHIVE: ++ case ACTION_SHIFT: + return action_list_or_mtree_or_copy_or_make_archive(m, d, userns_fd); + + case ACTION_WITH: diff --git a/0582-userdb-optionally-parse-numeric-UIDs-GIDs-where-a-us.patch b/0582-userdb-optionally-parse-numeric-UIDs-GIDs-where-a-us.patch new file mode 100644 index 0000000..c32849e --- /dev/null +++ b/0582-userdb-optionally-parse-numeric-UIDs-GIDs-where-a-us.patch @@ -0,0 +1,95 @@ +From 269ed9b1c475c27f7b96713d1927f89616c4a465 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Tue, 14 Jan 2025 18:28:27 +0100 +Subject: [PATCH] userdb: optionally parse numeric UIDs/GIDs where a username + is expected + +Let's move this logic from userdbctl into generic code so that we can +use it elsewhere. + +(cherry picked from commit cc7300fc5868f6d47f3f47076100b574bf54e58d) + +Related: RHEL-143036 +--- + src/shared/userdb.c | 14 ++++++++++++++ + src/shared/userdb.h | 2 ++ + src/userdb/userdbctl.c | 12 ++---------- + 3 files changed, 18 insertions(+), 10 deletions(-) + +diff --git a/src/shared/userdb.c b/src/shared/userdb.c +index 1154906fd5..a1da514884 100644 +--- a/src/shared/userdb.c ++++ b/src/shared/userdb.c +@@ -680,6 +680,13 @@ int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; + int r; + ++ if (FLAGS_SET(flags, USERDB_PARSE_NUMERIC)) { ++ uid_t uid; ++ ++ if (parse_uid(name, &uid) >= 0) ++ return userdb_by_uid(uid, flags, ret); ++ } ++ + if (!valid_user_group_name(name, VALID_USER_RELAX)) + return -EINVAL; + +@@ -992,6 +999,13 @@ int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; + int r; + ++ if (FLAGS_SET(flags, USERDB_PARSE_NUMERIC)) { ++ gid_t gid; ++ ++ if (parse_gid(name, &gid) >= 0) ++ return groupdb_by_gid(gid, flags, ret); ++ } ++ + if (!valid_user_group_name(name, VALID_USER_RELAX)) + return -EINVAL; + +diff --git a/src/shared/userdb.h b/src/shared/userdb.h +index daf87fb5cf..9bb47efbfe 100644 +--- a/src/shared/userdb.h ++++ b/src/shared/userdb.h +@@ -30,6 +30,8 @@ typedef enum UserDBFlags { + /* Combinations */ + USERDB_NSS_ONLY = USERDB_EXCLUDE_VARLINK|USERDB_EXCLUDE_DROPIN|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, + USERDB_DROPIN_ONLY = USERDB_EXCLUDE_NSS|USERDB_EXCLUDE_VARLINK|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, ++ ++ USERDB_PARSE_NUMERIC = 1 << 8, /* if a numeric UID is specified as name, parse it and look up by UID/GID */ + } UserDBFlags; + + /* Well-known errors we'll return here: +diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c +index eac7636ace..4525de3a46 100644 +--- a/src/userdb/userdbctl.c ++++ b/src/userdb/userdbctl.c +@@ -422,12 +422,8 @@ static int display_user(int argc, char *argv[], void *userdata) { + } else if (argc > 1 && !arg_fuzzy) + 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); ++ r = userdb_by_name(*i, arg_userdb_flags|USERDB_PARSE_NUMERIC, &ur); + if (r < 0) { + if (r == -ESRCH) + log_error_errno(r, "User %s does not exist.", *i); +@@ -774,12 +770,8 @@ static int display_group(int argc, char *argv[], void *userdata) { + } else if (argc > 1 && !arg_fuzzy) + 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); ++ r = groupdb_by_name(*i, arg_userdb_flags|USERDB_PARSE_NUMERIC, &gr); + if (r < 0) { + if (r == -ESRCH) + log_error_errno(r, "Group %s does not exist.", *i); diff --git a/0583-userdbd-separate-parameter-structure-of-GetMembershi.patch b/0583-userdbd-separate-parameter-structure-of-GetMembershi.patch new file mode 100644 index 0000000..c39a678 --- /dev/null +++ b/0583-userdbd-separate-parameter-structure-of-GetMembershi.patch @@ -0,0 +1,128 @@ +From 0bb32ee43fd41ed3914a9400bd2e2074e34f0f4b Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Tue, 21 Jan 2025 23:28:12 +0100 +Subject: [PATCH] userdbd: separate parameter structure of GetMemberships() + varlink call from the GetUserRecord() one + +The GetUserRecord() and GetMemberships() have quite different arguments, +hence let's use separate structures for both. + +This makes sense on its own, since it makes the structures a bit +smaller, but is also preparation for a later commit that adds a bunch of +new fields to one of the structs but not the other. + +(cherry picked from commit 45e587d822e60f643c0cf69584d7ef1ff03818a5) + +Related: RHEL-143036 +--- + src/userdb/userwork.c | 41 +++++++++++++++++++++++------------------ + 1 file changed, 23 insertions(+), 18 deletions(-) + +diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c +index dce60e2ebd..c8fef87326 100644 +--- a/src/userdb/userwork.c ++++ b/src/userdb/userwork.c +@@ -29,8 +29,7 @@ + #define LISTEN_IDLE_USEC (90 * USEC_PER_SEC) + + typedef struct LookupParameters { +- const char *user_name; +- const char *group_name; ++ const char *name; + union { + uid_t uid; + gid_t gid; +@@ -135,9 +134,9 @@ static int userdb_flags_from_service(sd_varlink *link, const char *service, User + static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { +- { "uid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, +- { "userName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, user_name), SD_JSON_RELAX }, +- { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, ++ { "uid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, ++ { "userName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, name), SD_JSON_RELAX }, ++ { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + +@@ -162,8 +161,8 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + + if (uid_is_valid(p.uid)) + r = userdb_by_uid(p.uid, userdb_flags, &hr); +- else if (p.user_name) +- r = userdb_by_name(p.user_name, userdb_flags, &hr); ++ else if (p.name) ++ r = userdb_by_name(p.name, userdb_flags, &hr); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *last = NULL; +@@ -215,7 +214,7 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + } + + if ((uid_is_valid(p.uid) && hr->uid != p.uid) || +- (p.user_name && !user_record_matches_user_name(hr, p.user_name))) ++ (p.name && !user_record_matches_user_name(hr, p.name))) + return sd_varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_user_json(link, hr, &v); +@@ -272,9 +271,9 @@ static int build_group_json(sd_varlink *link, GroupRecord *gr, sd_json_variant * + static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { +- { "gid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, +- { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, group_name), SD_JSON_RELAX }, +- { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, ++ { "gid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, ++ { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, name), SD_JSON_RELAX }, ++ { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + +@@ -298,8 +297,8 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + + if (gid_is_valid(p.gid)) + r = groupdb_by_gid(p.gid, userdb_flags, &g); +- else if (p.group_name) +- r = groupdb_by_name(p.group_name, userdb_flags, &g); ++ else if (p.name) ++ r = groupdb_by_name(p.name, userdb_flags, &g); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *last = NULL; +@@ -345,7 +344,7 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + } + + if ((uid_is_valid(p.gid) && g->gid != p.gid) || +- (p.group_name && !group_record_matches_group_name(g, p.group_name))) ++ (p.name && !group_record_matches_group_name(g, p.name))) + return sd_varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(link, g, &v); +@@ -355,17 +354,23 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + return sd_varlink_reply(link, v); + } + ++typedef struct MembershipLookupParameters { ++ const char *user_name; ++ const char *group_name; ++ const char *service; ++} MembershipLookupParameters; ++ + static int vl_method_get_memberships(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + static const sd_json_dispatch_field dispatch_table[] = { +- { "userName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, user_name), SD_JSON_RELAX }, +- { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, group_name), SD_JSON_RELAX }, +- { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, ++ { "userName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(MembershipLookupParameters, user_name), SD_JSON_RELAX }, ++ { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(MembershipLookupParameters, group_name), SD_JSON_RELAX }, ++ { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MembershipLookupParameters, service), 0 }, + {} + }; + + _cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL; + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; +- LookupParameters p = {}; ++ MembershipLookupParameters p = {}; + UserDBFlags userdb_flags; + int r; + diff --git a/0584-userdb-move-setting-of-service-varlink-parameter-int.patch b/0584-userdb-move-setting-of-service-varlink-parameter-int.patch new file mode 100644 index 0000000..39340bb --- /dev/null +++ b/0584-userdb-move-setting-of-service-varlink-parameter-int.patch @@ -0,0 +1,84 @@ +From fcf21e465c847084f198b4ca2659acc011410b81 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:34:16 +0100 +Subject: [PATCH] userdb: move setting of 'service' varlink parameter into + userdb_connect() + +We currently set this at two distinct places right before calling +userdb_connect(). let's do this inside of userdb_connect() instead, and +derive it directly from the socket path. + +This doesn't change behaviour but simplifies things a bit. + +(cherry picked from commit 6a43f0a73c33cb417a218de89858fde006214db7) + +Related: RHEL-143036 +--- + src/shared/userdb.c | 31 +++++++++++++++---------------- + 1 file changed, 15 insertions(+), 16 deletions(-) + +diff --git a/src/shared/userdb.c b/src/shared/userdb.c +index a1da514884..32f851b0f3 100644 +--- a/src/shared/userdb.c ++++ b/src/shared/userdb.c +@@ -384,10 +384,21 @@ static int userdb_connect( + if (r < 0) + return log_debug_errno(r, "Failed to bind reply callback: %m"); + ++ _cleanup_free_ char *service = NULL; ++ r = path_extract_filename(path, &service); ++ if (r < 0) ++ return log_debug_errno(r, "Failed to extract service name from socket path: %m"); ++ assert(r != O_DIRECTORY); ++ ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *patched_query = sd_json_variant_ref(query); ++ r = sd_json_variant_set_field_string(&patched_query, "service", service); ++ if (r < 0) ++ return log_debug_errno(r, "Unable to set service JSON field: %m"); ++ + if (more) +- r = sd_varlink_observe(vl, method, query); ++ r = sd_varlink_observe(vl, method, patched_query); + else +- r = sd_varlink_invoke(vl, method, query); ++ r = sd_varlink_invoke(vl, method, patched_query); + if (r < 0) + return log_debug_errno(r, "Failed to invoke varlink method: %m"); + +@@ -438,13 +449,7 @@ static int userdb_start_query( + if ((flags & (USERDB_AVOID_MULTIPLEXER|USERDB_EXCLUDE_DYNAMIC_USER|USERDB_EXCLUDE_NSS|USERDB_EXCLUDE_DROPIN|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN)) == 0 && + !strv_contains(except, "io.systemd.Multiplexer") && + (!only || strv_contains(only, "io.systemd.Multiplexer"))) { +- _cleanup_(sd_json_variant_unrefp) sd_json_variant *patched_query = sd_json_variant_ref(query); +- +- r = sd_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); ++ r = userdb_connect(iterator, "/run/systemd/userdb/io.systemd.Multiplexer", method, more, query); + if (r >= 0) { + iterator->nss_covered = true; /* The multiplexer does NSS */ + iterator->dropin_covered = true; /* It also handles drop-in stuff */ +@@ -461,7 +466,6 @@ static int userdb_start_query( + } + + FOREACH_DIRENT(de, d, return -errno) { +- _cleanup_(sd_json_variant_unrefp) sd_json_variant *patched_query = NULL; + _cleanup_free_ char *p = NULL; + bool is_nss, is_dropin; + +@@ -495,12 +499,7 @@ static int userdb_start_query( + if (!p) + return -ENOMEM; + +- patched_query = sd_json_variant_ref(query); +- r = sd_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); ++ r = userdb_connect(iterator, p, method, more, query); + if (is_nss && r >= 0) /* Turn off fallback NSS + dropin if we found the NSS/dropin service + * and could connect to it */ + iterator->nss_covered = true; diff --git a/0585-user-record-make-a-NULL-UserDBMatch-be-equivalent-to.patch b/0585-user-record-make-a-NULL-UserDBMatch-be-equivalent-to.patch new file mode 100644 index 0000000..80b4266 --- /dev/null +++ b/0585-user-record-make-a-NULL-UserDBMatch-be-equivalent-to.patch @@ -0,0 +1,44 @@ +From 10ab6be5caf19592ebf487ff8075d5e0c17825b4 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:24:17 +0100 +Subject: [PATCH] user-record: make a NULL UserDBMatch be equivalent to no + filtering + +(cherry picked from commit 83eabe102a91d4c4ac0080c31252cd60476af7ca) + +Related: RHEL-143036 +--- + src/shared/group-record.c | 4 +++- + src/shared/user-record.c | 4 +++- + 2 files changed, 6 insertions(+), 2 deletions(-) + +diff --git a/src/shared/group-record.c b/src/shared/group-record.c +index dcabdf634f..07bce7056d 100644 +--- a/src/shared/group-record.c ++++ b/src/shared/group-record.c +@@ -345,7 +345,9 @@ bool group_record_matches_group_name(const GroupRecord *g, const char *group_nam + + int group_record_match(GroupRecord *h, const UserDBMatch *match) { + assert(h); +- assert(match); ++ ++ if (!match) ++ return true; + + if (h->gid < match->gid_min || h->gid > match->gid_max) + return false; +diff --git a/src/shared/user-record.c b/src/shared/user-record.c +index 75605f248b..bbf5c71849 100644 +--- a/src/shared/user-record.c ++++ b/src/shared/user-record.c +@@ -2693,7 +2693,9 @@ bool user_name_fuzzy_match(const char *names[], size_t n_names, char **matches) + + int user_record_match(UserRecord *u, const UserDBMatch *match) { + assert(u); +- assert(match); ++ ++ if (!match) ++ return true; + + if (u->uid < match->uid_min || u->uid > match->uid_max) + return false; diff --git a/0586-sd-varlink-add-sd_varlink_get_description-call.patch b/0586-sd-varlink-add-sd_varlink_get_description-call.patch new file mode 100644 index 0000000..92c3eb5 --- /dev/null +++ b/0586-sd-varlink-add-sd_varlink_get_description-call.patch @@ -0,0 +1,175 @@ +From e627c841a9e701c675a984e4ff01556c72b44baa Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:13:59 +0100 +Subject: [PATCH] sd-varlink: add sd_varlink_get_description() call + +(cherry picked from commit 25c24619db829a589dbb4cd53ec3ccf9e2b28aa8) + +Related: RHEL-143036 +--- + man/rules/meson.build | 1 + + man/sd_varlink_set_description.xml | 101 +++++++++++++++++++++++++ + src/libsystemd/libsystemd.sym | 1 + + src/libsystemd/sd-varlink/sd-varlink.c | 6 ++ + src/systemd/sd-varlink.h | 1 + + 5 files changed, 110 insertions(+) + create mode 100644 man/sd_varlink_set_description.xml + +diff --git a/man/rules/meson.build b/man/rules/meson.build +index e76cb0223b..a4767eae59 100644 +--- a/man/rules/meson.build ++++ b/man/rules/meson.build +@@ -889,6 +889,7 @@ manpages = [ + 'sd_uid_get_sessions', + 'sd_uid_is_on_seat'], + 'HAVE_PAM'], ++ ['sd_varlink_set_description', '3', ['sd_varlink_get_description'], ''], + ['sd_watchdog_enabled', '3', [], ''], + ['shutdown', '8', [], ''], + ['smbios-type-11', '7', [], ''], +diff --git a/man/sd_varlink_set_description.xml b/man/sd_varlink_set_description.xml +new file mode 100644 +index 0000000000..e64cd8a5ac +--- /dev/null ++++ b/man/sd_varlink_set_description.xml +@@ -0,0 +1,101 @@ ++ ++ ++ ++ ++ ++ ++ ++ sd_varlink_set_description ++ systemd ++ ++ ++ ++ sd_varlink_set_description ++ 3 ++ ++ ++ ++ sd_varlink_set_description ++ sd_varlink_get_description ++ ++ Set or query description of a Varlink connection object ++ ++ ++ ++ ++ #include <systemd/sd-link.h> ++ ++ ++ int sd_varlink_set_description ++ sd_varlink *link ++ const char *description ++ ++ ++ ++ const char* sd_varlink_get_description ++ sd_varlink *link ++ ++ ++ ++ ++ ++ Description ++ ++ sd_varlink_set_description() sets the description string that is used in ++ logging to the specified string. The string is copied internally and freed when the Varlink connection ++ object is deallocated. The description argument may be NULL, ++ in which case the description is unset. ++ ++ sd_varlink_get_description() returns a description string for the specified ++ Varlink connection. This string may have been previously set with ++ sd_varlink_set_description(). If not set this way, a default string or ++ NULL may be returned, depending how the connection was allocated and set up. ++ ++ ++ ++ Return Value ++ ++ On success, sd_varlink_set_description() returns a non-negative integer. On ++ failure, it returns a negative errno-style error code. sd_varlink_get_description() ++ returns either NULL or a pointer to the description string. ++ ++ ++ Errors ++ ++ Returned errors may indicate the following problems: ++ ++ ++ ++ -EINVAL ++ ++ An argument is invalid. ++ ++ ++ ++ -ENOMEM ++ ++ Memory allocation failed. ++ ++ ++ ++ ++ ++ ++ ++ ++ History ++ sd_varlink_set_description() was added in version 257. ++ sd_varlink_get_description() was added in version 258. ++ ++ ++ ++ See Also ++ ++ ++ systemd1 ++ sd-varlink3 ++ ++ ++ ++ +diff --git a/src/libsystemd/libsystemd.sym b/src/libsystemd/libsystemd.sym +index 4dbe4e3c76..00ebc710be 100644 +--- a/src/libsystemd/libsystemd.sym ++++ b/src/libsystemd/libsystemd.sym +@@ -1063,4 +1063,5 @@ global: + LIBSYSTEMD_258 { + global: + sd_varlink_get_current_method; ++ sd_varlink_get_description; + } LIBSYSTEMD_257; +diff --git a/src/libsystemd/sd-varlink/sd-varlink.c b/src/libsystemd/sd-varlink/sd-varlink.c +index 5f3b17199d..0b9bf48363 100644 +--- a/src/libsystemd/sd-varlink/sd-varlink.c ++++ b/src/libsystemd/sd-varlink/sd-varlink.c +@@ -2879,6 +2879,12 @@ _public_ int sd_varlink_set_description(sd_varlink *v, const char *description) + return free_and_strdup(&v->description, description); + } + ++_public_ const char* sd_varlink_get_description(sd_varlink *v) { ++ assert_return(v, NULL); ++ ++ return v->description; ++} ++ + static int io_callback(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + sd_varlink *v = ASSERT_PTR(userdata); + +diff --git a/src/systemd/sd-varlink.h b/src/systemd/sd-varlink.h +index 9401d417b2..7d4da21b0f 100644 +--- a/src/systemd/sd-varlink.h ++++ b/src/systemd/sd-varlink.h +@@ -210,6 +210,7 @@ int sd_varlink_set_relative_timeout(sd_varlink *v, uint64_t usec); + sd_varlink_server* sd_varlink_get_server(sd_varlink *v); + + int sd_varlink_set_description(sd_varlink *v, const char *d); ++const char* sd_varlink_get_description(sd_varlink *v); + + /* Automatically mark the parameters part of incoming messages as security sensitive */ + int sd_varlink_set_input_sensitive(sd_varlink *v); diff --git a/0587-user-record-add-helper-for-dispatching-a-disposition.patch b/0587-user-record-add-helper-for-dispatching-a-disposition.patch new file mode 100644 index 0000000..5af9387 --- /dev/null +++ b/0587-user-record-add-helper-for-dispatching-a-disposition.patch @@ -0,0 +1,78 @@ +From bcb78a95d52b54a3efd662a9e04f5a6f98f8d36e Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Tue, 21 Jan 2025 12:53:02 +0100 +Subject: [PATCH] user-record: add helper for dispatching a disposition mask + +(cherry picked from commit be093d457ffb9b8448c184fd83d49cfd98b91c96) + +Related: RHEL-143036 +--- + src/shared/user-record.c | 34 ++++++++++++++++++++++++++++++++++ + src/shared/user-record.h | 2 ++ + 2 files changed, 36 insertions(+) + +diff --git a/src/shared/user-record.c b/src/shared/user-record.c +index bbf5c71849..e984f6d24c 100644 +--- a/src/shared/user-record.c ++++ b/src/shared/user-record.c +@@ -2,6 +2,7 @@ + + #include + ++#include "bitfield.h" + #include "cap-list.h" + #include "cgroup-util.h" + #include "dns-domain.h" +@@ -2724,6 +2725,39 @@ int user_record_match(UserRecord *u, const UserDBMatch *match) { + return true; + } + ++int json_dispatch_dispositions_mask(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { ++ uint64_t *mask = ASSERT_PTR(userdata); ++ ++ if (sd_json_variant_is_null(variant)) { ++ *mask = UINT64_MAX; ++ return 0; ++ } ++ ++ if (!sd_json_variant_is_array(variant)) ++ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); ++ ++ uint64_t m = 0; ++ for (size_t i = 0; i < sd_json_variant_elements(variant); i++) { ++ sd_json_variant *e; ++ const char *a; ++ ++ e = sd_json_variant_by_index(variant, i); ++ if (!sd_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 = sd_json_variant_string(e)); ++ ++ UserDisposition d = user_disposition_from_string(a); ++ if (d < 0) ++ return json_log(e, flags, d, "JSON field '%s' contains an invalid user disposition type: %s", strna(name), a); ++ ++ m |= INDEX_TO_MASK(uint64_t, d); ++ } ++ ++ *mask = m; ++ return 0; ++} ++ + static const char* const user_storage_table[_USER_STORAGE_MAX] = { + [USER_CLASSIC] = "classic", + [USER_LUKS] = "luks", +diff --git a/src/shared/user-record.h b/src/shared/user-record.h +index d80a46130a..a188dc0d0b 100644 +--- a/src/shared/user-record.h ++++ b/src/shared/user-record.h +@@ -493,6 +493,8 @@ int user_record_match(UserRecord *u, const UserDBMatch *match); + + bool user_record_matches_user_name(const UserRecord *u, const char *username); + ++int json_dispatch_dispositions_mask(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); ++ + const char* user_storage_to_string(UserStorage t) _const_; + UserStorage user_storage_from_string(const char *s) _pure_; + diff --git a/0588-user-record-rename-USER_DISPOSITION_MASK_MAX-USER_DI.patch b/0588-user-record-rename-USER_DISPOSITION_MASK_MAX-USER_DI.patch new file mode 100644 index 0000000..72a0628 --- /dev/null +++ b/0588-user-record-rename-USER_DISPOSITION_MASK_MAX-USER_DI.patch @@ -0,0 +1,45 @@ +From a02347043677bb5218bdd4483fefae891da352d8 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Fri, 24 Jan 2025 22:15:29 +0100 +Subject: [PATCH] =?UTF-8?q?user-record:=20rename=20USER=5FDISPOSITION=5FMA?= + =?UTF-8?q?SK=5FMAX=20=E2=86=92=20USER=5FDISPOSITION=5FMASK=5FALL?= +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +On request by yuwata. + +(cherry picked from commit 27cce1f1efb564f30563eb0e3d37264bed3b093c) + +Related: RHEL-143036 +--- + src/shared/user-record.h | 2 +- + src/userdb/userdbctl.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/shared/user-record.h b/src/shared/user-record.h +index a188dc0d0b..5ecc443d22 100644 +--- a/src/shared/user-record.h ++++ b/src/shared/user-record.h +@@ -486,7 +486,7 @@ typedef struct UserDBMatch { + }; + } UserDBMatch; + +-#define USER_DISPOSITION_MASK_MAX ((UINT64_C(1) << _USER_DISPOSITION_MAX) - UINT64_C(1)) ++#define USER_DISPOSITION_MASK_ALL ((UINT64_C(1) << _USER_DISPOSITION_MAX) - UINT64_C(1)) + + bool user_name_fuzzy_match(const char *names[], size_t n_names, char **matches); + int user_record_match(UserRecord *u, const UserDBMatch *match); +diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c +index 4525de3a46..2c3b274828 100644 +--- a/src/userdb/userdbctl.c ++++ b/src/userdb/userdbctl.c +@@ -1510,7 +1510,7 @@ static int parse_argv(int argc, char *argv[]) { + + /* If not mask was specified, use the all bits on mask */ + if (arg_disposition_mask == UINT64_MAX) +- arg_disposition_mask = USER_DISPOSITION_MASK_MAX; ++ arg_disposition_mask = USER_DISPOSITION_MASK_ALL; + + if (arg_from_file) + arg_boundaries = false; diff --git a/0589-user-record-add-some-helpers-for-working-with-UserDB.patch b/0589-user-record-add-some-helpers-for-working-with-UserDB.patch new file mode 100644 index 0000000..1e5657f --- /dev/null +++ b/0589-user-record-add-some-helpers-for-working-with-UserDB.patch @@ -0,0 +1,55 @@ +From 14dbb7e8f0a7640bbffe6d8152e4a998a86297d8 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:26:53 +0100 +Subject: [PATCH] user-record: add some helpers for working with UserDBMatch + +(cherry picked from commit d6db229ffc94981339a6a6e41c2172b0f962c6f6) + +Related: RHEL-143036 +--- + src/shared/user-record.h | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/src/shared/user-record.h b/src/shared/user-record.h +index 5ecc443d22..91402f38b9 100644 +--- a/src/shared/user-record.h ++++ b/src/shared/user-record.h +@@ -9,7 +9,9 @@ + + #include "hashmap.h" + #include "missing_resource.h" ++#include "strv.h" + #include "time-util.h" ++#include "user-util.h" + + typedef enum UserDisposition { + USER_INTRINSIC, /* root and nobody */ +@@ -488,6 +490,28 @@ typedef struct UserDBMatch { + + #define USER_DISPOSITION_MASK_ALL ((UINT64_C(1) << _USER_DISPOSITION_MAX) - UINT64_C(1)) + ++#define USERDB_MATCH_NULL \ ++ (UserDBMatch) { \ ++ .disposition_mask = USER_DISPOSITION_MASK_ALL, \ ++ .uid_min = 0, \ ++ .uid_max = UID_INVALID-1, \ ++ } ++ ++static inline bool userdb_match_is_set(const UserDBMatch *match) { ++ if (!match) ++ return false; ++ ++ return !strv_isempty(match->fuzzy_names) || ++ !FLAGS_SET(match->disposition_mask, USER_DISPOSITION_MASK_ALL) || ++ match->uid_min > 0 || ++ match->uid_max < UID_INVALID-1; ++} ++ ++static inline void userdb_match_done(UserDBMatch *match) { ++ assert(match); ++ strv_free(match->fuzzy_names); ++} ++ + bool user_name_fuzzy_match(const char *names[], size_t n_names, char **matches); + int user_record_match(UserRecord *u, const UserDBMatch *match); + diff --git a/0590-varlink-add-new-calls-for-server-side-user-record-fi.patch b/0590-varlink-add-new-calls-for-server-side-user-record-fi.patch new file mode 100644 index 0000000..0576182 --- /dev/null +++ b/0590-varlink-add-new-calls-for-server-side-user-record-fi.patch @@ -0,0 +1,143 @@ +From 56df03dca7a70c341ec30fec489e2ee32531906b Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:27:14 +0100 +Subject: [PATCH] varlink: add new calls for server-side user record filtering + to varlink IDL + to spec + +This is preparation for adding server side filtering to the userdb +logic: it adds some fields for this to the userdb varlink API. This only +adds the IDL for it, no client will use it for now, no server implement +it. That's added in later commits. + +(cherry picked from commit 1ff1e0e01b8cddea89aad88671069527b981a9a2) + +Related: RHEL-143036 +--- + docs/USER_GROUP_API.md | 43 ++++++++++++++++++++ + src/shared/varlink-io.systemd.UserDatabase.c | 19 +++++++++ + 2 files changed, 62 insertions(+) + +diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md +index 033c2a1b98..0beb3bb912 100644 +--- a/docs/USER_GROUP_API.md ++++ b/docs/USER_GROUP_API.md +@@ -161,6 +161,10 @@ interface io.systemd.UserDatabase + method GetUserRecord( + uid : ?int, + userName : ?string, ++ fuzzyNames: ?[]string, ++ dispositionMask: ?[]string, ++ uidMin: ?int, ++ uidMax: ?int, + service : string + ) -> ( + record : object, +@@ -170,6 +174,10 @@ method GetUserRecord( + method GetGroupRecord( + gid : ?int, + groupName : ?string, ++ fuzzyNames: ?[]string, ++ dispositionMask: ?[]string, ++ gidMin: ?int, ++ gidMax: ?int, + service : string + ) -> ( + record : object, +@@ -189,6 +197,7 @@ error NoRecordFound() + error BadService() + error ServiceNotAvailable() + error ConflictingRecordFound() ++error NonMatchingRecordFound() + error EnumerationNotSupported() + ``` + +@@ -203,6 +212,40 @@ 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 `fuzzyNames`, `dispositionMask`, `uidMin`, `uidMax` fields permit ++*additional* filtering of the returned set of user records. The `fuzzyNames` ++parameter shall be one or more strings that shall be searched for in "fuzzy" ++way. What specifically this means is left for the backend to decide, but ++typically this should result in substring or string proximity matching of the ++primary user name, the real name of the record and possibly other fields that ++carry identifying information for the user. The `dispositionMask` field shall ++be one of more user record `disposition` strings. If specified only user ++records matching one of the specified dispositions should be enumerated. The ++`uidMin` and `uidMax` fields specify a minimum and maximum value for the UID of ++returned records. Inline searching for `uid` and `userName` support for ++filtering with these four additional parameters is optional, and clients are ++expected to be able to do client-side filtering in case the parameters are not ++supported by a service. The service should return the usual `InvalidParameter` ++error for the relevant parameter if one is passed and it does not support ++it. If a request is made specifying `uid` or `userName` and a suitable record ++is found, but the specified filter via `fuzzyNames`, `dispositionMask`, ++`uidMin`, or `uidMax` does not match, a `NonMatchingRecordFound` error should ++be returned. ++ ++Or to say this differently: the *primary search keys* are ++`userName`/`groupName` and `uid`/`gid` and the *secondary search filters* are ++`fuzzyNames`, `dispositionMask`, `uidMin`, `uidMax`. If no entry matching ++either of the primary search keys are found `NoRecordFound()` is returned. If ++one is found that matches one but not the other primary search key ++`ConflictingRecordFound()` is returned. If an entry is found that matches the ++primary search key, but not the secondary match filters ++`NonMatchingRecordFound()` is returned. Finally, if an entry is found that ++matches both the primary search keys and the secondary search filters, they are ++returned as successful response. Note that both the primary search keys and the ++secondary search filters are optional, it is possible to use both, use one of ++the two, or the other of the two, or neither (the latter for a complete dump of ++the database). ++ + 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 +diff --git a/src/shared/varlink-io.systemd.UserDatabase.c b/src/shared/varlink-io.systemd.UserDatabase.c +index a9484484e3..b2157298af 100644 +--- a/src/shared/varlink-io.systemd.UserDatabase.c ++++ b/src/shared/varlink-io.systemd.UserDatabase.c +@@ -7,6 +7,14 @@ static SD_VARLINK_DEFINE_METHOD_FULL( + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_DEFINE_INPUT(uid, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_DEFINE_INPUT(userName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), ++ SD_VARLINK_FIELD_COMMENT("Names to search for in a fuzzy fashion."), ++ SD_VARLINK_DEFINE_INPUT(fuzzyNames, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), ++ SD_VARLINK_FIELD_COMMENT("User dispositions to limit search by."), ++ SD_VARLINK_DEFINE_INPUT(dispositionMask, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), ++ SD_VARLINK_FIELD_COMMENT("Minimum UID to restrict search too."), ++ SD_VARLINK_DEFINE_INPUT(uidMin, SD_VARLINK_INT, SD_VARLINK_NULLABLE), ++ SD_VARLINK_FIELD_COMMENT("Maximum UID to restrict search too."), ++ SD_VARLINK_DEFINE_INPUT(uidMax, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_DEFINE_INPUT(service, SD_VARLINK_STRING, 0), + SD_VARLINK_DEFINE_OUTPUT(record, SD_VARLINK_OBJECT, 0), + SD_VARLINK_DEFINE_OUTPUT(incomplete, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); +@@ -16,6 +24,14 @@ static SD_VARLINK_DEFINE_METHOD_FULL( + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_DEFINE_INPUT(gid, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_DEFINE_INPUT(groupName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), ++ SD_VARLINK_FIELD_COMMENT("Additional names to search for in a fuzzy fashion."), ++ SD_VARLINK_DEFINE_INPUT(fuzzyNames, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), ++ SD_VARLINK_FIELD_COMMENT("Group dispositions to limit search by."), ++ SD_VARLINK_DEFINE_INPUT(dispositionMask, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), ++ SD_VARLINK_FIELD_COMMENT("Minimum GID to restrict search too."), ++ SD_VARLINK_DEFINE_INPUT(gidMin, SD_VARLINK_INT, SD_VARLINK_NULLABLE), ++ SD_VARLINK_FIELD_COMMENT("Maximum GID to restrict search too."), ++ SD_VARLINK_DEFINE_INPUT(gidMax, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_DEFINE_INPUT(service, SD_VARLINK_STRING, 0), + SD_VARLINK_DEFINE_OUTPUT(record, SD_VARLINK_OBJECT, 0), + SD_VARLINK_DEFINE_OUTPUT(incomplete, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); +@@ -34,6 +50,7 @@ static SD_VARLINK_DEFINE_ERROR(BadService); + static SD_VARLINK_DEFINE_ERROR(ServiceNotAvailable); + static SD_VARLINK_DEFINE_ERROR(ConflictingRecordFound); + static SD_VARLINK_DEFINE_ERROR(EnumerationNotSupported); ++static SD_VARLINK_DEFINE_ERROR(NonMatchingRecordFound); + + /* As per https://systemd.io/USER_GROUP_API/ */ + SD_VARLINK_DEFINE_INTERFACE( +@@ -46,4 +63,6 @@ SD_VARLINK_DEFINE_INTERFACE( + &vl_error_BadService, + &vl_error_ServiceNotAvailable, + &vl_error_ConflictingRecordFound, ++ SD_VARLINK_SYMBOL_COMMENT("Error indicating that there's a user record matching the primary UID/GID or user/group, but that doesn't match the additional specified matches."), ++ &vl_error_NonMatchingRecordFound, + &vl_error_EnumerationNotSupported); diff --git a/0591-userdb-move-UserDBMatch-handling-from-userdbctl-into.patch b/0591-userdb-move-UserDBMatch-handling-from-userdbctl-into.patch new file mode 100644 index 0000000..82fa8d6 --- /dev/null +++ b/0591-userdb-move-UserDBMatch-handling-from-userdbctl-into.patch @@ -0,0 +1,1361 @@ +From 3954be8a1a0b0eb2ad42ee00d2263cab5b0c191d Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:40:47 +0100 +Subject: [PATCH] userdb: move UserDBMatch handling from userdbctl into generic + userdb code to allow it to be done server side + +This moves around the UserDBMatch handling, moves it out of userdbctl +and into generic userdb code, so that it can be passed to the server +side, to allow server side filtering. + +This is preparation for one day allowing complex software to do such +filtering server side, and thus reducing the necessary traffic. + +Right now no server side actually knows this, hence care is taken to +downgrade to the userdb varlink API as it was in v257 in case the new +options are not understood. This retains compatibility with any +implementation hence. + +(cherry picked from commit 7419291670dd4066594350cce585031f60bc4f0a) + +Related: RHEL-143036 +--- + src/home/homectl.c | 14 +- + src/login/logind-core.c | 4 +- + src/login/pam_systemd.c | 2 +- + src/nspawn/nspawn-bind-user.c | 4 +- + src/nsresourced/nsresourcework.c | 8 +- + src/nss-systemd/nss-systemd.c | 18 +- + src/nss-systemd/userdb-glue.c | 12 +- + src/shared/user-record-show.c | 2 +- + src/shared/userdb.c | 507 ++++++++++++++++++++++++++----- + src/shared/userdb.h | 18 +- + src/userdb/userdbctl.c | 55 ++-- + src/userdb/userwork.c | 16 +- + 12 files changed, 503 insertions(+), 157 deletions(-) + +diff --git a/src/home/homectl.c b/src/home/homectl.c +index 08136cda3f..46a2a4c806 100644 +--- a/src/home/homectl.c ++++ b/src/home/homectl.c +@@ -2395,14 +2395,14 @@ static int has_regular_user(void) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + +- r = userdb_all(USERDB_SUPPRESS_SHADOW, &iterator); ++ r = userdb_all(/* match= */ NULL, USERDB_SUPPRESS_SHADOW, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to create user enumerator: %m"); + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + +- r = userdb_iterator_get(iterator, &ur); ++ r = userdb_iterator_get(iterator, /* match= */ NULL, &ur); + if (r == -ESRCH) + break; + if (r < 0) +@@ -2422,7 +2422,7 @@ static int acquire_group_list(char ***ret) { + + assert(ret); + +- r = groupdb_all(USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, &iterator); ++ r = groupdb_all(/* match= */ NULL, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, &iterator); + if (r == -ENOLINK) + log_debug_errno(r, "No groups found. (Didn't check via Varlink.)"); + else if (r == -ESRCH) +@@ -2433,7 +2433,7 @@ static int acquire_group_list(char ***ret) { + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + +- r = groupdb_iterator_get(iterator, &gr); ++ r = groupdb_iterator_get(iterator, /* match= */ NULL, &gr); + if (r == -ESRCH) + break; + if (r < 0) +@@ -2447,7 +2447,7 @@ static int acquire_group_list(char ***ret) { + + /* Filter groups here that belong to a specific user, and are named like them */ + +- r = userdb_by_name(gr->group_name, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, &ur); ++ r = userdb_by_name(gr->group_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, &ur); + if (r < 0 && r != -ESRCH) + return log_debug_errno(r, "Failed to check if matching user exists for group '%s': %m", gr->group_name); + +@@ -2498,7 +2498,7 @@ static int create_interactively(void) { + continue; + } + +- r = userdb_by_name(username, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL); ++ r = userdb_by_name(username, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, /* ret= */ NULL); + if (r == -ESRCH) + break; + if (r < 0) +@@ -2568,7 +2568,7 @@ static int create_interactively(void) { + continue; + } + +- r = groupdb_by_name(s, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, /*ret=*/ NULL); ++ r = groupdb_by_name(s, /* match= */ NULL, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, /*ret=*/ NULL); + if (r == -ESRCH) { + log_notice("Specified auxiliary group does not exist, try again: %s", s); + continue; +diff --git a/src/login/logind-core.c b/src/login/logind-core.c +index 197b1e8cd2..4629e4aa50 100644 +--- a/src/login/logind-core.c ++++ b/src/login/logind-core.c +@@ -190,7 +190,7 @@ int manager_add_user_by_name( + assert(m); + assert(name); + +- r = userdb_by_name(name, USERDB_SUPPRESS_SHADOW, &ur); ++ r = userdb_by_name(name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &ur); + if (r < 0) + return r; + +@@ -208,7 +208,7 @@ int manager_add_user_by_uid( + assert(m); + assert(uid_is_valid(uid)); + +- r = userdb_by_uid(uid, USERDB_SUPPRESS_SHADOW, &ur); ++ r = userdb_by_uid(uid, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &ur); + if (r < 0) + return r; + +diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c +index ab50137e4e..c2f074ca0a 100644 +--- a/src/login/pam_systemd.c ++++ b/src/login/pam_systemd.c +@@ -219,7 +219,7 @@ static int acquire_user_record( + _cleanup_free_ char *formatted = NULL; + + /* Request the record ourselves */ +- r = userdb_by_name(username, /* flags= */ 0, &ur); ++ r = userdb_by_name(username, /* match= */ NULL, /* flags= */ 0, &ur); + if (r < 0) { + pam_syslog_errno(handle, LOG_ERR, r, "Failed to get user record: %m"); + return PAM_USER_UNKNOWN; +diff --git a/src/nspawn/nspawn-bind-user.c b/src/nspawn/nspawn-bind-user.c +index 749accdce8..8964de22a1 100644 +--- a/src/nspawn/nspawn-bind-user.c ++++ b/src/nspawn/nspawn-bind-user.c +@@ -231,7 +231,7 @@ int bind_user_prepare( + _cleanup_(group_record_unrefp) GroupRecord *g = NULL, *cg = NULL; + _cleanup_free_ char *sm = NULL, *sd = NULL; + +- r = userdb_by_name(*n, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &u); ++ r = userdb_by_name(*n, /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &u); + if (r < 0) + return log_error_errno(r, "Failed to resolve user '%s': %m", *n); + +@@ -252,7 +252,7 @@ int bind_user_prepare( + if (u->uid >= uid_shift && u->uid < uid_shift + uid_range) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID of user '%s' to map is already in container UID range, refusing.", u->user_name); + +- r = groupdb_by_gid(u->gid, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &g); ++ r = groupdb_by_gid(u->gid, /* match= */ NULL, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, &g); + if (r < 0) + return log_error_errno(r, "Failed to resolve group of user '%s': %m", u->user_name); + +diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c +index 21fc2d9088..c611f01507 100644 +--- a/src/nsresourced/nsresourcework.c ++++ b/src/nsresourced/nsresourcework.c +@@ -361,13 +361,13 @@ static int uid_is_available( + if (r > 0) + return false; + +- r = userdb_by_uid(candidate, USERDB_AVOID_MULTIPLEXER, NULL); ++ r = userdb_by_uid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret_record= */ NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + +- r = groupdb_by_gid(candidate, USERDB_AVOID_MULTIPLEXER, NULL); ++ r = groupdb_by_gid(candidate, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret_record= */ NULL); + if (r >= 0) + return false; + if (r != -ESRCH) +@@ -398,13 +398,13 @@ static int name_is_available( + if (!user_name) + return -ENOMEM; + +- r = userdb_by_name(user_name, USERDB_AVOID_MULTIPLEXER, NULL); ++ r = userdb_by_name(user_name, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret_record= */ NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + +- r = groupdb_by_name(user_name, USERDB_AVOID_MULTIPLEXER, NULL); ++ r = groupdb_by_name(user_name, /* match= */ NULL, USERDB_AVOID_MULTIPLEXER, /* ret_record= */ NULL); + if (r >= 0) + return false; + if (r != -ESRCH) +diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c +index d686d920fc..a72d8b8d0d 100644 +--- a/src/nss-systemd/nss-systemd.c ++++ b/src/nss-systemd/nss-systemd.c +@@ -615,7 +615,7 @@ enum nss_status _nss_systemd_setpwent(int stayopen) { + * (think: LDAP/NIS type situations), and our synthesizing of root/nobody is a robustness fallback + * only, which matters for getpwnam()/getpwuid() primarily, which are the main NSS entrypoints to the + * user database. */ +- r = userdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getpwent_data.iterator); ++ r = userdb_all(/* match= */ NULL, nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getpwent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +@@ -635,7 +635,7 @@ enum nss_status _nss_systemd_setgrent(int stayopen) { + getgrent_data.by_membership = false; + + /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE_INTRINSIC here */ +- r = groupdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getgrent_data.iterator); ++ r = groupdb_all(/* match= */ NULL, nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getgrent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +@@ -655,7 +655,7 @@ enum nss_status _nss_systemd_setspent(int stayopen) { + getspent_data.by_membership = false; + + /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE_INTRINSIC here */ +- r = userdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getspent_data.iterator); ++ r = userdb_all(/* match= */ NULL, nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getspent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +@@ -675,7 +675,7 @@ enum nss_status _nss_systemd_setsgent(int stayopen) { + getsgent_data.by_membership = false; + + /* See _nss_systemd_setpwent() for an explanation why we use USERDB_DONT_SYNTHESIZE here */ +- r = groupdb_all(nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getsgent_data.iterator); ++ r = groupdb_all(/* match= */ NULL, nss_glue_userdb_flags() | USERDB_DONT_SYNTHESIZE_INTRINSIC | USERDB_DONT_SYNTHESIZE_FOREIGN, &getsgent_data.iterator); + return r < 0 ? NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + } + +@@ -705,7 +705,7 @@ enum nss_status _nss_systemd_getpwent_r( + return NSS_STATUS_UNAVAIL; + } + +- r = userdb_iterator_get(getpwent_data.iterator, &ur); ++ r = userdb_iterator_get(getpwent_data.iterator, /* match= */ NULL, &ur); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { +@@ -752,7 +752,7 @@ enum nss_status _nss_systemd_getgrent_r( + } + + if (!getgrent_data.by_membership) { +- r = groupdb_iterator_get(getgrent_data.iterator, &gr); ++ r = groupdb_iterator_get(getgrent_data.iterator, /* match= */ NULL, &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 +@@ -878,7 +878,7 @@ enum nss_status _nss_systemd_getspent_r( + } + + for (;;) { +- r = userdb_iterator_get(getspent_data.iterator, &ur); ++ r = userdb_iterator_get(getspent_data.iterator, /* match= */ NULL, &ur); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { +@@ -930,7 +930,7 @@ enum nss_status _nss_systemd_getsgent_r( + } + + for (;;) { +- r = groupdb_iterator_get(getsgent_data.iterator, &gr); ++ r = groupdb_iterator_get(getsgent_data.iterator, /* match= */ NULL, &gr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { +@@ -1010,7 +1010,7 @@ enum nss_status _nss_systemd_initgroups_dyn( + /* 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. */ + +- r = groupdb_by_name(group_name, (nss_glue_userdb_flags() & ~USERDB_EXCLUDE_NSS) | USERDB_SUPPRESS_SHADOW, &g); ++ r = groupdb_by_name(group_name, /* match= */ NULL, (nss_glue_userdb_flags() & ~USERDB_EXCLUDE_NSS) | USERDB_SUPPRESS_SHADOW, &g); + if (r == -ESRCH) + continue; + if (r < 0) { +diff --git a/src/nss-systemd/userdb-glue.c b/src/nss-systemd/userdb-glue.c +index 61311d8db3..4f96e9b90f 100644 +--- a/src/nss-systemd/userdb-glue.c ++++ b/src/nss-systemd/userdb-glue.c +@@ -81,7 +81,7 @@ enum nss_status userdb_getpwnam( + if (_nss_systemd_is_blocked()) + return NSS_STATUS_NOTFOUND; + +- r = userdb_by_name(name, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &hr); ++ r = userdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { +@@ -114,7 +114,7 @@ enum nss_status userdb_getpwuid( + if (_nss_systemd_is_blocked()) + return NSS_STATUS_NOTFOUND; + +- r = userdb_by_uid(uid, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &hr); ++ r = userdb_by_uid(uid, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { +@@ -190,7 +190,7 @@ enum nss_status userdb_getspnam( + if (_nss_systemd_is_blocked()) + return NSS_STATUS_NOTFOUND; + +- r = userdb_by_name(name, nss_glue_userdb_flags(), &hr); ++ r = userdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags(), &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { +@@ -290,7 +290,7 @@ enum nss_status userdb_getgrnam( + if (_nss_systemd_is_blocked()) + return NSS_STATUS_NOTFOUND; + +- r = groupdb_by_name(name, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &g); ++ r = groupdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &g); + if (r < 0 && r != -ESRCH) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; +@@ -357,7 +357,7 @@ enum nss_status userdb_getgrgid( + if (_nss_systemd_is_blocked()) + return NSS_STATUS_NOTFOUND; + +- r = groupdb_by_gid(gid, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &g); ++ r = groupdb_by_gid(gid, /* match= */ NULL, nss_glue_userdb_flags()|USERDB_SUPPRESS_SHADOW, &g); + if (r < 0 && r != -ESRCH) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; +@@ -456,7 +456,7 @@ enum nss_status userdb_getsgnam( + if (_nss_systemd_is_blocked()) + return NSS_STATUS_NOTFOUND; + +- r = groupdb_by_name(name, nss_glue_userdb_flags(), &hr); ++ r = groupdb_by_name(name, /* match= */ NULL, nss_glue_userdb_flags(), &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { +diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c +index f47da4b4c7..754895c787 100644 +--- a/src/shared/user-record-show.c ++++ b/src/shared/user-record-show.c +@@ -200,7 +200,7 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { + if (show_full_group_info) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + +- r = groupdb_by_gid(hr->gid, 0, &gr); ++ r = groupdb_by_gid(hr->gid, /* match= */ NULL, /* flags= */ 0, &gr); + if (r < 0) { + errno = -r; + printf(" GID: " GID_FMT " (unresolvable: %m)\n", hr->gid); +diff --git a/src/shared/userdb.c b/src/shared/userdb.c +index 32f851b0f3..ac505285bb 100644 +--- a/src/shared/userdb.c ++++ b/src/shared/userdb.c +@@ -4,6 +4,7 @@ + + #include "sd-varlink.h" + ++#include "bitfield.h" + #include "conf-files.h" + #include "dirent-util.h" + #include "dlfcn-util.h" +@@ -35,16 +36,23 @@ struct UserDBIterator { + LookupWhat what; + UserDBFlags flags; + Set *links; ++ ++ const char *method; /* Note, this is a const static string! */ ++ sd_json_variant *query; ++ ++ bool more:1; + bool nss_covered:1; + bool nss_iterating:1; + bool dropin_covered:1; + bool synthesize_root:1; + bool synthesize_nobody:1; + bool nss_systemd_blocked:1; ++ + char **dropins; + size_t current_dropin; + int error; + unsigned n_found; ++ + sd_event *event; + UserRecord *found_user; /* when .what == LOOKUP_USER */ + GroupRecord *found_group; /* when .what == LOOKUP_GROUP */ +@@ -55,10 +63,14 @@ struct UserDBIterator { + char *filter_user_name, *filter_group_name; + }; + ++static int userdb_connect(UserDBIterator *iterator, const char *path, const char *method, bool more, sd_json_variant *query); ++ + UserDBIterator* userdb_iterator_free(UserDBIterator *iterator) { + if (!iterator) + return NULL; + ++ sd_json_variant_unref(iterator->query); ++ + set_free(iterator->links); + strv_free(iterator->dropins); + +@@ -159,6 +171,70 @@ static void membership_data_done(struct membership_data *d) { + free(d->group_name); + } + ++static int userdb_maybe_restart_query( ++ UserDBIterator *iterator, ++ sd_varlink *link, ++ sd_json_variant *parameters, ++ const char *error_id) { ++ ++ int r; ++ ++ assert(iterator); ++ assert(link); ++ assert(error_id); ++ ++ /* These fields were added in v258 and didn't exist in previous implementations. Hence, we consider ++ * their support optional: if any service refuses any of these fields, we'll restart the query ++ * without them, and apply the filtering they are supposed to do client side. */ ++ static const char *const fields[] = { ++ "fuzzyNames", ++ "dispositionMask", ++ "uidMin", ++ "uidMax", ++ "gidMin", ++ "gidMax", ++ NULL ++ }; ++ ++ /* Figure out if the reported error indicates any of the suppressable fields are at fault, and that ++ * our query actually included them */ ++ bool restart = false; ++ STRV_FOREACH(f, fields) { ++ if (!sd_varlink_error_is_invalid_parameter(error_id, parameters, *f)) ++ continue; ++ ++ if (!sd_json_variant_by_key(iterator->query, *f)) ++ continue; ++ ++ restart = true; ++ break; ++ } ++ ++ if (!restart) ++ return 0; ++ ++ /* Now patch the fields out */ ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *patched_query = ++ sd_json_variant_ref(iterator->query); ++ ++ r = sd_json_variant_filter(&patched_query, (char**const) fields); ++ if (r < 0) ++ return r; ++ ++ /* NB: we stored the socket path in the varlink connection description when we set things up here! */ ++ r = userdb_connect( ++ iterator, ++ ASSERT_PTR(sd_varlink_get_description(link)), ++ iterator->method, ++ iterator->more, ++ patched_query); ++ if (r < 0) ++ return r; ++ ++ log_debug("Restarted query to service '%s' due to missing features.", sd_varlink_get_description(link)); ++ return 1; ++} ++ + static int userdb_on_query_reply( + sd_varlink *link, + sd_json_variant *parameters, +@@ -172,6 +248,14 @@ static int userdb_on_query_reply( + if (error_id) { + log_debug("Got lookup error: %s", error_id); + ++ r = userdb_maybe_restart_query(iterator, link, parameters, error_id); ++ if (r < 0) ++ return r; ++ if (r > 0) { ++ r = 0; ++ goto finish; ++ } ++ + /* Convert various forms of record not found into -ESRCH, since NSS typically doesn't care, + * about the details. Note that if a userName specification is refused as invalid parameter, + * we also turn this into -ESRCH following the logic that there cannot be a user record for a +@@ -182,6 +266,8 @@ static int userdb_on_query_reply( + sd_varlink_error_is_invalid_parameter(error_id, parameters, "userName") || + sd_varlink_error_is_invalid_parameter(error_id, parameters, "groupName")) + r = -ESRCH; ++ else if (streq(error_id, "io.systemd.UserDatabase.NonMatchingRecordFound")) ++ r = -ENOEXEC; + else if (streq(error_id, "io.systemd.UserDatabase.ServiceNotAvailable")) + r = -EHOSTDOWN; + else if (streq(error_id, "io.systemd.UserDatabase.EnumerationNotSupported")) +@@ -338,9 +424,9 @@ static int userdb_on_query_reply( + } + + 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) ++ /* If we got one ESRCH or ENOEXEC, let that win. This way when we do a wild dump we won't be tripped ++ * up by bad errors – as long as at least one connection ended somewhat cleanly */ ++ if (IN_SET(r, -ESRCH, -ENOEXEC) || iterator->error == 0) + iterator->error = -r; + + assert_se(set_remove(iterator->links, link) == link); +@@ -378,7 +464,12 @@ static int userdb_connect( + if (r < 0) + return log_debug_errno(r, "Failed to attach varlink connection to event loop: %m"); + +- (void) sd_varlink_set_description(vl, path); ++ /* Note, this is load bearing: we store the socket path as description for the varlink ++ * connection. That's not just good for debugging, but we reuse this information in case we need to ++ * reissue the query with a reduced set of parameters. */ ++ r = sd_varlink_set_description(vl, path); ++ if (r < 0) ++ return log_debug_errno(r, "Failed to set varlink connection description: %m"); + + r = sd_varlink_bind_reply(vl, userdb_on_query_reply); + if (r < 0) +@@ -410,7 +501,7 @@ static int userdb_connect( + + static int userdb_start_query( + UserDBIterator *iterator, +- const char *method, ++ const char *method, /* must be a static string, we are not going to copy this here! */ + bool more, + sd_json_variant *query, + UserDBFlags flags) { +@@ -426,6 +517,11 @@ static int userdb_start_query( + if (FLAGS_SET(flags, USERDB_EXCLUDE_VARLINK)) + return -ENOLINK; + ++ assert(!iterator->query); ++ iterator->method = method; /* note: we don't make a copy here! */ ++ iterator->query = sd_json_variant_ref(query); ++ iterator->more = more; ++ + e = getenv("SYSTEMD_BYPASS_USERDB"); + if (e) { + r = parse_boolean(e); +@@ -674,38 +770,61 @@ nomatch: + return 0; + } + +-int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { +- _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; +- _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; ++static int query_append_disposition_mask(sd_json_variant **query, uint64_t mask) { + int r; + +- if (FLAGS_SET(flags, USERDB_PARSE_NUMERIC)) { +- uid_t uid; ++ assert(query); + +- if (parse_uid(name, &uid) >= 0) +- return userdb_by_uid(uid, flags, ret); ++ if (FLAGS_SET(mask, USER_DISPOSITION_MASK_ALL)) ++ return 0; ++ ++ _cleanup_strv_free_ char **dispositions = NULL; ++ for (UserDisposition d = 0; d < _USER_DISPOSITION_MAX; d++) { ++ if (!BITS_SET(mask, d)) ++ continue; ++ ++ r = strv_extend(&dispositions, user_disposition_to_string(d)); ++ if (r < 0) ++ return r; + } + +- if (!valid_user_group_name(name, VALID_USER_RELAX)) +- return -EINVAL; ++ return sd_json_variant_merge_objectbo( ++ query, ++ SD_JSON_BUILD_PAIR_STRV("dispositionMask", dispositions)); ++} + +- r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(name))); ++static int query_append_uid_match(sd_json_variant **query, const UserDBMatch *match) { ++ int r; ++ ++ assert(query); ++ ++ if (!userdb_match_is_set(match)) ++ return 0; ++ ++ r = sd_json_variant_merge_objectbo( ++ query, ++ SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(match->fuzzy_names), "fuzzyNames", SD_JSON_BUILD_STRV(match->fuzzy_names)), ++ SD_JSON_BUILD_PAIR_CONDITION(match->uid_min > 0, "uidMin", SD_JSON_BUILD_UNSIGNED(match->uid_min)), ++ SD_JSON_BUILD_PAIR_CONDITION(match->uid_max < UID_INVALID-1, "uidMax", SD_JSON_BUILD_UNSIGNED(match->uid_max))); + if (r < 0) + return r; + +- iterator = userdb_iterator_new(LOOKUP_USER, flags); +- if (!iterator) +- return -ENOMEM; ++ return query_append_disposition_mask(query, match->disposition_mask); ++} + +- 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; +- } ++static int userdb_by_name_fallbacks( ++ const char *name, ++ UserDBIterator *iterator, ++ UserDBFlags flags, ++ UserRecord **ret) { ++ int r; ++ ++ assert(name); ++ assert(iterator); ++ assert(ret); + + if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !iterator->dropin_covered) { +- r = dropin_user_record_by_name(name, NULL, flags, ret); ++ r = dropin_user_record_by_name(name, /* path= */ NULL, flags, ret); + if (r >= 0) + return r; + } +@@ -737,21 +856,41 @@ int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { + return r; + if (r > 0) + return synthetic_foreign_user_build(foreign_uid, ret); +- r = -ESRCH; + } + +- return r; ++ return -ESRCH; + } + +-int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) { ++int userdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flags, UserRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; + int r; + +- if (!uid_is_valid(uid)) ++ /* Well known errors this returns: ++ * -EINVAL → user name is not valid ++ * -ESRCH → no such user ++ * -ENOEXEC → found a user by request UID or name, but it does not match filter ++ * -EHOSTDOWN → service failed for some reason ++ * -ETIMEDOUT → service timed out ++ */ ++ ++ assert(name); ++ ++ if (FLAGS_SET(flags, USERDB_PARSE_NUMERIC)) { ++ uid_t uid; ++ ++ if (parse_uid(name, &uid) >= 0) ++ return userdb_by_uid(uid, match, flags, ret); ++ } ++ ++ if (!valid_user_group_name(name, VALID_USER_RELAX)) + return -EINVAL; + +- r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_UNSIGNED(uid))); ++ r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("userName", SD_JSON_BUILD_STRING(name))); ++ if (r < 0) ++ return r; ++ ++ r = query_append_uid_match(&query, match); + if (r < 0) + return r; + +@@ -759,12 +898,45 @@ int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) { + if (!iterator) + return -ENOMEM; + +- r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags); ++ _cleanup_(user_record_unrefp) UserRecord *ur = NULL; ++ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ false, query, flags); + if (r >= 0) { +- r = userdb_process(iterator, ret, NULL, NULL, NULL); +- if (r >= 0) ++ r = userdb_process(iterator, &ur, /* ret_group_record= */ NULL, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); ++ if (r == -ENOEXEC) /* found a user matching UID or name, but not filter. In this case the ++ * fallback paths below are pointless */ + return r; + } ++ if (r < 0) { /* If the above fails for any other reason, try fallback paths */ ++ r = userdb_by_name_fallbacks(name, iterator, flags, &ur); ++ if (r < 0) ++ return r; ++ } ++ ++ /* NB: we always apply our own filtering here, explicitly, regardless if the server supported it or ++ * not. It's more robust this way, we never know how carefully the server is written, and whether it ++ * properly implements all details of the filtering logic. */ ++ r = user_record_match(ur, match); ++ if (r < 0) ++ return r; ++ if (r == 0) ++ return -ENOEXEC; ++ ++ if (ret) ++ *ret = TAKE_PTR(ur); ++ ++ return 0; ++} ++ ++static int userdb_by_uid_fallbacks( ++ uid_t uid, ++ UserDBIterator *iterator, ++ UserDBFlags flags, ++ UserRecord **ret) { ++ int r; ++ ++ assert(uid_is_valid(uid)); ++ assert(iterator); ++ assert(ret); + + if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !iterator->dropin_covered) { + r = dropin_user_record_by_uid(uid, NULL, flags, ret); +@@ -793,20 +965,70 @@ int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) { + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_FOREIGN) && uid_is_foreign(uid)) + return synthetic_foreign_user_build(uid - FOREIGN_UID_BASE, ret); + +- return r; ++ return -ESRCH; + } + +-int userdb_all(UserDBFlags flags, UserDBIterator **ret) { ++int userdb_by_uid(uid_t uid, const UserDBMatch *match, UserDBFlags flags, UserRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; ++ int r; ++ ++ if (!uid_is_valid(uid)) ++ return -EINVAL; ++ ++ r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("uid", SD_JSON_BUILD_UNSIGNED(uid))); ++ if (r < 0) ++ return r; ++ ++ r = query_append_uid_match(&query, match); ++ if (r < 0) ++ return r; ++ ++ iterator = userdb_iterator_new(LOOKUP_USER, flags); ++ if (!iterator) ++ return -ENOMEM; ++ ++ _cleanup_(user_record_unrefp) UserRecord *ur = NULL; ++ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ false, query, flags); ++ if (r >= 0) { ++ r = userdb_process(iterator, &ur, /* ret_group_record= */ NULL, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); ++ if (r == -ENOEXEC) ++ return r; ++ } ++ if (r < 0) { ++ r = userdb_by_uid_fallbacks(uid, iterator, flags, &ur); ++ if (r < 0) ++ return r; ++ } ++ ++ r = user_record_match(ur, match); ++ if (r < 0) ++ return r; ++ if (r == 0) ++ return -ENOEXEC; ++ ++ if (ret) ++ *ret = TAKE_PTR(ur); ++ ++ return 0; ++} ++ ++int userdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **ret) { ++ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; + int r, qr; + + assert(ret); + ++ r = query_append_uid_match(&query, match); ++ if (r < 0) ++ return r; ++ + iterator = userdb_iterator_new(LOOKUP_USER, flags); + if (!iterator) + return -ENOMEM; + +- qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", true, NULL, flags); ++ qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ true, query, flags); + + if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) { + r = userdb_iterator_block_nss_systemd(iterator); +@@ -840,7 +1062,7 @@ int userdb_all(UserDBFlags flags, UserDBIterator **ret) { + return 0; + } + +-int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret) { ++static int userdb_iterator_get_one(UserDBIterator *iterator, UserRecord **ret) { + int r; + + assert(iterator); +@@ -928,7 +1150,7 @@ int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret) { + } + + /* Then, let's return the users provided by varlink IPC */ +- r = userdb_process(iterator, ret, NULL, NULL, NULL); ++ r = userdb_process(iterator, ret, /* ret_group_record= */ NULL, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); + if (r < 0) { + + /* Finally, synthesize root + nobody if not done yet */ +@@ -952,6 +1174,29 @@ int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret) { + return r; + } + ++int userdb_iterator_get(UserDBIterator *iterator, const UserDBMatch *match, UserRecord **ret) { ++ int r; ++ ++ assert(iterator); ++ assert(iterator->what == LOOKUP_USER); ++ ++ for (;;) { ++ _cleanup_(user_record_unrefp) UserRecord *ur = NULL; ++ ++ r = userdb_iterator_get_one(iterator, userdb_match_is_set(match) || ret ? &ur : NULL); ++ if (r < 0) ++ return r; ++ ++ if (ur && !user_record_match(ur, match)) ++ continue; ++ ++ if (ret) ++ *ret = TAKE_PTR(ur); ++ ++ return r; ++ } ++} ++ + static int synthetic_root_group_build(GroupRecord **ret) { + return group_record_build( + ret, +@@ -993,43 +1238,44 @@ static int synthetic_foreign_group_build(gid_t foreign_gid, GroupRecord **ret) { + SD_JSON_BUILD_PAIR("disposition", JSON_BUILD_CONST_STRING("foreign")))); + } + +-int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { +- _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; +- _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; ++static int query_append_gid_match(sd_json_variant **query, const UserDBMatch *match) { + int r; + +- if (FLAGS_SET(flags, USERDB_PARSE_NUMERIC)) { +- gid_t gid; +- +- if (parse_gid(name, &gid) >= 0) +- return groupdb_by_gid(gid, flags, ret); +- } ++ assert(query); + +- if (!valid_user_group_name(name, VALID_USER_RELAX)) +- return -EINVAL; ++ if (!userdb_match_is_set(match)) ++ return 0; + +- r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("groupName", SD_JSON_BUILD_STRING(name))); ++ r = sd_json_variant_merge_objectbo( ++ query, ++ SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(match->fuzzy_names), "fuzzyNames", SD_JSON_BUILD_STRV(match->fuzzy_names)), ++ SD_JSON_BUILD_PAIR_CONDITION(match->gid_min > 0, "gidMin", SD_JSON_BUILD_UNSIGNED(match->gid_min)), ++ SD_JSON_BUILD_PAIR_CONDITION(match->gid_max < GID_INVALID-1, "gidMax", SD_JSON_BUILD_UNSIGNED(match->gid_max))); + if (r < 0) + return r; + +- iterator = userdb_iterator_new(LOOKUP_GROUP, flags); +- if (!iterator) +- return -ENOMEM; ++ return query_append_disposition_mask(query, match->disposition_mask); ++} + +- 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; +- } ++static int groupdb_by_name_fallbacks( ++ const char *name, ++ UserDBIterator *iterator, ++ UserDBFlags flags, ++ GroupRecord **ret) { + +- if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !(iterator && iterator->dropin_covered)) { ++ int r; ++ ++ assert(name); ++ assert(iterator); ++ assert(ret); ++ ++ if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !iterator->dropin_covered) { + r = dropin_group_record_by_name(name, NULL, flags, ret); + if (r >= 0) + return r; + } + +- if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && !(iterator && iterator->nss_covered)) { ++ if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && !iterator->nss_covered) { + r = userdb_iterator_block_nss_systemd(iterator); + if (r >= 0) { + r = nss_group_record_by_name(name, !FLAGS_SET(flags, USERDB_SUPPRESS_SHADOW), ret); +@@ -1053,21 +1299,33 @@ int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { + return r; + if (r > 0) + return synthetic_foreign_group_build(foreign_gid, ret); +- r = -ESRCH; + } + +- return r; ++ return -ESRCH; + } + +-int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) { ++int groupdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; + int r; + +- if (!gid_is_valid(gid)) ++ assert(name); ++ ++ if (FLAGS_SET(flags, USERDB_PARSE_NUMERIC)) { ++ gid_t gid; ++ ++ if (parse_gid(name, &gid) >= 0) ++ return groupdb_by_gid(gid, match, flags, ret); ++ } ++ ++ if (!valid_user_group_name(name, VALID_USER_RELAX)) + return -EINVAL; + +- r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(gid))); ++ r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("groupName", SD_JSON_BUILD_STRING(name))); ++ if (r < 0) ++ return r; ++ ++ r = query_append_gid_match(&query, match); + if (r < 0) + return r; + +@@ -1075,13 +1333,43 @@ int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) { + if (!iterator) + return -ENOMEM; + +- r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags); ++ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; ++ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ false, query, flags); + if (r >= 0) { +- r = userdb_process(iterator, NULL, ret, NULL, NULL); +- if (r >= 0) ++ r = userdb_process(iterator, /* ret_user_record= */ NULL, &gr, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); ++ if (r == -ENOEXEC) ++ return r; ++ } ++ if (r < 0) { ++ r = groupdb_by_name_fallbacks(name, iterator, flags, &gr); ++ if (r < 0) + return r; + } + ++ /* As above, we apply our own client-side filtering even if server-side filtering worked, for robustness and simplicity reasons. */ ++ r = group_record_match(gr, match); ++ if (r < 0) ++ return r; ++ if (r == 0) ++ return -ENOEXEC; ++ ++ if (ret) ++ *ret = TAKE_PTR(gr); ++ ++ return r; ++} ++ ++static int groupdb_by_gid_fallbacks( ++ gid_t gid, ++ UserDBIterator *iterator, ++ UserDBFlags flags, ++ GroupRecord **ret) { ++ int r; ++ ++ assert(gid_is_valid(gid)); ++ assert(iterator); ++ assert(ret); ++ + if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !(iterator && iterator->dropin_covered)) { + r = dropin_group_record_by_gid(gid, NULL, flags, ret); + if (r >= 0) +@@ -1108,20 +1396,70 @@ int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) { + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE_FOREIGN) && gid_is_foreign(gid)) + return synthetic_foreign_group_build(gid - FOREIGN_UID_BASE, ret); + +- return r; ++ return -ESRCH; + } + +-int groupdb_all(UserDBFlags flags, UserDBIterator **ret) { ++int groupdb_by_gid(gid_t gid, const UserDBMatch *match, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; ++ int r; ++ ++ if (!gid_is_valid(gid)) ++ return -EINVAL; ++ ++ r = sd_json_buildo(&query, SD_JSON_BUILD_PAIR("gid", SD_JSON_BUILD_UNSIGNED(gid))); ++ if (r < 0) ++ return r; ++ ++ r = query_append_gid_match(&query, match); ++ if (r < 0) ++ return r; ++ ++ iterator = userdb_iterator_new(LOOKUP_GROUP, flags); ++ if (!iterator) ++ return -ENOMEM; ++ ++ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; ++ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ false, query, flags); ++ if (r >= 0) { ++ r = userdb_process(iterator, /* ret_user_record= */ NULL, &gr, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL); ++ if (r == -ENOEXEC) ++ return r; ++ } ++ if (r < 0) { ++ r = groupdb_by_gid_fallbacks(gid, iterator, flags, &gr); ++ if (r < 0) ++ return r; ++ } ++ ++ r = group_record_match(gr, match); ++ if (r < 0) ++ return r; ++ if (r == 0) ++ return -ENOEXEC; ++ ++ if (ret) ++ *ret = TAKE_PTR(gr); ++ ++ return 0; ++} ++ ++int groupdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **ret) { ++ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *query = NULL; + int r, qr; + + assert(ret); + ++ r = query_append_gid_match(&query, match); ++ if (r < 0) ++ return r; ++ + iterator = userdb_iterator_new(LOOKUP_GROUP, flags); + if (!iterator) + return -ENOMEM; + +- qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", true, NULL, flags); ++ qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ true, query, flags); + + if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) { + r = userdb_iterator_block_nss_systemd(iterator); +@@ -1152,7 +1490,7 @@ int groupdb_all(UserDBFlags flags, UserDBIterator **ret) { + return 0; + } + +-int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret) { ++static int groupdb_iterator_get_one(UserDBIterator *iterator, GroupRecord **ret) { + int r; + + assert(iterator); +@@ -1254,6 +1592,29 @@ int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret) { + return r; + } + ++int groupdb_iterator_get(UserDBIterator *iterator, const UserDBMatch *match, GroupRecord **ret) { ++ int r; ++ ++ assert(iterator); ++ assert(iterator->what == LOOKUP_GROUP); ++ ++ for (;;) { ++ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; ++ ++ r = groupdb_iterator_get_one(iterator, userdb_match_is_set(match) || ret ? &gr : NULL); ++ if (r < 0) ++ return r; ++ ++ if (gr && !group_record_match(gr, match)) ++ continue; ++ ++ if (ret) ++ *ret = TAKE_PTR(gr); ++ ++ return r; ++ } ++} ++ + static void discover_membership_dropins(UserDBIterator *i, UserDBFlags flags) { + int r; + +diff --git a/src/shared/userdb.h b/src/shared/userdb.h +index 9bb47efbfe..783e39d591 100644 +--- a/src/shared/userdb.h ++++ b/src/shared/userdb.h +@@ -42,15 +42,15 @@ typedef enum UserDBFlags { + * -ETIMEDOUT: Time-out + */ + +-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 userdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flags, UserRecord **ret); ++int userdb_by_uid(uid_t uid, const UserDBMatch *match, UserDBFlags flags, UserRecord **ret); ++int userdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **ret); ++int userdb_iterator_get(UserDBIterator *iterator, const UserDBMatch *match, UserRecord **ret); ++ ++int groupdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flags, GroupRecord **ret); ++int groupdb_by_gid(gid_t gid, const UserDBMatch *match, UserDBFlags flags, GroupRecord **ret); ++int groupdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **ret); ++int groupdb_iterator_get(UserDBIterator *iterator, const UserDBMatch *match, GroupRecord **ret); + + int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret); + int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret); +diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c +index 2c3b274828..074d9103af 100644 +--- a/src/userdb/userdbctl.c ++++ b/src/userdb/userdbctl.c +@@ -397,7 +397,7 @@ static int display_user(int argc, char *argv[], void *userdata) { + (void) table_hide_column_from_display(table, (size_t) 0); + } + +- UserDBMatch match = { ++ _cleanup_(userdb_match_done) UserDBMatch match = { + .disposition_mask = arg_disposition_mask, + .uid_min = arg_uid_min, + .uid_max = arg_uid_max, +@@ -423,19 +423,18 @@ static int display_user(int argc, char *argv[], void *userdata) { + STRV_FOREACH(i, argv + 1) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + +- r = userdb_by_name(*i, arg_userdb_flags|USERDB_PARSE_NUMERIC, &ur); ++ r = userdb_by_name(*i, &match, arg_userdb_flags|USERDB_PARSE_NUMERIC, &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 if (r == -ENOEXEC) ++ log_error_errno(r, "User '%s' exists but does not match specified filter.", *i); + else + log_error_errno(r, "Failed to find user %s: %m", *i); + + RET_GATHER(ret, r); +- } else if (!user_record_match(ur, &match)) { +- log_error("User '%s' does not match filter.", *i); +- RET_GATHER(ret, -ENOEXEC); + } else { + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); +@@ -448,18 +447,15 @@ static int display_user(int argc, char *argv[], void *userdata) { + } + } + else { +- _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; +- _cleanup_strv_free_ char **names = NULL; +- + if (argc > 1) { +- names = strv_copy(argv + 1); +- if (!names) ++ /* If there are further arguments, they are the fuzzy match strings. */ ++ match.fuzzy_names = strv_copy(strv_skip(argv, 1)); ++ if (!match.fuzzy_names) + return log_oom(); +- +- match.fuzzy_names = names; + } + +- r = userdb_all(arg_userdb_flags, &iterator); ++ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; ++ r = userdb_all(&match, arg_userdb_flags, &iterator); + if (r == -ENOLINK) /* ENOLINK → Didn't find answer without Varlink, and didn't try Varlink because was configured to off. */ + log_debug_errno(r, "No entries found. (Didn't check via Varlink.)"); + else if (r == -ESRCH) /* ESRCH → Couldn't find any suitable entry, but we checked all sources */ +@@ -470,7 +466,7 @@ static int display_user(int argc, char *argv[], void *userdata) { + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + +- r = userdb_iterator_get(iterator, &ur); ++ r = userdb_iterator_get(iterator, &match, &ur); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) +@@ -478,9 +474,6 @@ static int display_user(int argc, char *argv[], void *userdata) { + if (r < 0) + return log_error_errno(r, "Failed acquire next user: %m"); + +- if (!user_record_match(ur, &match)) +- continue; +- + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); + +@@ -745,7 +738,7 @@ static int display_group(int argc, char *argv[], void *userdata) { + (void) table_hide_column_from_display(table, (size_t) 0); + } + +- UserDBMatch match = { ++ _cleanup_(userdb_match_done) UserDBMatch match = { + .disposition_mask = arg_disposition_mask, + .gid_min = arg_uid_min, + .gid_max = arg_uid_max, +@@ -771,19 +764,18 @@ static int display_group(int argc, char *argv[], void *userdata) { + STRV_FOREACH(i, argv + 1) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + +- r = groupdb_by_name(*i, arg_userdb_flags|USERDB_PARSE_NUMERIC, &gr); ++ r = groupdb_by_name(*i, &match, arg_userdb_flags|USERDB_PARSE_NUMERIC, &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 if (r == -ENOEXEC) ++ log_error_errno(r, "Group '%s' exists but does not match specified filter.", *i); + else + log_error_errno(r, "Failed to find group %s: %m", *i); + + RET_GATHER(ret, r); +- } else if (!group_record_match(gr, &match)) { +- log_error("Group '%s' does not match filter.", *i); +- RET_GATHER(ret, -ENOEXEC); + } else { + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); +@@ -796,18 +788,14 @@ static int display_group(int argc, char *argv[], void *userdata) { + } + } + else { +- _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; +- _cleanup_strv_free_ char **names = NULL; +- + if (argc > 1) { +- names = strv_copy(argv + 1); +- if (!names) ++ match.fuzzy_names = strv_copy(strv_skip(argv, 1)); ++ if (!match.fuzzy_names) + return log_oom(); +- +- match.fuzzy_names = names; + } + +- r = groupdb_all(arg_userdb_flags, &iterator); ++ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; ++ r = groupdb_all(&match, arg_userdb_flags, &iterator); + if (r == -ENOLINK) + log_debug_errno(r, "No entries found. (Didn't check via Varlink.)"); + else if (r == -ESRCH) +@@ -818,7 +806,7 @@ static int display_group(int argc, char *argv[], void *userdata) { + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + +- r = groupdb_iterator_get(iterator, &gr); ++ r = groupdb_iterator_get(iterator, &match, &gr); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) +@@ -826,9 +814,6 @@ static int display_group(int argc, char *argv[], void *userdata) { + if (r < 0) + return log_error_errno(r, "Failed acquire next group: %m"); + +- if (!group_record_match(gr, &match)) +- continue; +- + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); + +@@ -1131,7 +1116,7 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { + chain_invocation = NULL; + } + +- r = userdb_by_name(argv[1], arg_userdb_flags, &ur); ++ r = userdb_by_name(argv[1], /* match= */ NULL, arg_userdb_flags, &ur); + if (r == -ESRCH) + log_error_errno(r, "User %s does not exist.", argv[1]); + else if (r == -EHOSTDOWN) +diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c +index c8fef87326..da115ec6e5 100644 +--- a/src/userdb/userwork.c ++++ b/src/userdb/userwork.c +@@ -160,14 +160,14 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + return r; + + if (uid_is_valid(p.uid)) +- r = userdb_by_uid(p.uid, userdb_flags, &hr); ++ r = userdb_by_uid(p.uid, /* match= */ NULL, userdb_flags, &hr); + else if (p.name) +- r = userdb_by_name(p.name, userdb_flags, &hr); ++ r = userdb_by_name(p.name, /* match= */ NULL, userdb_flags, &hr); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *last = NULL; + +- r = userdb_all(userdb_flags, &iterator); ++ r = userdb_all(/* match= */ NULL, userdb_flags, &iterator); + if (IN_SET(r, -ESRCH, -ENOLINK)) + /* We turn off Varlink lookups in various cases (e.g. in case we only enable DropIn + * backend) — this might make userdb_all return ENOLINK (which indicates that varlink +@@ -182,7 +182,7 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *z = NULL; + +- r = userdb_iterator_get(iterator, &z); ++ r = userdb_iterator_get(iterator, /* match= */ NULL, &z); + if (r == -ESRCH) + break; + if (r < 0) +@@ -296,14 +296,14 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + return r; + + if (gid_is_valid(p.gid)) +- r = groupdb_by_gid(p.gid, userdb_flags, &g); ++ r = groupdb_by_gid(p.gid, /* match= */ NULL, userdb_flags, &g); + else if (p.name) +- r = groupdb_by_name(p.name, userdb_flags, &g); ++ r = groupdb_by_name(p.name, /* match= */ NULL, userdb_flags, &g); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *last = NULL; + +- r = groupdb_all(userdb_flags, &iterator); ++ r = groupdb_all(/* match= */ NULL, userdb_flags, &iterator); + if (IN_SET(r, -ESRCH, -ENOLINK)) + return sd_varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) +@@ -312,7 +312,7 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *z = NULL; + +- r = groupdb_iterator_get(iterator, &z); ++ r = groupdb_iterator_get(iterator, /* match= */ NULL, &z); + if (r == -ESRCH) + break; + if (r < 0) diff --git a/0592-userdbd-implement-server-side-filtering-in-the-Multi.patch b/0592-userdbd-implement-server-side-filtering-in-the-Multi.patch new file mode 100644 index 0000000..1124895 --- /dev/null +++ b/0592-userdbd-implement-server-side-filtering-in-the-Multi.patch @@ -0,0 +1,162 @@ +From 9b5c0f5201194158377da6a3366fa87743395b9b Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:45:52 +0100 +Subject: [PATCH] userdbd: implement server side filtering in the Multiplexer + API + +This impelements server side filtering in userdbd's multiplexer logic. +Note thta this means that even if some backend doesn't support it +natively the multiplexer will deal with it and apply the filtering as +necessary. + +(cherry picked from commit 5ec4933dd5c38d09ed6169cbb22c79ca3be217ce) + +Related: RHEL-143036 +--- + src/userdb/userwork.c | 53 ++++++++++++++++++++++++++++++------------- + 1 file changed, 37 insertions(+), 16 deletions(-) + +diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c +index da115ec6e5..b030fae84c 100644 +--- a/src/userdb/userwork.c ++++ b/src/userdb/userwork.c +@@ -35,8 +35,15 @@ typedef struct LookupParameters { + gid_t gid; + }; + const char *service; ++ UserDBMatch match; + } LookupParameters; + ++static void lookup_parameters_done(LookupParameters *p) { ++ assert(p); ++ ++ userdb_match_done(&p->match); ++} ++ + static int add_nss_service(sd_json_variant **v) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *status = NULL, *z = NULL; + sd_id128_t mid; +@@ -134,16 +141,21 @@ static int userdb_flags_from_service(sd_varlink *link, const char *service, User + static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { +- { "uid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, +- { "userName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, name), SD_JSON_RELAX }, +- { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, ++ { "uid", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, ++ { "userName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, name), SD_JSON_RELAX }, ++ { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, ++ { "fuzzyNames", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(LookupParameters, match.fuzzy_names), 0 }, ++ { "dispositionMask", SD_JSON_VARIANT_ARRAY, json_dispatch_dispositions_mask, offsetof(LookupParameters, match.disposition_mask), 0 }, ++ { "uidMin", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.uid_min), 0 }, ++ { "uidMax", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.uid_max), 0 }, + {} + }; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; +- LookupParameters p = { ++ _cleanup_(lookup_parameters_done) LookupParameters p = { + .uid = UID_INVALID, ++ .match = USERDB_MATCH_NULL, + }; + UserDBFlags userdb_flags; + int r; +@@ -160,14 +172,14 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + return r; + + if (uid_is_valid(p.uid)) +- r = userdb_by_uid(p.uid, /* match= */ NULL, userdb_flags, &hr); ++ r = userdb_by_uid(p.uid, &p.match, userdb_flags, &hr); + else if (p.name) +- r = userdb_by_name(p.name, /* match= */ NULL, userdb_flags, &hr); ++ r = userdb_by_name(p.name, &p.match, userdb_flags, &hr); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *last = NULL; + +- r = userdb_all(/* match= */ NULL, userdb_flags, &iterator); ++ r = userdb_all(&p.match, userdb_flags, &iterator); + if (IN_SET(r, -ESRCH, -ENOLINK)) + /* We turn off Varlink lookups in various cases (e.g. in case we only enable DropIn + * backend) — this might make userdb_all return ENOLINK (which indicates that varlink +@@ -182,7 +194,7 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *z = NULL; + +- r = userdb_iterator_get(iterator, /* match= */ NULL, &z); ++ r = userdb_iterator_get(iterator, &p.match, &z); + if (r == -ESRCH) + break; + if (r < 0) +@@ -208,6 +220,8 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + } + if (r == -ESRCH) + return sd_varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); ++ if (r == -ENOEXEC) ++ return sd_varlink_error(link, "io.systemd.UserDatabase.NonMatchingRecordFound", NULL); + if (r < 0) { + log_debug_errno(r, "User lookup failed abnormally: %m"); + return sd_varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL); +@@ -271,16 +285,21 @@ static int build_group_json(sd_varlink *link, GroupRecord *gr, sd_json_variant * + static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { +- { "gid", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, +- { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, name), SD_JSON_RELAX }, +- { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, ++ { "gid", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, ++ { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(LookupParameters, name), SD_JSON_RELAX }, ++ { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, ++ { "fuzzyNames", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(LookupParameters, match.fuzzy_names), 0 }, ++ { "dispositionMask", SD_JSON_VARIANT_ARRAY, json_dispatch_dispositions_mask, offsetof(LookupParameters, match.disposition_mask), 0 }, ++ { "gidMin", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.gid_min), 0 }, ++ { "gidMax", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.gid_max), 0 }, + {} + }; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; +- LookupParameters p = { ++ _cleanup_(lookup_parameters_done) LookupParameters p = { + .gid = GID_INVALID, ++ .match = USERDB_MATCH_NULL, + }; + UserDBFlags userdb_flags; + int r; +@@ -296,14 +315,14 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + return r; + + if (gid_is_valid(p.gid)) +- r = groupdb_by_gid(p.gid, /* match= */ NULL, userdb_flags, &g); ++ r = groupdb_by_gid(p.gid, &p.match, userdb_flags, &g); + else if (p.name) +- r = groupdb_by_name(p.name, /* match= */ NULL, userdb_flags, &g); ++ r = groupdb_by_name(p.name, &p.match, userdb_flags, &g); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *last = NULL; + +- r = groupdb_all(/* match= */ NULL, userdb_flags, &iterator); ++ r = groupdb_all(&p.match, userdb_flags, &iterator); + if (IN_SET(r, -ESRCH, -ENOLINK)) + return sd_varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) +@@ -312,7 +331,7 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *z = NULL; + +- r = groupdb_iterator_get(iterator, /* match= */ NULL, &z); ++ r = groupdb_iterator_get(iterator, &p.match, &z); + if (r == -ESRCH) + break; + if (r < 0) +@@ -338,6 +357,8 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + } + if (r == -ESRCH) + return sd_varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); ++ if (r == -ENOEXEC) ++ return sd_varlink_error(link, "io.systemd.UserDatabase.NonMatchingRecordFound", NULL); + if (r < 0) { + log_debug_errno(r, "Group lookup failed abnormally: %m"); + return sd_varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL); diff --git a/0593-homectl-port-has_regular_user-acquire_group_list-to-.patch b/0593-homectl-port-has_regular_user-acquire_group_list-to-.patch new file mode 100644 index 0000000..790c862 --- /dev/null +++ b/0593-homectl-port-has_regular_user-acquire_group_list-to-.patch @@ -0,0 +1,108 @@ +From b81ac039917b08d298ae03d59a449ae7aee0df75 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 16:53:01 +0100 +Subject: [PATCH] homectl: port has_regular_user() + acquire_group_list() to + use server-side filtering + +(cherry picked from commit dea3dd66495f8d12977cb1603322a85d155057f3) + +Related: RHEL-143036 +--- + src/home/homectl.c | 42 +++++++++++++++++++++--------------------- + 1 file changed, 21 insertions(+), 21 deletions(-) + +diff --git a/src/home/homectl.c b/src/home/homectl.c +index 46a2a4c806..c11e6e0fdc 100644 +--- a/src/home/homectl.c ++++ b/src/home/homectl.c +@@ -5,6 +5,7 @@ + #include "sd-bus.h" + + #include "ask-password-api.h" ++#include "bitfield.h" + #include "build.h" + #include "bus-common-errors.h" + #include "bus-error.h" +@@ -2393,36 +2394,35 @@ static int create_from_credentials(void) { + + static int has_regular_user(void) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; ++ UserDBMatch match = USERDB_MATCH_NULL; + int r; + +- r = userdb_all(/* match= */ NULL, USERDB_SUPPRESS_SHADOW, &iterator); ++ match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR); ++ ++ r = userdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to create user enumerator: %m"); + +- for (;;) { +- _cleanup_(user_record_unrefp) UserRecord *ur = NULL; +- +- r = userdb_iterator_get(iterator, /* match= */ NULL, &ur); +- if (r == -ESRCH) +- break; +- if (r < 0) +- return log_error_errno(r, "Failed to enumerate users: %m"); +- +- if (user_record_disposition(ur) == USER_REGULAR) +- return true; +- } ++ r = userdb_iterator_get(iterator, &match, /* ret= */ NULL); ++ if (r == -ESRCH) ++ return false; ++ if (r < 0) ++ return log_error_errno(r, "Failed to enumerate users: %m"); + +- return false; ++ return true; + } + + static int acquire_group_list(char ***ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_strv_free_ char **groups = NULL; ++ UserDBMatch match = USERDB_MATCH_NULL; + int r; + + assert(ret); + +- r = groupdb_all(/* match= */ NULL, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, &iterator); ++ match.disposition_mask = INDEXES_TO_MASK(uint64_t, USER_REGULAR, USER_SYSTEM); ++ ++ r = groupdb_all(&match, USERDB_SUPPRESS_SHADOW, &iterator); + if (r == -ENOLINK) + log_debug_errno(r, "No groups found. (Didn't check via Varlink.)"); + else if (r == -ESRCH) +@@ -2433,25 +2433,25 @@ static int acquire_group_list(char ***ret) { + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + +- r = groupdb_iterator_get(iterator, /* match= */ NULL, &gr); ++ r = groupdb_iterator_get(iterator, &match, &gr); + if (r == -ESRCH) + break; + if (r < 0) + return log_debug_errno(r, "Failed acquire next group: %m"); + +- if (!IN_SET(group_record_disposition(gr), USER_REGULAR, USER_SYSTEM)) +- continue; +- + if (group_record_disposition(gr) == USER_REGULAR) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + + /* Filter groups here that belong to a specific user, and are named like them */ + +- r = userdb_by_name(gr->group_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW|USERDB_EXCLUDE_DYNAMIC_USER, &ur); ++ UserDBMatch user_match = USERDB_MATCH_NULL; ++ user_match.disposition_mask = INDEX_TO_MASK(uint64_t, USER_REGULAR); ++ ++ r = userdb_by_name(gr->group_name, &user_match, USERDB_SUPPRESS_SHADOW, &ur); + if (r < 0 && r != -ESRCH) + return log_debug_errno(r, "Failed to check if matching user exists for group '%s': %m", gr->group_name); + +- if (r >= 0 && ur->gid == gr->gid && user_record_disposition(ur) == USER_REGULAR) ++ if (r >= 0 && ur->gid == gr->gid) + continue; + } + diff --git a/0594-update-TODO.patch b/0594-update-TODO.patch new file mode 100644 index 0000000..17fe5b0 --- /dev/null +++ b/0594-update-TODO.patch @@ -0,0 +1,26 @@ +From bdbfe972548a78d6ac81986ccb70f54142fdd770 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 22 Jan 2025 23:27:54 +0100 +Subject: [PATCH] update TODO + +(cherry picked from commit 35121184993fa7c9937a5c9654db3e64d5cb0815) + +Related: RHEL-143036 +--- + TODO | 3 --- + 1 file changed, 3 deletions(-) + +diff --git a/TODO b/TODO +index a20eb2c61e..d6c2cef2f1 100644 +--- a/TODO ++++ b/TODO +@@ -1601,9 +1601,6 @@ Features: + * add growvol and makevol options for /etc/crypttab, similar to + x-systemd.growfs and x-systemd-makefs. + +-* userdb: allow username prefix searches in varlink API, allow realname and +- realname substr searches in varlink API +- + * userdb: allow uid/gid range checks + + * userdb: allow existence checks diff --git a/0595-JSON-User-Group-records-Add-properties-for-UUIDs.patch b/0595-JSON-User-Group-records-Add-properties-for-UUIDs.patch new file mode 100644 index 0000000..46ee65f --- /dev/null +++ b/0595-JSON-User-Group-records-Add-properties-for-UUIDs.patch @@ -0,0 +1,57 @@ +From 6fe4e2e64df52c985acd6ccf02cc69415fbef116 Mon Sep 17 00:00:00 2001 +From: Erin Shepherd +Date: Sat, 5 Apr 2025 21:40:08 +0200 +Subject: [PATCH] JSON User/Group records: Add properties for UUIDs + +It is useful to have stable and unique identifiers for a security principal. +The majority of identitiy management systems in use with Unix systems today +(e.g. Active Directory objectGUID, FreeIPA ipaUniqueID, Kanidm UUIDs) assign +each account and group a unique UUID and exposing that to applications allows +them to refer to accounts in a stable manner. + +This change does not implement user or group lookup by UUID; that is left for +a later PR. + +(cherry picked from commit 800afbbcd7f11255b7fc0ab3948861b27be96eb8) + +Related: RHEL-143036 +--- + docs/GROUP_RECORD.md | 6 ++++++ + docs/USER_RECORD.md | 7 +++++++ + 2 files changed, 13 insertions(+) + +diff --git a/docs/GROUP_RECORD.md b/docs/GROUP_RECORD.md +index c055e49d43..add1a0d786 100644 +--- a/docs/GROUP_RECORD.md ++++ b/docs/GROUP_RECORD.md +@@ -20,6 +20,12 @@ they carry some identical (or at least very similar) fields. + Matches the `gr_name` field of UNIX/glibc NSS `struct group`, + or the shadow structure `struct sgrp`'s `sg_namp` field. + ++`uuid` -> A string containing a lowercase UUID that identifies this group. ++The same considerations apply to this field as they do to the corresponding field of user records. ++Users and groups MUST NOT share the same UUID unless they are semantically ++the same security principal e.g. if a system synthesizes a single-user group from ++user records to be the user's primary group. ++ + `realm` → The "realm" the group belongs to, conceptually identical to the same field of user records. + A string in DNS domain name syntax. + +diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md +index a8e02b2c5e..350ca76649 100644 +--- a/docs/USER_RECORD.md ++++ b/docs/USER_RECORD.md +@@ -234,6 +234,13 @@ retrievable and resolvable under every name listed here, pretty much everywhere + the primary user name is. If logging in is attempted via an alias name it + should be normalized to the primary name. + ++`uuid` -> A string containing a lowercase UUID that identifies this user. ++The UUID should be assigned to the user at creation, be the same across multiple machines, ++and never change (even if the user's username, realm or other identifying attributes change). ++When the user database is backed by Microsoft Active Directory, this field should contain ++he value from the [objectGUID](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ada3/937eb5c6-f6b3-4652-a276-5d6bb8979658) ++attribute. The same UUID can be retrieved via `mbr_uid_to_uuid` on macOS. ++ + `blobDirectory` → The absolute path to a world-readable copy of the user's blob + directory. See [Blob Directories](/USER_RECORD_BLOB_DIRS) for more details. + diff --git a/0596-userdb-add-support-for-printing-the-UUID-from-user-a.patch b/0596-userdb-add-support-for-printing-the-UUID-from-user-a.patch new file mode 100644 index 0000000..6ddfc6e --- /dev/null +++ b/0596-userdb-add-support-for-printing-the-UUID-from-user-a.patch @@ -0,0 +1,89 @@ +From eee22e76c180f9afb5401c8c5e9dcee55cd70c43 Mon Sep 17 00:00:00 2001 +From: Erin Shepherd +Date: Tue, 8 Apr 2025 12:07:46 +0000 +Subject: [PATCH] userdb: add support for printing the UUID from user and group + records + +(cherry picked from commit 60cd0cc77a6312a260ed06e73e62d8de942dcb79) + +Related: RHEL-143036 +--- + src/shared/group-record.c | 1 + + src/shared/group-record.h | 1 + + src/shared/user-record-show.c | 6 ++++++ + src/shared/user-record.c | 1 + + src/shared/user-record.h | 1 + + 5 files changed, 10 insertions(+) + +diff --git a/src/shared/group-record.c b/src/shared/group-record.c +index 07bce7056d..99e1148447 100644 +--- a/src/shared/group-record.c ++++ b/src/shared/group-record.c +@@ -173,6 +173,7 @@ int group_record_load( + static const sd_json_dispatch_field group_dispatch_table[] = { + { "groupName", SD_JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(GroupRecord, group_name), SD_JSON_RELAX }, + { "realm", SD_JSON_VARIANT_STRING, json_dispatch_realm, offsetof(GroupRecord, realm), 0 }, ++ { "uuid", SD_JSON_VARIANT_STRING, sd_json_dispatch_id128, offsetof(GroupRecord, uuid), 0 }, + { "description", SD_JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(GroupRecord, description), 0 }, + { "disposition", SD_JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(GroupRecord, disposition), 0 }, + { "service", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(GroupRecord, service), SD_JSON_STRICT }, +diff --git a/src/shared/group-record.h b/src/shared/group-record.h +index 5705fe2511..1a81b006ce 100644 +--- a/src/shared/group-record.h ++++ b/src/shared/group-record.h +@@ -13,6 +13,7 @@ typedef struct GroupRecord { + char *group_name; + char *realm; + char *group_name_and_realm_auto; ++ sd_id128_t uuid; + + char *description; + +diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c +index 754895c787..dab75d1f79 100644 +--- a/src/shared/user-record-show.c ++++ b/src/shared/user-record-show.c +@@ -239,6 +239,9 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) { + } + } + ++ if (!sd_id128_is_null(hr->uuid)) ++ printf(" UUID: " SD_ID128_UUID_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->uuid)); ++ + if (hr->real_name && !streq(hr->real_name, hr->user_name)) + printf(" Real Name: %s\n", hr->real_name); + +@@ -644,6 +647,9 @@ void group_record_show(GroupRecord *gr, bool show_full_user_info) { + if (gid_is_valid(gr->gid)) + printf(" GID: " GID_FMT "\n", gr->gid); + ++ if (!sd_id128_is_null(gr->uuid)) ++ printf(" UUID: " SD_ID128_UUID_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(gr->uuid)); ++ + if (show_full_user_info) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + +diff --git a/src/shared/user-record.c b/src/shared/user-record.c +index e984f6d24c..3b2194a9de 100644 +--- a/src/shared/user-record.c ++++ b/src/shared/user-record.c +@@ -1541,6 +1541,7 @@ int user_record_load(UserRecord *h, sd_json_variant *v, UserRecordLoadFlags load + { "userName", SD_JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), SD_JSON_RELAX }, + { "aliases", SD_JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, aliases), SD_JSON_RELAX }, + { "realm", SD_JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 }, ++ { "uuid", SD_JSON_VARIANT_STRING, sd_json_dispatch_id128, offsetof(UserRecord, uuid), 0 }, + { "blobDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), SD_JSON_STRICT }, + { "blobManifest", SD_JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 }, + { "realName", SD_JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 }, +diff --git a/src/shared/user-record.h b/src/shared/user-record.h +index 91402f38b9..a7a3f5e924 100644 +--- a/src/shared/user-record.h ++++ b/src/shared/user-record.h +@@ -242,6 +242,7 @@ typedef struct UserRecord { + char *realm; + char *user_name_and_realm_auto; /* the user_name field concatenated with '@' and the realm, if the latter is defined */ + char **aliases; ++ sd_id128_t uuid; + char *real_name; + char *email_address; + char *password_hint; diff --git a/0597-userdb-add-support-for-looking-up-users-or-groups-by.patch b/0597-userdb-add-support-for-looking-up-users-or-groups-by.patch new file mode 100644 index 0000000..4f1f6cd --- /dev/null +++ b/0597-userdb-add-support-for-looking-up-users-or-groups-by.patch @@ -0,0 +1,213 @@ +From 6f5da55f271c36fbc55a6e5f343375b25978c2ed Mon Sep 17 00:00:00 2001 +From: Erin Shepherd +Date: Fri, 11 Apr 2025 19:18:32 +0000 +Subject: [PATCH] userdb: add support for looking up users or groups by uuid. + +This propagates the UUID lookup parameter through the API permitting +lookups to be done by uuid. + +(cherry picked from commit 52874bb763cf97e453ef49a9f71db7c4cc6c1322) + +Resolves: RHEL-143036 +--- + docs/USER_GROUP_API.md | 11 +++++++---- + src/shared/user-record.c | 3 +++ + src/shared/user-record.h | 5 ++++- + src/shared/userdb.c | 33 +++++++++++++++++---------------- + src/userdb/userwork.c | 2 ++ + 5 files changed, 33 insertions(+), 21 deletions(-) + +diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md +index 0beb3bb912..3e273cccb0 100644 +--- a/docs/USER_GROUP_API.md ++++ b/docs/USER_GROUP_API.md +@@ -165,6 +165,7 @@ method GetUserRecord( + dispositionMask: ?[]string, + uidMin: ?int, + uidMax: ?int, ++ uuid: ?string, + service : string + ) -> ( + record : object, +@@ -178,6 +179,7 @@ method GetGroupRecord( + dispositionMask: ?[]string, + gidMin: ?int, + gidMax: ?int, ++ uuid: ?string, + service : string + ) -> ( + record : object, +@@ -212,7 +214,7 @@ 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 `fuzzyNames`, `dispositionMask`, `uidMin`, `uidMax` fields permit ++The `fuzzyNames`, `dispositionMask`, `uidMin`, `uidMax` and `uuid` fields permit + *additional* filtering of the returned set of user records. The `fuzzyNames` + parameter shall be one or more strings that shall be searched for in "fuzzy" + way. What specifically this means is left for the backend to decide, but +@@ -222,19 +224,20 @@ carry identifying information for the user. The `dispositionMask` field shall + be one of more user record `disposition` strings. If specified only user + records matching one of the specified dispositions should be enumerated. The + `uidMin` and `uidMax` fields specify a minimum and maximum value for the UID of +-returned records. Inline searching for `uid` and `userName` support for ++returned records. The `uuid` field specifies to search for the user record associated ++with the specified UUID. Inline searching for `uid` and `userName` support for + filtering with these four additional parameters is optional, and clients are + expected to be able to do client-side filtering in case the parameters are not + supported by a service. The service should return the usual `InvalidParameter` + error for the relevant parameter if one is passed and it does not support + it. If a request is made specifying `uid` or `userName` and a suitable record + is found, but the specified filter via `fuzzyNames`, `dispositionMask`, +-`uidMin`, or `uidMax` does not match, a `NonMatchingRecordFound` error should ++`uidMin`, `uidMax` or `uuid` does not match, a `NonMatchingRecordFound` error should + be returned. + + Or to say this differently: the *primary search keys* are + `userName`/`groupName` and `uid`/`gid` and the *secondary search filters* are +-`fuzzyNames`, `dispositionMask`, `uidMin`, `uidMax`. If no entry matching ++`fuzzyNames`, `dispositionMask`, `uidMin`, `uidMax`, `uuid`. If no entry matching + either of the primary search keys are found `NoRecordFound()` is returned. If + one is found that matches one but not the other primary search key + `ConflictingRecordFound()` is returned. If an entry is found that matches the +diff --git a/src/shared/user-record.c b/src/shared/user-record.c +index 3b2194a9de..342a1c9c4f 100644 +--- a/src/shared/user-record.c ++++ b/src/shared/user-record.c +@@ -2705,6 +2705,9 @@ int user_record_match(UserRecord *u, const UserDBMatch *match) { + if (!FLAGS_SET(match->disposition_mask, UINT64_C(1) << user_record_disposition(u))) + return false; + ++ if (!sd_id128_is_null(match->uuid) && !sd_id128_equal(match->uuid, u->uuid)) ++ return false; ++ + if (!strv_isempty(match->fuzzy_names)) { + + /* Note this array of names is sparse, i.e. various entries listed in it will be +diff --git a/src/shared/user-record.h b/src/shared/user-record.h +index a7a3f5e924..a3d8f4eb07 100644 +--- a/src/shared/user-record.h ++++ b/src/shared/user-record.h +@@ -487,6 +487,7 @@ typedef struct UserDBMatch { + uid_t uid_max; + gid_t gid_max; + }; ++ sd_id128_t uuid; + } UserDBMatch; + + #define USER_DISPOSITION_MASK_ALL ((UINT64_C(1) << _USER_DISPOSITION_MAX) - UINT64_C(1)) +@@ -496,6 +497,7 @@ typedef struct UserDBMatch { + .disposition_mask = USER_DISPOSITION_MASK_ALL, \ + .uid_min = 0, \ + .uid_max = UID_INVALID-1, \ ++ .uuid = SD_ID128_NULL, \ + } + + static inline bool userdb_match_is_set(const UserDBMatch *match) { +@@ -505,7 +507,8 @@ static inline bool userdb_match_is_set(const UserDBMatch *match) { + return !strv_isempty(match->fuzzy_names) || + !FLAGS_SET(match->disposition_mask, USER_DISPOSITION_MASK_ALL) || + match->uid_min > 0 || +- match->uid_max < UID_INVALID-1; ++ match->uid_max < UID_INVALID-1 || ++ !sd_id128_is_null(match->uuid); + } + + static inline void userdb_match_done(UserDBMatch *match) { +diff --git a/src/shared/userdb.c b/src/shared/userdb.c +index ac505285bb..638fc5e9af 100644 +--- a/src/shared/userdb.c ++++ b/src/shared/userdb.c +@@ -770,27 +770,29 @@ nomatch: + return 0; + } + +-static int query_append_disposition_mask(sd_json_variant **query, uint64_t mask) { ++static int query_append_common(sd_json_variant **query, const UserDBMatch *match) { + int r; ++ _cleanup_strv_free_ char **dispositions = NULL; + + assert(query); + +- if (FLAGS_SET(mask, USER_DISPOSITION_MASK_ALL)) +- return 0; +- +- _cleanup_strv_free_ char **dispositions = NULL; +- for (UserDisposition d = 0; d < _USER_DISPOSITION_MAX; d++) { +- if (!BITS_SET(mask, d)) +- continue; ++ uint64_t mask = match->disposition_mask; ++ if (FLAGS_SET(mask, USER_DISPOSITION_MASK_ALL)) { ++ for (UserDisposition d = 0; d < _USER_DISPOSITION_MAX; d++) { ++ if (!BIT_SET(mask, d)) ++ continue; + +- r = strv_extend(&dispositions, user_disposition_to_string(d)); +- if (r < 0) +- return r; ++ r = strv_extend(&dispositions, user_disposition_to_string(d)); ++ if (r < 0) ++ return r; ++ } + } + + return sd_json_variant_merge_objectbo( + query, +- SD_JSON_BUILD_PAIR_STRV("dispositionMask", dispositions)); ++ SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(match->fuzzy_names), "fuzzyNames", SD_JSON_BUILD_STRV(match->fuzzy_names)), ++ SD_JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(match->uuid), "uuid", SD_JSON_BUILD_UUID(match->uuid)), ++ SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(dispositions), "dispositionMask", SD_JSON_BUILD_STRV(dispositions))); + } + + static int query_append_uid_match(sd_json_variant **query, const UserDBMatch *match) { +@@ -803,13 +805,12 @@ static int query_append_uid_match(sd_json_variant **query, const UserDBMatch *ma + + r = sd_json_variant_merge_objectbo( + query, +- SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(match->fuzzy_names), "fuzzyNames", SD_JSON_BUILD_STRV(match->fuzzy_names)), + SD_JSON_BUILD_PAIR_CONDITION(match->uid_min > 0, "uidMin", SD_JSON_BUILD_UNSIGNED(match->uid_min)), + SD_JSON_BUILD_PAIR_CONDITION(match->uid_max < UID_INVALID-1, "uidMax", SD_JSON_BUILD_UNSIGNED(match->uid_max))); + if (r < 0) + return r; + +- return query_append_disposition_mask(query, match->disposition_mask); ++ return query_append_common(query, match); + } + + static int userdb_by_name_fallbacks( +@@ -1248,13 +1249,13 @@ static int query_append_gid_match(sd_json_variant **query, const UserDBMatch *ma + + r = sd_json_variant_merge_objectbo( + query, +- SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(match->fuzzy_names), "fuzzyNames", SD_JSON_BUILD_STRV(match->fuzzy_names)), + SD_JSON_BUILD_PAIR_CONDITION(match->gid_min > 0, "gidMin", SD_JSON_BUILD_UNSIGNED(match->gid_min)), + SD_JSON_BUILD_PAIR_CONDITION(match->gid_max < GID_INVALID-1, "gidMax", SD_JSON_BUILD_UNSIGNED(match->gid_max))); ++ + if (r < 0) + return r; + +- return query_append_disposition_mask(query, match->disposition_mask); ++ return query_append_common(query, match); + } + + static int groupdb_by_name_fallbacks( +diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c +index b030fae84c..d079af4dcb 100644 +--- a/src/userdb/userwork.c ++++ b/src/userdb/userwork.c +@@ -148,6 +148,7 @@ static int vl_method_get_user_record(sd_varlink *link, sd_json_variant *paramete + { "dispositionMask", SD_JSON_VARIANT_ARRAY, json_dispatch_dispositions_mask, offsetof(LookupParameters, match.disposition_mask), 0 }, + { "uidMin", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.uid_min), 0 }, + { "uidMax", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.uid_max), 0 }, ++ { "uuid", SD_JSON_VARIANT_STRING, sd_json_dispatch_id128, offsetof(LookupParameters, match.uuid), 0 }, + {} + }; + +@@ -292,6 +293,7 @@ static int vl_method_get_group_record(sd_varlink *link, sd_json_variant *paramet + { "dispositionMask", SD_JSON_VARIANT_ARRAY, json_dispatch_dispositions_mask, offsetof(LookupParameters, match.disposition_mask), 0 }, + { "gidMin", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.gid_min), 0 }, + { "gidMax", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(LookupParameters, match.gid_max), 0 }, ++ { "uuid", SD_JSON_VARIANT_STRING, sd_json_dispatch_id128, offsetof(LookupParameters, match.uuid), 0 }, + {} + }; + diff --git a/0598-userdbctl-add-uuid-filtering-option.patch b/0598-userdbctl-add-uuid-filtering-option.patch new file mode 100644 index 0000000..d93541e --- /dev/null +++ b/0598-userdbctl-add-uuid-filtering-option.patch @@ -0,0 +1,111 @@ +From 9a92c83a44c68511b52302058a95de89239cbf15 Mon Sep 17 00:00:00 2001 +From: Erin Shepherd +Date: Mon, 21 Jul 2025 19:29:53 +0000 +Subject: [PATCH] userdbctl: add --uuid filtering option + +This uses the new UUID-based filtering logic inside the userdb library +to return just the requested user/group record + +(cherry picked from commit 466562c69b75cec197176f556b940a43bb8350f2) + +Related: RHEL-143036 +--- + man/userdbctl.xml | 9 +++++++++ + src/userdb/userdbctl.c | 15 +++++++++++++-- + 2 files changed, 22 insertions(+), 2 deletions(-) + +diff --git a/man/userdbctl.xml b/man/userdbctl.xml +index 22d7da4d12..110b1c0f35 100644 +--- a/man/userdbctl.xml ++++ b/man/userdbctl.xml +@@ -225,6 +225,15 @@ + + + ++ ++ ++ ++ When used with the user or group command, ++ filters the output to the record with the specified UUID. If unspecified, no UUID-based filtering is applied. ++ ++ ++ ++ + + + +diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c +index 074d9103af..cb9d2e1897 100644 +--- a/src/userdb/userdbctl.c ++++ b/src/userdb/userdbctl.c +@@ -42,6 +42,7 @@ static bool arg_chain = false; + static uint64_t arg_disposition_mask = UINT64_MAX; + static uid_t arg_uid_min = 0; + static uid_t arg_uid_max = UID_INVALID-1; ++static sd_id128_t arg_uuid = SD_ID128_NULL; + static bool arg_fuzzy = false; + static bool arg_boundaries = true; + static sd_json_variant *arg_from_file = NULL; +@@ -381,7 +382,7 @@ static int display_user(int argc, char *argv[], void *userdata) { + int ret = 0, r; + + if (arg_output < 0) +- arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; ++ arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) || !sd_id128_is_null(arg_uuid) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + + if (arg_output == OUTPUT_TABLE) { + table = table_new(" ", "name", "disposition", "uid", "gid", "realname", "home", "shell", "order"); +@@ -401,6 +402,7 @@ static int display_user(int argc, char *argv[], void *userdata) { + .disposition_mask = arg_disposition_mask, + .uid_min = arg_uid_min, + .uid_max = arg_uid_max, ++ .uuid = arg_uuid, + }; + + if (arg_from_file) { +@@ -723,7 +725,7 @@ static int display_group(int argc, char *argv[], void *userdata) { + int ret = 0, r; + + if (arg_output < 0) +- arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; ++ arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) || !sd_id128_is_null(arg_uuid) ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + + if (arg_output == OUTPUT_TABLE) { + table = table_new(" ", "name", "disposition", "gid", "description", "order"); +@@ -742,6 +744,7 @@ static int display_group(int argc, char *argv[], void *userdata) { + .disposition_mask = arg_disposition_mask, + .gid_min = arg_uid_min, + .gid_max = arg_uid_max, ++ .uuid = arg_uuid, + }; + + if (arg_from_file) { +@@ -1235,6 +1238,7 @@ static int parse_argv(int argc, char *argv[]) { + ARG_CHAIN, + ARG_UID_MIN, + ARG_UID_MAX, ++ ARG_UUID, + ARG_DISPOSITION, + ARG_BOUNDARIES, + }; +@@ -1255,6 +1259,7 @@ static int parse_argv(int argc, char *argv[]) { + { "chain", no_argument, NULL, ARG_CHAIN }, + { "uid-min", required_argument, NULL, ARG_UID_MIN }, + { "uid-max", required_argument, NULL, ARG_UID_MAX }, ++ { "uuid", required_argument, NULL, ARG_UUID }, + { "fuzzy", no_argument, NULL, 'z' }, + { "disposition", required_argument, NULL, ARG_DISPOSITION }, + { "boundaries", required_argument, NULL, ARG_BOUNDARIES }, +@@ -1450,6 +1455,12 @@ static int parse_argv(int argc, char *argv[]) { + return log_error_errno(r, "Failed to parse --uid-max= value: %s", optarg); + break; + ++ case ARG_UUID: ++ r = sd_id128_from_string(optarg, &arg_uuid); ++ if (r < 0) ++ return log_error_errno(r, "Failed to parse --uuid= value: %s", optarg); ++ break; ++ + case 'z': + arg_fuzzy = true; + break; diff --git a/0599-userdbctl-add-missing-uuid-to-help-text.patch b/0599-userdbctl-add-missing-uuid-to-help-text.patch new file mode 100644 index 0000000..3a90739 --- /dev/null +++ b/0599-userdbctl-add-missing-uuid-to-help-text.patch @@ -0,0 +1,26 @@ +From 09282345b595d817725be5333baa08e16ce1f8de Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Sat, 20 Dec 2025 06:33:55 +0100 +Subject: [PATCH] userdbctl: add missing --uuid= to --help text + +Follow-up for: 466562c69b75cec197176f556b940a43bb8350f2 + +(cherry picked from commit f42ac2477258987cf6982c19b05cb46fabca16c9) + +Related: RHEL-143036 +--- + src/userdb/userdbctl.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c +index cb9d2e1897..4c304cc656 100644 +--- a/src/userdb/userdbctl.c ++++ b/src/userdb/userdbctl.c +@@ -1205,6 +1205,7 @@ static int help(int argc, char *argv[], void *userdata) { + " --chain Chain another command\n" + " --uid-min=ID Filter by minimum UID/GID (default 0)\n" + " --uid-max=ID Filter by maximum UID/GID (default 4294967294)\n" ++ " --uuid=UUID Filter by UUID\n" + " -z --fuzzy Do a fuzzy name search\n" + " --disposition=VALUE Filter by disposition\n" + " -I Equivalent to --disposition=intrinsic\n" diff --git a/0600-userdb-fix-typo.patch b/0600-userdb-fix-typo.patch new file mode 100644 index 0000000..ca688c9 --- /dev/null +++ b/0600-userdb-fix-typo.patch @@ -0,0 +1,27 @@ +From d4761084258e69b0f02d5802635ff943607b7625 Mon Sep 17 00:00:00 2001 +From: Yu Watanabe +Date: Sat, 1 Feb 2025 14:09:03 +0900 +Subject: [PATCH] userdb: fix typo + +Follow-up for 7419291670dd4066594350cce585031f60bc4f0a. + +(cherry picked from commit 546e6cb2e3e949e7162e4cf45cce8a6b26db175d) + +Related: RHEL-143036 +--- + src/shared/userdb.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/shared/userdb.c b/src/shared/userdb.c +index 638fc5e9af..d4eecbd001 100644 +--- a/src/shared/userdb.c ++++ b/src/shared/userdb.c +@@ -196,7 +196,7 @@ static int userdb_maybe_restart_query( + NULL + }; + +- /* Figure out if the reported error indicates any of the suppressable fields are at fault, and that ++ /* Figure out if the reported error indicates any of the suppressible fields are at fault, and that + * our query actually included them */ + bool restart = false; + STRV_FOREACH(f, fields) { diff --git a/0601-man-userdbctl-fixup-version-info.patch b/0601-man-userdbctl-fixup-version-info.patch new file mode 100644 index 0000000..e9f15b6 --- /dev/null +++ b/0601-man-userdbctl-fixup-version-info.patch @@ -0,0 +1,27 @@ +From f1e64d4b3583e78e4fbbee9f21e663974d0af1fe Mon Sep 17 00:00:00 2001 +From: Mike Yuan +Date: Wed, 8 Oct 2025 22:17:38 +0200 +Subject: [PATCH] man/userdbctl: fixup version info + +Follow-up for 466562c69b75cec197176f556b940a43bb8350f2 + +(cherry picked from commit fd99d9d1cb53f32dea46f1770446db729c316304) + +Related: RHEL-143036 +--- + man/userdbctl.xml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/man/userdbctl.xml b/man/userdbctl.xml +index 110b1c0f35..4da9762ab0 100644 +--- a/man/userdbctl.xml ++++ b/man/userdbctl.xml +@@ -231,7 +231,7 @@ + When used with the user or group command, + filters the output to the record with the specified UUID. If unspecified, no UUID-based filtering is applied. + +- ++ + + + diff --git a/0602-user-record-add-a-concept-of-inverting-per-host-matc.patch b/0602-user-record-add-a-concept-of-inverting-per-host-matc.patch new file mode 100644 index 0000000..c09f05e --- /dev/null +++ b/0602-user-record-add-a-concept-of-inverting-per-host-matc.patch @@ -0,0 +1,80 @@ +From 41d06bdfa06e1a230661a4281b4003cb53ba3a71 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 19 Feb 2025 17:15:56 +0100 +Subject: [PATCH] user-record: add a concept of inverting per-host matching + sections in user record + +Sometimes it is useful to apply options on all hosts except some. Add a +simple concept for that. + +(cherry picked from commit ce94761debfab321d608e5c4ea876d7bc7f65097) + +Resolves: RHEL-143034 +--- + src/shared/user-record.c | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/src/shared/user-record.c b/src/shared/user-record.c +index 342a1c9c4f..bfea52427a 100644 +--- a/src/shared/user-record.c ++++ b/src/shared/user-record.c +@@ -1087,6 +1087,8 @@ int per_machine_id_match(sd_json_variant *ids, sd_json_dispatch_flags_t flags) { + sd_id128_t mid; + int r; + ++ assert(ids); ++ + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(ids, flags, r, "Failed to acquire machine ID: %m"); +@@ -1135,6 +1137,8 @@ int per_machine_hostname_match(sd_json_variant *hns, sd_json_dispatch_flags_t fl + _cleanup_free_ char *hn = NULL; + int r; + ++ assert(hns); ++ + r = gethostname_strict(&hn); + if (r == -ENXIO) { + json_log(hns, flags, r, "No hostname set, not matching perMachine hostname record: %m"); +@@ -1182,6 +1186,15 @@ int per_machine_match(sd_json_variant *entry, sd_json_dispatch_flags_t flags) { + return true; + } + ++ m = sd_json_variant_by_key(entry, "matchNotMachineId"); ++ if (m) { ++ r = per_machine_id_match(m, flags); ++ if (r < 0) ++ return r; ++ if (r == 0) ++ return true; ++ } ++ + m = sd_json_variant_by_key(entry, "matchHostname"); + if (m) { + r = per_machine_hostname_match(m, flags); +@@ -1191,6 +1204,15 @@ int per_machine_match(sd_json_variant *entry, sd_json_dispatch_flags_t flags) { + return true; + } + ++ m = sd_json_variant_by_key(entry, "matchNotHostname"); ++ if (m) { ++ r = per_machine_hostname_match(m, flags); ++ if (r < 0) ++ return r; ++ if (r == 0) ++ return true; ++ } ++ + return false; + } + +@@ -1198,7 +1220,9 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j + + static const sd_json_dispatch_field per_machine_dispatch_table[] = { + { "matchMachineId", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, ++ { "matchNotMachineId", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchHostname", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, ++ { "matchNotHostname", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "blobDirectory", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, blob_directory), SD_JSON_STRICT }, + { "blobManifest", SD_JSON_VARIANT_OBJECT, dispatch_blob_manifest, offsetof(UserRecord, blob_manifest), 0 }, + { "iconName", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(UserRecord, icon_name), SD_JSON_STRICT }, diff --git a/0603-homectl-add-interface-for-controlling-storage-for-ne.patch b/0603-homectl-add-interface-for-controlling-storage-for-ne.patch new file mode 100644 index 0000000..5ec010d --- /dev/null +++ b/0603-homectl-add-interface-for-controlling-storage-for-ne.patch @@ -0,0 +1,647 @@ +From 9df0f85a5a9a27a135f8caab544827932170c585 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Wed, 19 Feb 2025 21:56:54 +0100 +Subject: [PATCH] homectl: add interface for controlling storage for negative + machine ID matches + +(cherry picked from commit 0e1ede4b4b6d1ce6b5b6cda5f803e4f1b5aa4a03) + +Related: RHEL-143034 +--- + docs/USER_RECORD.md | 17 +++- + man/homectl.xml | 34 +++++++ + shell-completion/bash/homectl | 5 +- + src/home/homectl.c | 181 +++++++++++++++++++++++----------- + 4 files changed, 172 insertions(+), 65 deletions(-) + +diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md +index 350ca76649..90675dd532 100644 +--- a/docs/USER_RECORD.md ++++ b/docs/USER_RECORD.md +@@ -758,14 +758,23 @@ If any of the specified IDs match the system's local machine ID + (As a special case, if only a single machine ID is listed this field may be a single + string rather than an array of strings.) + ++`matchNotMachineId` → Similar to `matchMachineId` but implements the inverse ++match: this section only applies if the local machine ID does *not* match any ++of the listed IDs. ++ + `matchHostname` → An array of strings that are valid hostnames. + 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. + (As a special case, if only a single hostname is listed this field may be a single string rather than an array of strings.) + +-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 ++`matchNotHostname` → Similar to `matchHostname`, but implement the inverse ++match, as above. ++ ++If any of these four fields are used within the same array entry, the object is ++honored when either match succeeds, i.e. the match types are combined in OR, ++not in AND. ++ ++These four are the only 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: + + `blobDirectory`, `blobManifest`, `iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`, +diff --git a/man/homectl.xml b/man/homectl.xml +index 282066c4fa..d08972c421 100644 +--- a/man/homectl.xml ++++ b/man/homectl.xml +@@ -179,6 +179,40 @@ + + + ++ ++ ++ ++ ++ ++ ++ Takes one of this, other, ++ any or auto. Some user record settings can be defined to match ++ only specific machines, or all machines but one, or all machines. With this switch it is possibly to ++ control to which machines to apply the settings appearing on the command line after it. If ++ this is specified the setting will only apply to the local system (positive ++ match), if other it will apply to all but the local system (negative match), if ++ any it will apply to all systems (unless there's a matching positive or negative ++ per-machine setting). If auto returns to the default logic: whether a setting ++ applies by default to the local system or all systems depends on the option in question. ++ ++ Note that only some user record settings can be conditioned like this. This option has no ++ effect on the others and is ignored there. This option may appear multiple times in a single command ++ line to apply settings conditioned by different matches to the same user record. See JSON User Records for details on which settings may be ++ used with such per-machine matching and which ones may not. ++ ++ is a shortcut for , is ++ short for and is short for ++ . ++ ++ Here's an example call that sets the storage field to luks on the local ++ system, but to cifs on all others: ++ ++ # homectl update lennart -T --storage=luks -N --storage=cifs ++ ++ ++ ++ + + + +diff --git a/shell-completion/bash/homectl b/shell-completion/bash/homectl +index 5e2235bc3b..c8fbf8c553 100644 +--- a/shell-completion/bash/homectl ++++ b/shell-completion/bash/homectl +@@ -41,7 +41,7 @@ _homectl() { + local -A OPTS=( + [STANDALONE]='-h --help --version + --no-pager --no-legend --no-ask-password +- -j -E -P' ++ -j -E -P -A -N -T' + [ARG]=' -H --host + -M --machine + --identity +@@ -112,7 +112,8 @@ _homectl() { + --avatar + --login-background + --session-launcher +- --session-type' ++ --session-type ++ --match' + ) + + if __contains_word "$prev" ${OPTS[ARG]}; then +diff --git a/src/home/homectl.c b/src/home/homectl.c +index c11e6e0fdc..6d5aec1602 100644 +--- a/src/home/homectl.c ++++ b/src/home/homectl.c +@@ -68,6 +68,7 @@ static const char *arg_identity = NULL; + static sd_json_variant *arg_identity_extra = NULL; + static sd_json_variant *arg_identity_extra_privileged = NULL; + static sd_json_variant *arg_identity_extra_this_machine = NULL; ++static sd_json_variant *arg_identity_extra_other_machines = NULL; + static sd_json_variant *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; +@@ -99,6 +100,7 @@ static Hashmap *arg_blob_files = NULL; + + STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, sd_json_variant_unrefp); + STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, sd_json_variant_unrefp); ++STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_other_machines, sd_json_variant_unrefp); + STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, sd_json_variant_unrefp); + STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, sd_json_variant_unrefp); + STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); +@@ -116,6 +118,7 @@ static bool identity_properties_specified(void) { + !sd_json_variant_is_blank_object(arg_identity_extra) || + !sd_json_variant_is_blank_object(arg_identity_extra_privileged) || + !sd_json_variant_is_blank_object(arg_identity_extra_this_machine) || ++ !sd_json_variant_is_blank_object(arg_identity_extra_other_machines) || + !sd_json_variant_is_blank_object(arg_identity_extra_rlimits) || + !strv_isempty(arg_identity_filter) || + !strv_isempty(arg_identity_filter_rlimits) || +@@ -917,7 +920,7 @@ static int apply_identity_changes(sd_json_variant **_v) { + if (r < 0) + return log_error_errno(r, "Failed to merge identities: %m"); + +- if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) { ++ if (arg_identity_extra_this_machine || arg_identity_extra_other_machines || !strv_isempty(arg_identity_filter)) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *per_machine = NULL, *mmid = NULL; + sd_id128_t mid; + +@@ -931,7 +934,7 @@ static int apply_identity_changes(sd_json_variant **_v) { + + per_machine = sd_json_variant_ref(sd_json_variant_by_key(v, "perMachine")); + if (per_machine) { +- _cleanup_(sd_json_variant_unrefp) sd_json_variant *npm = NULL, *add = NULL; ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *npm = NULL, *positive = NULL, *negative = NULL; + _cleanup_free_ sd_json_variant **array = NULL; + sd_json_variant *z; + size_t i = 0; +@@ -939,7 +942,7 @@ static int apply_identity_changes(sd_json_variant **_v) { + if (!sd_json_variant_is_array(per_machine)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing."); + +- array = new(sd_json_variant*, sd_json_variant_elements(per_machine) + 1); ++ array = new(sd_json_variant*, sd_json_variant_elements(per_machine) + 2); + if (!array) + return log_oom(); + +@@ -952,31 +955,41 @@ static int apply_identity_changes(sd_json_variant **_v) { + array[i++] = z; + + u = sd_json_variant_by_key(z, "matchMachineId"); +- if (!u) +- continue; +- +- if (!sd_json_variant_equal(u, mmid)) +- continue; ++ if (u && sd_json_variant_equal(u, mmid)) ++ r = sd_json_variant_merge_object(&positive, z); ++ else { ++ u = sd_json_variant_by_key(z, "matchNotMachineId"); ++ if (!u || !sd_json_variant_equal(u, mmid)) ++ continue; + +- r = sd_json_variant_merge_object(&add, z); ++ r = sd_json_variant_merge_object(&negative, z); ++ } + if (r < 0) + return log_error_errno(r, "Failed to merge perMachine entry: %m"); + + i--; + } + +- r = sd_json_variant_filter(&add, arg_identity_filter); ++ r = sd_json_variant_filter(&positive, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter perMachine: %m"); + +- r = sd_json_variant_merge_object(&add, arg_identity_extra_this_machine); ++ r = sd_json_variant_filter(&negative, arg_identity_filter); ++ if (r < 0) ++ return log_error_errno(r, "Failed to filter perMachine: %m"); ++ ++ r = sd_json_variant_merge_object(&positive, arg_identity_extra_this_machine); ++ if (r < 0) ++ return log_error_errno(r, "Failed to merge in perMachine fields: %m"); ++ ++ r = sd_json_variant_merge_object(&negative, arg_identity_extra_other_machines); + 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_(sd_json_variant_unrefp) sd_json_variant *rlv = NULL; + +- rlv = sd_json_variant_ref(sd_json_variant_by_key(add, "resourceLimits")); ++ rlv = sd_json_variant_ref(sd_json_variant_by_key(positive, "resourceLimits")); + + r = sd_json_variant_filter(&rlv, arg_identity_filter_rlimits); + if (r < 0) +@@ -987,22 +1000,30 @@ static int apply_identity_changes(sd_json_variant **_v) { + return log_error_errno(r, "Failed to set resource limits: %m"); + + if (sd_json_variant_is_blank_object(rlv)) { +- r = sd_json_variant_filter(&add, STRV_MAKE("resourceLimits")); ++ r = sd_json_variant_filter(&positive, STRV_MAKE("resourceLimits")); + if (r < 0) + return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); + } else { +- r = sd_json_variant_set_field(&add, "resourceLimits", rlv); ++ r = sd_json_variant_set_field(&positive, "resourceLimits", rlv); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + } + +- if (!sd_json_variant_is_blank_object(add)) { +- r = sd_json_variant_set_field(&add, "matchMachineId", mmid); ++ if (!sd_json_variant_is_blank_object(positive)) { ++ r = sd_json_variant_set_field(&positive, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); + +- array[i++] = add; ++ array[i++] = positive; ++ } ++ ++ if (!sd_json_variant_is_blank_object(negative)) { ++ r = sd_json_variant_set_field(&negative, "matchNotMachineId", mmid); ++ if (r < 0) ++ return log_error_errno(r, "Failed to set matchNotMachineId field: %m"); ++ ++ array[i++] = negative; + } + + r = sd_json_variant_new_array(&npm, array, i); +@@ -1012,21 +1033,34 @@ static int apply_identity_changes(sd_json_variant **_v) { + sd_json_variant_unref(per_machine); + per_machine = TAKE_PTR(npm); + } else { +- _cleanup_(sd_json_variant_unrefp) sd_json_variant *item = sd_json_variant_ref(arg_identity_extra_this_machine); ++ _cleanup_(sd_json_variant_unrefp) sd_json_variant *positive = sd_json_variant_ref(arg_identity_extra_this_machine), ++ *negative = sd_json_variant_ref(arg_identity_extra_other_machines); + + if (arg_identity_extra_rlimits) { +- r = sd_json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits); ++ r = sd_json_variant_set_field(&positive, "resourceLimits", arg_identity_extra_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + +- r = sd_json_variant_set_field(&item, "matchMachineId", mmid); +- if (r < 0) +- return log_error_errno(r, "Failed to set matchMachineId field: %m"); ++ if (positive) { ++ r = sd_json_variant_set_field(&positive, "matchMachineId", mmid); ++ if (r < 0) ++ return log_error_errno(r, "Failed to set matchMachineId field: %m"); + +- r = sd_json_variant_append_array(&per_machine, item); +- if (r < 0) +- return log_error_errno(r, "Failed to append to perMachine array: %m"); ++ r = sd_json_variant_append_array(&per_machine, positive); ++ if (r < 0) ++ return log_error_errno(r, "Failed to append to perMachine array: %m"); ++ } ++ ++ if (negative) { ++ r = sd_json_variant_set_field(&negative, "matchNotMachineId", mmid); ++ if (r < 0) ++ return log_error_errno(r, "Failed to set matchNotMachineId field: %m"); ++ ++ r = sd_json_variant_append_array(&per_machine, negative); ++ if (r < 0) ++ return log_error_errno(r, "Failed to append to perMachine array: %m"); ++ } + } + + r = sd_json_variant_set_field(&v, "perMachine", per_machine); +@@ -2968,6 +3002,7 @@ static int parse_argv(int argc, char *argv[]) { + ARG_PROMPT_NEW_USER, + ARG_AVATAR, + ARG_LOGIN_BACKGROUND, ++ ARG_MATCH, + }; + + static const struct option options[] = { +@@ -3068,18 +3103,24 @@ static int parse_argv(int argc, char *argv[]) { + { "blob", required_argument, NULL, 'b' }, + { "avatar", required_argument, NULL, ARG_AVATAR }, + { "login-background", required_argument, NULL, ARG_LOGIN_BACKGROUND }, ++ { "match", required_argument, NULL, ARG_MATCH }, + {} + }; + + int r; + ++ /* This points to one of arg_identity_extra, arg_identity_extra_this_machine, ++ * arg_identity_extra_other_machines, in order to redirect changes on the next property being set to ++ * this part of the identity, instead of the default. */ ++ sd_json_variant **match_identity = NULL; ++ + assert(argc >= 0); + assert(argv); + + for (;;) { + int c; + +- c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPE", options, NULL); ++ c = getopt_long(argc, argv, "hH:M:I:c:d:u:G:k:s:e:b:jPENAT", options, NULL); + if (c < 0) + break; + +@@ -3133,7 +3174,7 @@ static int parse_argv(int argc, char *argv[]) { + if (!valid_gecos(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg); + +- r = sd_json_variant_set_field_string(&arg_identity_extra, "realName", optarg); ++ r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "realName", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set realName field: %m"); + +@@ -3263,7 +3304,7 @@ static int parse_argv(int argc, char *argv[]) { + break; + } + +- r = sd_json_variant_set_field_string(&arg_identity_extra, field, optarg); ++ r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, field, optarg); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + +@@ -3283,7 +3324,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to validate CIFS service name: %s", optarg); + +- r = sd_json_variant_set_field_string(&arg_identity_extra, "cifsService", optarg); ++ r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "cifsService", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set cifsService field: %m"); + +@@ -3319,7 +3360,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse nice level: %s", optarg); + +- r = sd_json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc); ++ r = sd_json_variant_set_field_integer(match_identity ?: &arg_identity_extra, "niceLevel", nc); + if (r < 0) + return log_error_errno(r, "Failed to set niceLevel field: %m"); + +@@ -3443,7 +3484,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return r; + +- r = sd_json_variant_set_field_string(&arg_identity_extra_this_machine, field, v); ++ r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra_this_machine, field, v); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", v); + +@@ -3462,7 +3503,7 @@ static int parse_argv(int argc, char *argv[]) { + if (!valid_shell(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg); + +- r = sd_json_variant_set_field_string(&arg_identity_extra, "shell", optarg); ++ r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "shell", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set shell field: %m"); + +@@ -3481,7 +3522,7 @@ static int parse_argv(int argc, char *argv[]) { + break; + } + +- e = sd_json_variant_by_key(arg_identity_extra, "environment"); ++ e = sd_json_variant_by_key(match_identity ? *match_identity: arg_identity_extra, "environment"); + if (e) { + r = sd_json_variant_strv(e, &l); + if (r < 0) +@@ -3498,7 +3539,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to allocate environment list JSON: %m"); + +- r = sd_json_variant_set_field(&arg_identity_extra, "environment", ne); ++ r = sd_json_variant_set_field(match_identity ?: &arg_identity_extra, "environment", ne); + if (r < 0) + return log_error_errno(r, "Failed to set environment list: %m"); + +@@ -3506,7 +3547,6 @@ static int parse_argv(int argc, char *argv[]) { + } + + case ARG_TIMEZONE: +- + if (isempty(optarg)) { + r = drop_from_identity("timeZone"); + if (r < 0) +@@ -3518,7 +3558,7 @@ static int parse_argv(int argc, char *argv[]) { + if (!timezone_is_valid(optarg, LOG_DEBUG)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg); + +- r = sd_json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg); ++ r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "timeZone", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set timezone field: %m"); + +@@ -3598,7 +3638,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse %s boolean: %m", field); + +- r = sd_json_variant_set_field_boolean(&arg_identity_extra, field, r > 0); ++ r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, field, r > 0); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + +@@ -3634,7 +3674,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return r; + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "diskSize", arg_disk_size); + if (r < 0) + return log_error_errno(r, "Failed to set diskSize field: %m"); + +@@ -3647,7 +3687,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return r; + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); + if (r < 0) + return log_error_errno(r, "Failed to set diskSizeRelative field: %m"); + +@@ -3655,7 +3695,7 @@ static int parse_argv(int argc, char *argv[]) { + } + + /* Automatically turn off the rebalance logic if user configured a size explicitly */ +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "rebalanceWeight", REBALANCE_WEIGHT_OFF); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, "rebalanceWeight", REBALANCE_WEIGHT_OFF); + if (r < 0) + return log_error_errno(r, "Failed to set rebalanceWeight field: %m"); + +@@ -3696,7 +3736,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg); + +- r = sd_json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r); ++ r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "luksDiscard", r); + if (r < 0) + return log_error_errno(r, "Failed to set discard field: %m"); + +@@ -3715,7 +3755,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse --luks-offline-discard= parameter: %s", optarg); + +- r = sd_json_variant_set_field_boolean(&arg_identity_extra, "luksOfflineDiscard", r); ++ r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "luksOfflineDiscard", r); + if (r < 0) + return log_error_errno(r, "Failed to set offline discard field: %m"); + +@@ -3744,7 +3784,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + +@@ -3766,7 +3806,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return r; + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "luksSectorSize", ss); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "luksSectorSize", ss); + if (r < 0) + return log_error_errno(r, "Failed to set sector size field: %m"); + +@@ -3788,7 +3828,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse umask: %m"); + +- r = sd_json_variant_set_field_integer(&arg_identity_extra, "umask", m); ++ r = sd_json_variant_set_field_integer(match_identity ?: &arg_identity_extra, "umask", m); + if (r < 0) + return log_error_errno(r, "Failed to set umask field: %m"); + +@@ -3900,7 +3940,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %m", field); + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + break; +@@ -3933,7 +3973,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %m", field); + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, n); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + break; +@@ -3968,9 +4008,9 @@ static int parse_argv(int argc, char *argv[]) { + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg); + + r = sd_json_variant_set_field_string( +- IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? +- &arg_identity_extra_this_machine : +- &arg_identity_extra, field, optarg); ++ match_identity ?: (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); + +@@ -4001,7 +4041,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse %s field: %s", field, optarg); + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, t); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, t); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + +@@ -4050,7 +4090,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to create group list JSON: %m"); + +- r = sd_json_variant_set_field(&arg_identity_extra, "memberOf", mo); ++ r = sd_json_variant_set_field(match_identity ?: &arg_identity_extra, "memberOf", mo); + if (r < 0) + return log_error_errno(r, "Failed to update group list: %m"); + } +@@ -4072,7 +4112,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg); + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "tasksMax", u); + if (r < 0) + return log_error_errno(r, "Failed to set tasksMax field: %m"); + +@@ -4102,7 +4142,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra_this_machine, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + +@@ -4131,7 +4171,7 @@ static int parse_argv(int argc, char *argv[]) { + if (!CGROUP_WEIGHT_IS_OK(u)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u); + +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, field, u); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + +@@ -4262,7 +4302,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return log_error_errno(r, "Failed to parse --auto-resize-mode= argument: %s", optarg); + +- r = sd_json_variant_set_field_string(&arg_identity_extra, "autoResizeMode", auto_resize_mode_to_string(r)); ++ r = sd_json_variant_set_field_string(match_identity ?: &arg_identity_extra, "autoResizeMode", auto_resize_mode_to_string(r)); + if (r < 0) + return log_error_errno(r, "Failed to set autoResizeMode field: %m"); + +@@ -4296,7 +4336,7 @@ static int parse_argv(int argc, char *argv[]) { + return r; + + /* Add to main identity */ +- r = sd_json_variant_set_field_unsigned(&arg_identity_extra, "rebalanceWeight", u); ++ r = sd_json_variant_set_field_unsigned(match_identity ?: &arg_identity_extra, "rebalanceWeight", u); + if (r < 0) + return log_error_errno(r, "Failed to set rebalanceWeight field: %m"); + +@@ -4363,7 +4403,7 @@ static int parse_argv(int argc, char *argv[]) { + if (r < 0) + return r; + +- r = sd_json_variant_set_field_boolean(&arg_identity_extra, "dropCaches", r); ++ r = sd_json_variant_set_field_boolean(match_identity ?: &arg_identity_extra, "dropCaches", r); + if (r < 0) + return log_error_errno(r, "Failed to set drop caches field: %m"); + +@@ -4417,7 +4457,7 @@ static int parse_argv(int argc, char *argv[]) { + if (capability_set_to_strv(updated, &l) < 0) + return log_oom(); + +- r = sd_json_variant_set_field_strv(&arg_identity_extra, field, l); ++ r = sd_json_variant_set_field_strv(match_identity ?: &arg_identity_extra, field, l); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + +@@ -4501,6 +4541,29 @@ static int parse_argv(int argc, char *argv[]) { + break; + } + ++ case ARG_MATCH: ++ if (streq(optarg, "any")) ++ match_identity = &arg_identity_extra; ++ else if (streq(optarg, "this")) ++ match_identity = &arg_identity_extra_this_machine; ++ else if (streq(optarg, "other")) ++ match_identity = &arg_identity_extra_other_machines; ++ else if (streq(optarg, "auto")) ++ match_identity = NULL; ++ else ++ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--machine= argument not understood. Refusing."); ++ break; ++ ++ case 'A': ++ match_identity = &arg_identity_extra; ++ break; ++ case 'T': ++ match_identity = &arg_identity_extra_this_machine; ++ break; ++ case 'N': ++ match_identity = &arg_identity_extra_other_machines; ++ break; ++ + case '?': + return -EINVAL; + diff --git a/0604-logind-also-save-pidfdid-as-part-of-session-state-ev.patch b/0604-logind-also-save-pidfdid-as-part-of-session-state-ev.patch new file mode 100644 index 0000000..f43d5c0 --- /dev/null +++ b/0604-logind-also-save-pidfdid-as-part-of-session-state-ev.patch @@ -0,0 +1,31 @@ +From 119cca5054f04e1afefc15c626ed1b2353fdb080 Mon Sep 17 00:00:00 2001 +From: Lennart Poettering +Date: Mon, 19 May 2025 12:14:25 +0200 +Subject: [PATCH] logind: also save pidfdid as part of session state, even if + we don't parse it + +(cherry picked from commit f01d8658a3a57d05a5156aefd32d8137c3ee3996) + +Related: RHEL-53112 +--- + src/login/logind-session.c | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/src/login/logind-session.c b/src/login/logind-session.c +index aa2420d5dd..b28dd716d7 100644 +--- a/src/login/logind-session.c ++++ b/src/login/logind-session.c +@@ -398,8 +398,12 @@ int session_save(Session *s) { + if (!s->vtnr) + fprintf(f, "POSITION=%u\n", s->position); + +- if (pidref_is_set(&s->leader)) ++ if (pidref_is_set(&s->leader)) { + fprintf(f, "LEADER="PID_FMT"\n", s->leader.pid); ++ (void) pidref_acquire_pidfd_id(&s->leader); ++ if (s->leader.fd_id != 0) ++ fprintf(f, "LEADER_PIDFDID=%" PRIu64 "\n", s->leader.fd_id); ++ } + + if (audit_session_is_valid(s->audit_id)) + fprintf(f, "AUDIT=%"PRIu32"\n", s->audit_id); diff --git a/0605-logind-support-deserializing-session-leader-through-.patch b/0605-logind-support-deserializing-session-leader-through-.patch new file mode 100644 index 0000000..3019e70 --- /dev/null +++ b/0605-logind-support-deserializing-session-leader-through-.patch @@ -0,0 +1,179 @@ +From a19c25b8b5edbfa9a47f6648862cdea6938ee455 Mon Sep 17 00:00:00 2001 +From: Mike Yuan +Date: Fri, 24 Oct 2025 23:09:50 +0200 +Subject: [PATCH] logind: support deserializing session leader through pidfdid + +People make weird assumptions around state preservation and +expect logind to be stoppable. While this is realistically +not OK we can probably improve things a little. + +This complements f01d8658a3a57d05a5156aefd32d8137c3ee3996 and +adds support for deserializing the LEADER_PIDFDID= field. +We still prioritize pidfd if got one from fdstore (as with +service_notify_message_parse_new_pid() in pid1), but otherwise +this should make logind restart more robust when fdstore +gets spuriously cleared. + +Fixes #39437 + +(cherry picked from commit 45eea629e3b3a640bf6a5cd13f4c73c86b426b11) + +Resolves: RHEL-53112 +--- + src/login/logind-session.c | 62 +++++++++++++++++++++++++++++++------- + src/login/logind.c | 24 ++++++++++----- + 2 files changed, 68 insertions(+), 18 deletions(-) + +diff --git a/src/login/logind-session.c b/src/login/logind-session.c +index b28dd716d7..a81841611c 100644 +--- a/src/login/logind-session.c ++++ b/src/login/logind-session.c +@@ -469,6 +469,46 @@ static int session_load_devices(Session *s, const char *devices) { + return r; + } + ++static int session_load_leader(Session *s, uint64_t pidfdid) { ++ _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; ++ int r; ++ ++ assert(s); ++ assert(pid_is_valid(s->deserialized_pid)); ++ assert(!pidref_is_set(&s->leader)); ++ ++ if (pidfdid == 0 && s->leader_fd_saved) ++ /* We have no pidfd id for stable reference, but the pidfd has been submitted to fdstore. ++ * manager_enumerate_fds() will dispatch the leader fd for us later. */ ++ return 0; ++ ++ r = pidref_set_pid(&pidref, s->deserialized_pid); ++ if (r < 0) ++ return log_error_errno(r, "Failed to deserialize leader PID for session '%s': %m", s->id); ++ if (pidref.fd < 0) ++ return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), ++ "Failed to acquire pidfd for session leader '" PID_FMT "', refusing.", ++ pidref.pid); ++ ++ if (pidfdid > 0) { ++ r = pidref_acquire_pidfd_id(&pidref); ++ if (r < 0) ++ return log_error_errno(r, "Failed to acquire pidfd id of deserialized leader '" PID_FMT "': %m", ++ pidref.pid); ++ ++ if (pidref.fd_id != pidfdid) ++ return log_error_errno(SYNTHETIC_ERRNO(ESRCH), ++ "Deserialized pidfd id for process " PID_FMT " (%" PRIu64 ") doesn't match with the current one (%" PRIu64 "), refusing.", ++ pidref.pid, pidfdid, pidref.fd_id); ++ } ++ ++ r = session_set_leader_consume(s, TAKE_PIDREF(pidref)); ++ if (r < 0) ++ return log_error_errno(r, "Failed to set leader PID for session '%s': %m", s->id); ++ ++ return 1; ++} ++ + int session_load(Session *s) { + _cleanup_free_ char *remote = NULL, + *seat = NULL, +@@ -478,6 +518,7 @@ int session_load(Session *s) { + *position = NULL, + *leader_pid = NULL, + *leader_fd_saved = NULL, ++ *leader_pidfdid = NULL, + *type = NULL, + *original_type = NULL, + *class = NULL, +@@ -511,6 +552,7 @@ int session_load(Session *s) { + "POSITION", &position, + "LEADER", &leader_pid, + "LEADER_FD_SAVED", &leader_fd_saved, ++ "LEADER_PIDFDID", &leader_pidfdid, + "TYPE", &type, + "ORIGINAL_TYPE", &original_type, + "CLASS", &class, +@@ -662,8 +704,6 @@ int session_load(Session *s) { + } + + if (leader_pid) { +- assert(!pidref_is_set(&s->leader)); +- + r = parse_pid(leader_pid, &s->deserialized_pid); + if (r < 0) + return log_error_errno(r, "Failed to parse LEADER=%s: %m", leader_pid); +@@ -673,19 +713,19 @@ int session_load(Session *s) { + if (r < 0) + return log_error_errno(r, "Failed to parse LEADER_FD_SAVED=%s: %m", leader_fd_saved); + s->leader_fd_saved = r > 0; +- +- if (s->leader_fd_saved) +- /* The leader fd will be acquired from fdstore later */ +- return 0; + } + +- _cleanup_(pidref_done) PidRef p = PIDREF_NULL; ++ uint64_t pidfdid; ++ if (leader_pidfdid) { ++ r = safe_atou64(leader_pidfdid, &pidfdid); ++ if (r < 0) ++ return log_error_errno(r, "Failed to parse LEADER_PIDFDID=%s: %m", leader_pidfdid); ++ } else ++ pidfdid = 0; + +- r = pidref_set_pid(&p, s->deserialized_pid); +- if (r >= 0) +- r = session_set_leader_consume(s, TAKE_PIDREF(p)); ++ r = session_load_leader(s, pidfdid); + if (r < 0) +- log_warning_errno(r, "Failed to set leader PID for session '%s': %m", s->id); ++ return r; + } + + return r; +diff --git a/src/login/logind.c b/src/login/logind.c +index 8dc1781edf..a7f75f0904 100644 +--- a/src/login/logind.c ++++ b/src/login/logind.c +@@ -36,6 +36,7 @@ + #include "selinux-util.h" + #include "service-util.h" + #include "signal-util.h" ++#include "stat-util.h" + #include "strv.h" + #include "terminal-util.h" + #include "udev-util.h" +@@ -442,19 +443,28 @@ static int deliver_session_leader_fd_consume(Session *s, const char *fdname, int + assert(fdname); + assert(fd >= 0); + +- if (!pid_is_valid(s->deserialized_pid)) { ++ /* Already deserialized via pidfd id? */ ++ if (pidref_is_set(&s->leader)) { ++ assert(s->leader.pid == s->deserialized_pid); ++ assert(s->leader.fd >= 0); ++ ++ r = fd_inode_same(fd, s->leader.fd); ++ if (r < 0) ++ return log_warning_errno(r, "Failed to compare pidfd with deserialized leader for session '%s': %m", ++ s->id); ++ if (r > 0) ++ return 0; ++ ++ log_warning("Got leader pidfd for session '%s' which mismatches with the deserialized process, resetting with pidfd.", ++ s->id); ++ ++ } else if (!pid_is_valid(s->deserialized_pid)) { + r = log_warning_errno(SYNTHETIC_ERRNO(EOWNERDEAD), + "Got leader pidfd for session '%s', but LEADER= is not set, refusing.", + s->id); + goto fail_close; + } + +- if (!s->leader_fd_saved) +- log_warning("Got leader pidfd for session '%s', but not recorded in session state, proceeding anyway.", +- s->id); +- else +- assert(!pidref_is_set(&s->leader)); +- + r = pidref_set_pidfd_take(&leader_fdstore, fd); + if (r < 0) { + if (r == -ESRCH) diff --git a/0606-TEST-35-LOGIN-test-coldplug-without-fdstore-on-kerne.patch b/0606-TEST-35-LOGIN-test-coldplug-without-fdstore-on-kerne.patch new file mode 100644 index 0000000..faac134 --- /dev/null +++ b/0606-TEST-35-LOGIN-test-coldplug-without-fdstore-on-kerne.patch @@ -0,0 +1,36 @@ +From 969bae840bf3c9be3697ca00e3ece8ec32a80a31 Mon Sep 17 00:00:00 2001 +From: Mike Yuan +Date: Fri, 24 Oct 2025 23:40:12 +0200 +Subject: [PATCH] TEST-35-LOGIN: test coldplug without fdstore on kernels with + pidfd id + +(cherry picked from commit ebb730b96ded7674994235340c0d4a39af174f52) + +Related: RHEL-53112 +--- + test/units/TEST-35-LOGIN.sh | 8 +++++--- + 1 file changed, 5 insertions(+), 3 deletions(-) + +diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh +index 80320e32e1..755249140e 100755 +--- a/test/units/TEST-35-LOGIN.sh ++++ b/test/units/TEST-35-LOGIN.sh +@@ -32,13 +32,15 @@ Environment=SYSTEMD_LOG_LEVEL=debug + EOF + + # We test "coldplug" (completely stop and start logind) here. So we need to preserve +- # the fdstore, which might contain session leader pidfds. This is extremely rare use case +- # and shall not be considered fully supported. ++ # the fdstore, which might contain session leader pidfds, but only if pidfd id isn't ++ # a thing. This is extremely rare use case and shall not be considered fully supported. + # See also: https://github.com/systemd/systemd/pull/30610#discussion_r1440507850 +- systemctl edit --runtime --stdin systemd-logind.service --drop-in=fdstore-preserve.conf < +Date: Thu, 6 Nov 2025 19:31:18 +0100 +Subject: [PATCH] logind: fix potential fd leak in + deliver_session_leader_fd_consume() + +Follow-up for 45eea629e3b3a640bf6a5cd13f4c73c86b426b11 + +(cherry picked from commit c54112bdee1d32934e688961ca53e81ffff0c99a) + +Related: RHEL-53112 +--- + src/login/logind.c | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/src/login/logind.c b/src/login/logind.c +index a7f75f0904..afa9b1d42c 100644 +--- a/src/login/logind.c ++++ b/src/login/logind.c +@@ -435,13 +435,13 @@ static int deliver_session_device_fd(Session *s, const char *fdname, int fd, dev + return 0; + } + +-static int deliver_session_leader_fd_consume(Session *s, const char *fdname, int fd) { ++static int deliver_session_leader_fd_consume(Session *s, const char *fdname, int fd_consume) { ++ _cleanup_close_ int fd = ASSERT_FD(fd_consume); + _cleanup_(pidref_done) PidRef leader_fdstore = PIDREF_NULL; + int r; + + assert(s); + assert(fdname); +- assert(fd >= 0); + + /* Already deserialized via pidfd id? */ + if (pidref_is_set(&s->leader)) { +@@ -473,6 +473,7 @@ static int deliver_session_leader_fd_consume(Session *s, const char *fdname, int + log_warning_errno(r, "Failed to create reference to leader of session '%s': %m", s->id); + goto fail_close; + } ++ TAKE_FD(fd); + + if (leader_fdstore.pid != s->deserialized_pid) + log_warning("Leader from pidfd (" PID_FMT ") doesn't match with LEADER=" PID_FMT " for session '%s', proceeding anyway.", +@@ -485,7 +486,7 @@ static int deliver_session_leader_fd_consume(Session *s, const char *fdname, int + return 0; + + fail_close: +- close_and_notify_warn(fd, fdname); ++ close_and_notify_warn(TAKE_FD(fd), fdname); + return r; + } + diff --git a/0608-Revert-coredump-lock-down-EnterNamespace-mount-even-.patch b/0608-Revert-coredump-lock-down-EnterNamespace-mount-even-.patch new file mode 100644 index 0000000..37a6b4d --- /dev/null +++ b/0608-Revert-coredump-lock-down-EnterNamespace-mount-even-.patch @@ -0,0 +1,41 @@ +From b19c1dbe3962ba716555284136a23fb0b8da9e3a Mon Sep 17 00:00:00 2001 +From: Michal Sekletar +Date: Fri, 14 Mar 2025 09:16:17 +0100 +Subject: [PATCH] Revert "coredump: lock down EnterNamespace= mount even more" + +This reverts commit 4c9c8b8d09eff18df71ba4aa910df4201f9890a0. + +Reverted change broke EnterNamespace= completely. For example, libdw +tries to access libc in /lib64 which points to usr/lib64 and that fails. +Similarly for binaries, we need to be able to resolve /bin to usr/bin +and /sbin to usr/sbin at the very least. + +(cherry picked from commit 8f8148cb08bf9f2c0e1f7fe6a5e6eb383115957b) + +Related: RHEL-95219 +--- + src/coredump/coredump.c | 11 +++++++---- + 1 file changed, 7 insertions(+), 4 deletions(-) + +diff --git a/src/coredump/coredump.c b/src/coredump/coredump.c +index ea4f8805f4..9665b3fe3e 100644 +--- a/src/coredump/coredump.c ++++ b/src/coredump/coredump.c +@@ -832,10 +832,13 @@ static int attach_mount_tree(int mount_tree_fd) { + return log_warning_errno(r, "Failed to create directory: %m"); + + r = mount_setattr(mount_tree_fd, "", AT_EMPTY_PATH, +- &(struct mount_attr) { +- .attr_set = MOUNT_ATTR_RDONLY|MOUNT_ATTR_NOSUID|MOUNT_ATTR_NODEV|MOUNT_ATTR_NOEXEC|MOUNT_ATTR_NOSYMFOLLOW, +- .propagation = MS_SLAVE, +- }, sizeof(struct mount_attr)); ++ &(struct mount_attr) { ++ /* MOUNT_ATTR_NOSYMFOLLOW is left out on purpose to allow libdwfl to resolve symlinks. ++ * libdwfl will use openat2() with RESOLVE_IN_ROOT so there is no risk of symlink escape. ++ * https://sourceware.org/git/?p=elfutils.git;a=patch;h=06f0520f9a78b07c11c343181d552791dd630346 */ ++ .attr_set = MOUNT_ATTR_RDONLY|MOUNT_ATTR_NOSUID|MOUNT_ATTR_NODEV|MOUNT_ATTR_NOEXEC, ++ .propagation = MS_SLAVE, ++ }, sizeof(struct mount_attr)); + if (r < 0) + return log_warning_errno(errno, "Failed to change properties of mount tree: %m"); + diff --git a/0609-coredump-add-compat-support-for-SYSTEMD_COREDUMP_ALL.patch b/0609-coredump-add-compat-support-for-SYSTEMD_COREDUMP_ALL.patch new file mode 100644 index 0000000..6949831 --- /dev/null +++ b/0609-coredump-add-compat-support-for-SYSTEMD_COREDUMP_ALL.patch @@ -0,0 +1,40 @@ +From 0f03838ff135c027c249e8212f51957a4109cfc0 Mon Sep 17 00:00:00 2001 +From: Michal Sekletar +Date: Mon, 9 Jun 2025 15:22:33 +0200 +Subject: [PATCH] coredump: add compat support for + SYSTEMD_COREDUMP_ALLOW_NAMESPACE_CHANGE + +Set SYSTEMD_COREDUMP_ALLOW_NAMESPACE_CHANGE is an equivalent to +EnterNamespace=yes in the configuration file. + +This compat support will be removed in RHEL-11. + +rhel-only: policy + +Related: RHEL-95219 +--- + src/coredump/coredump.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/src/coredump/coredump.c b/src/coredump/coredump.c +index 9665b3fe3e..0c51f62728 100644 +--- a/src/coredump/coredump.c ++++ b/src/coredump/coredump.c +@@ -29,6 +29,7 @@ + #include "coredump-vacuum.h" + #include "dirent-util.h" + #include "elf-util.h" ++#include "env-util.h" + #include "escape.h" + #include "fd-util.h" + #include "fileio.h" +@@ -1812,6 +1813,9 @@ static int acquire_pid_mount_tree_fd(const Context *context, int *ret_fd) { + assert(context); + assert(ret_fd); + ++ if (getenv_bool("SYSTEMD_COREDUMP_ALLOW_NAMESPACE_CHANGE") > 0) ++ arg_enter_namespace = true; ++ + if (!arg_enter_namespace) { + *ret_fd = -EHOSTDOWN; + log_debug("EnterNamespace=no so we won't use mount tree of the crashed process for generating backtrace."); diff --git a/systemd.spec b/systemd.spec index a104127..eeafe96 100644 --- a/systemd.spec +++ b/systemd.spec @@ -48,7 +48,7 @@ Url: https://systemd.io # Allow users to specify the version and release when building the rpm by # setting the %%version_override and %%release_override macros. Version: %{?version_override}%{!?version_override:257} -Release: 22%{?dist} +Release: 23%{?dist} %global stable %(c="%version"; [ "$c" = "${c#*.*}" ]; echo $?) @@ -684,6 +684,41 @@ Patch0571: 0571-userdbctl-optionally-show-user-group-data-from-JSON-.patch Patch0572: 0572-man-fix-typo.patch Patch0573: 0573-test-fix-test-with-Dnetworkd-false.patch Patch0574: 0574-ci-run-apt-get-update-before-running-mkosi.patch +Patch0575: 0575-test-journal-dump-dump-the-headers-of-journal-files.patch +Patch0576: 0576-journal-store-counts-not-byte-sizes-in-table-size-co.patch +Patch0577: 0577-journal-treble-field-hash-table-size.patch +Patch0578: 0578-nspawn-move-uid-shift-chown-code-into-shared.patch +Patch0579: 0579-user-classification-add-new-foreign-UID-range.patch +Patch0580: 0580-userdb-synthesize-stub-user-records-for-the-foreign-.patch +Patch0581: 0581-dissect-add-new-shift-command.patch +Patch0582: 0582-userdb-optionally-parse-numeric-UIDs-GIDs-where-a-us.patch +Patch0583: 0583-userdbd-separate-parameter-structure-of-GetMembershi.patch +Patch0584: 0584-userdb-move-setting-of-service-varlink-parameter-int.patch +Patch0585: 0585-user-record-make-a-NULL-UserDBMatch-be-equivalent-to.patch +Patch0586: 0586-sd-varlink-add-sd_varlink_get_description-call.patch +Patch0587: 0587-user-record-add-helper-for-dispatching-a-disposition.patch +Patch0588: 0588-user-record-rename-USER_DISPOSITION_MASK_MAX-USER_DI.patch +Patch0589: 0589-user-record-add-some-helpers-for-working-with-UserDB.patch +Patch0590: 0590-varlink-add-new-calls-for-server-side-user-record-fi.patch +Patch0591: 0591-userdb-move-UserDBMatch-handling-from-userdbctl-into.patch +Patch0592: 0592-userdbd-implement-server-side-filtering-in-the-Multi.patch +Patch0593: 0593-homectl-port-has_regular_user-acquire_group_list-to-.patch +Patch0594: 0594-update-TODO.patch +Patch0595: 0595-JSON-User-Group-records-Add-properties-for-UUIDs.patch +Patch0596: 0596-userdb-add-support-for-printing-the-UUID-from-user-a.patch +Patch0597: 0597-userdb-add-support-for-looking-up-users-or-groups-by.patch +Patch0598: 0598-userdbctl-add-uuid-filtering-option.patch +Patch0599: 0599-userdbctl-add-missing-uuid-to-help-text.patch +Patch0600: 0600-userdb-fix-typo.patch +Patch0601: 0601-man-userdbctl-fixup-version-info.patch +Patch0602: 0602-user-record-add-a-concept-of-inverting-per-host-matc.patch +Patch0603: 0603-homectl-add-interface-for-controlling-storage-for-ne.patch +Patch0604: 0604-logind-also-save-pidfdid-as-part-of-session-state-ev.patch +Patch0605: 0605-logind-support-deserializing-session-leader-through-.patch +Patch0606: 0606-TEST-35-LOGIN-test-coldplug-without-fdstore-on-kerne.patch +Patch0607: 0607-logind-fix-potential-fd-leak-in-deliver_session_lead.patch +Patch0608: 0608-Revert-coredump-lock-down-EnterNamespace-mount-even-.patch +Patch0609: 0609-coredump-add-compat-support-for-SYSTEMD_COREDUMP_ALL.patch # Downstream-only patches (9000–9999) %endif @@ -1635,6 +1670,43 @@ rm -f .file-list-* rm -f %{name}.lang %changelog +* Tue Feb 17 2026 systemd maintenance team - 257-23 +- test-journal-dump: dump the headers of journal files (RHEL-106795) +- journal: store counts, not byte sizes, in table size constants (RHEL-106795) +- journal: treble field hash table size (RHEL-106795) +- nspawn: move uid shift/chown() code into shared/ (RHEL-143036) +- user-classification: add new "foreign" UID range (RHEL-143036) +- userdb: synthesize stub user records for the foreign UID (RHEL-143036) +- dissect: add new --shift command (RHEL-143036) +- userdb: optionally parse numeric UIDs/GIDs where a username is expected (RHEL-143036) +- userdbd: separate parameter structure of GetMemberships() varlink call from the GetUserRecord() one (RHEL-143036) +- userdb: move setting of 'service' varlink parameter into userdb_connect() (RHEL-143036) +- user-record: make a NULL UserDBMatch be equivalent to no filtering (RHEL-143036) +- sd-varlink: add sd_varlink_get_description() call (RHEL-143036) +- user-record: add helper for dispatching a disposition mask (RHEL-143036) +- user-record: rename USER_DISPOSITION_MASK_MAX → USER_DISPOSITION_MASK_ALL (RHEL-143036) +- user-record: add some helpers for working with UserDBMatch (RHEL-143036) +- varlink: add new calls for server-side user record filtering to varlink IDL + to spec (RHEL-143036) +- userdb: move UserDBMatch handling from userdbctl into generic userdb code to allow it to be done server side (RHEL-143036) +- userdbd: implement server side filtering in the Multiplexer API (RHEL-143036) +- homectl: port has_regular_user() + acquire_group_list() to use server-side filtering (RHEL-143036) +- update TODO (RHEL-143036) +- JSON User/Group records: Add properties for UUIDs (RHEL-143036) +- userdb: add support for printing the UUID from user and group records (RHEL-143036) +- userdb: add support for looking up users or groups by uuid. (RHEL-143036) +- userdbctl: add --uuid filtering option (RHEL-143036) +- userdbctl: add missing --uuid= to --help text (RHEL-143036) +- userdb: fix typo (RHEL-143036) +- man/userdbctl: fixup version info (RHEL-143036) +- user-record: add a concept of inverting per-host matching sections in user record (RHEL-143034) +- homectl: add interface for controlling storage for negative machine ID matches (RHEL-143034) +- logind: also save pidfdid as part of session state, even if we don't parse it (RHEL-53112) +- logind: support deserializing session leader through pidfdid (RHEL-53112) +- TEST-35-LOGIN: test coldplug without fdstore on kernels with pidfd id (RHEL-53112) +- logind: fix potential fd leak in deliver_session_leader_fd_consume() (RHEL-53112) +- Revert "coredump: lock down EnterNamespace= mount even more" (RHEL-95219) +- coredump: add compat support for SYSTEMD_COREDUMP_ALLOW_NAMESPACE_CHANGE (RHEL-95219) + * Fri Feb 06 2026 systemd maintenance team - 257-22 - fstab-generator: fix options in systemd.mount-extra= arg (RHEL-125822) - hwdb: Add Accelerometer mount matrix for Irbis TW43 (RHEL-72702)