systemd-257-23

Resolves: RHEL-106795, RHEL-143036, RHEL-143034, RHEL-53112, RHEL-95219
This commit is contained in:
Jan Macku 2026-02-17 15:53:20 +01:00
parent bf06933ed4
commit 363d20f6ec
36 changed files with 5935 additions and 1 deletions

View File

@ -0,0 +1,84 @@
From 53118a0bc2f4855cb90ba24c674c3c6044ea6e3d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= <zbyszek@in.waw.pl>
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);

View File

@ -0,0 +1,94 @@
From f9594f04c3a37ad8dab8f62bbeff67adc2e5ca44 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= <zbyszek@in.waw.pl>
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;
}

View File

@ -0,0 +1,243 @@
From 1aea0cf1757ce56a47f915aa89043fd5415d1f58 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= <zbyszek@in.waw.pl>
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

View File

@ -0,0 +1,375 @@
From f9eb81c4c764d4d0c5a0a9265f805be6f575f02d Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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 <sys/types.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))
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 <fcntl.h>
#include <sys/statvfs.h>
-#include <sys/vfs.h>
-#include <unistd.h>
#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 <stdlib.h>
#include "log.h"
-#include "nspawn-patch-uid.h"
+#include "shift-uid.h"
#include "user-util.h"
#include "string-util.h"
#include "tests.h"

View File

@ -0,0 +1,319 @@
From 69b8c163b4785b80195d109333776a416b9c4318 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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,

View File

@ -0,0 +1,388 @@
From de19e6fd05fe14ef0cf661abc0a087318019ed6b Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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 @@
<term><option>--synthesize=<replaceable>BOOL</replaceable></option></term>
<listitem><para>Controls whether to synthesize records for the root and nobody users/groups if they
- aren't defined otherwise. By default (or <literal>yes</literal>) such records are implicitly
- synthesized if otherwise missing since they have special significance to the OS. When
- <literal>no</literal> this synthesizing is turned off.</para>
+ are not defined otherwise, as well as the user/groups for the "foreign" UID range. By default (or with
+ <literal>yes</literal>), such records are implicitly synthesized if otherwise missing since they have
+ special significance to the OS. When <literal>no</literal>, this synthesizing is turned off.</para>
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
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:

View File

@ -0,0 +1,199 @@
From 9bda47d0ed718f7f7fe928dc0098ca13f139834e Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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 @@
<cmdsynopsis>
<command>systemd-dissect</command> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg>--validate</arg> <arg choice="plain"><replaceable>IMAGE</replaceable></arg>
</cmdsynopsis>
+ <cmdsynopsis>
+ <command>systemd-dissect</command> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg>--shift</arg> <arg choice="plain"><replaceable>IMAGE</replaceable></arg> <arg choice="plain"><replaceable>UIDBASE</replaceable></arg>
+ </cmdsynopsis>
</refsynopsisdiv>
<refsect1>
@@ -350,6 +353,27 @@
<xi:include href="version-info.xml" xpointer="v254"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--shift</option></term>
+
+ <listitem><para>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 <ulink
+ url="https://systemd.io/UIDS-GIDS/">Users, Groups, UIDs and GIDs on systemd Systems</ulink>), or as
+ the symbolic identifier <literal>foreign</literal> 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.</para>
+
+ <para>Use <command>systemd-dissect --shift /some/container/tree foreign</command> to shift a
+ container image into the foreign UID range, or <command>systemd-dissect --shift /some/container/tree
+ 0</command> to shift it to host UID range.</para>
+
+ <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+ </varlistentry>
+
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
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:

View File

@ -0,0 +1,95 @@
From 269ed9b1c475c27f7b96713d1927f89616c4a465 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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);

View File

@ -0,0 +1,128 @@
From 0bb32ee43fd41ed3914a9400bd2e2074e34f0f4b Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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;

View File

@ -0,0 +1,84 @@
From fcf21e465c847084f198b4ca2659acc011410b81 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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;

View File

@ -0,0 +1,44 @@
From 10ab6be5caf19592ebf487ff8075d5e0c17825b4 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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;

View File

@ -0,0 +1,175 @@
From e627c841a9e701c675a984e4ff01556c72b44baa Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="sd_varlink_set_description" xmlns:xi="http://www.w3.org/2001/XInclude">
+
+ <refentryinfo>
+ <title>sd_varlink_set_description</title>
+ <productname>systemd</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>sd_varlink_set_description</refentrytitle>
+ <manvolnum>3</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>sd_varlink_set_description</refname>
+ <refname>sd_varlink_get_description</refname>
+
+ <refpurpose>Set or query description of a Varlink connection object</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <funcsynopsis>
+ <funcsynopsisinfo>#include &lt;systemd/sd-link.h&gt;</funcsynopsisinfo>
+
+ <funcprototype>
+ <funcdef>int <function>sd_varlink_set_description</function></funcdef>
+ <paramdef>sd_varlink *<parameter>link</parameter></paramdef>
+ <paramdef>const char *<parameter>description</parameter></paramdef>
+ </funcprototype>
+
+ <funcprototype>
+ <funcdef>const char* <function>sd_varlink_get_description</function></funcdef>
+ <paramdef>sd_varlink *<parameter>link</parameter></paramdef>
+ </funcprototype>
+ </funcsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para><function>sd_varlink_set_description()</function> 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 <parameter>description</parameter> argument may be <constant>NULL</constant>,
+ in which case the description is unset.</para>
+
+ <para><function>sd_varlink_get_description()</function> returns a description string for the specified
+ Varlink connection. This string may have been previously set with
+ <function>sd_varlink_set_description()</function>. If not set this way, a default string or
+ <constant>NULL</constant> may be returned, depending how the connection was allocated and set up.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>Return Value</title>
+
+ <para>On success, <function>sd_varlink_set_description()</function> returns a non-negative integer. On
+ failure, it returns a negative errno-style error code. <function>sd_varlink_get_description()</function>
+ returns either <constant>NULL</constant> or a pointer to the description string.</para>
+
+ <refsect2>
+ <title>Errors</title>
+
+ <para>Returned errors may indicate the following problems:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><constant>-EINVAL</constant></term>
+
+ <listitem><para>An argument is invalid.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><constant>-ENOMEM</constant></term>
+
+ <listitem><para>Memory allocation failed.</para></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect2>
+ </refsect1>
+
+ <xi:include href="libsystemd-pkgconfig.xml" />
+
+ <refsect1>
+ <title>History</title>
+ <para><function>sd_varlink_set_description()</function> was added in version 257.</para>
+ <para><function>sd_varlink_get_description()</function> was added in version 258.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+
+ <para><simplelist type="inline">
+ <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>sd-varlink</refentrytitle><manvolnum>3</manvolnum></citerefentry></member>
+ </simplelist></para>
+ </refsect1>
+
+</refentry>
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);

View File

@ -0,0 +1,78 @@
From bcb78a95d52b54a3efd662a9e04f5a6f98f8d36e Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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 <sys/mount.h>
+#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_;

View File

@ -0,0 +1,45 @@
From a02347043677bb5218bdd4483fefae891da352d8 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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;

View File

@ -0,0 +1,55 @@
From 14dbb7e8f0a7640bbffe6d8152e4a998a86297d8 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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);

View File

@ -0,0 +1,143 @@
From 56df03dca7a70c341ec30fec489e2ee32531906b Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,162 @@
From 9b5c0f5201194158377da6a3366fa87743395b9b Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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);

View File

@ -0,0 +1,108 @@
From b81ac039917b08d298ae03d59a449ae7aee0df75 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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;
}

26
0594-update-TODO.patch Normal file
View File

@ -0,0 +1,26 @@
From bdbfe972548a78d6ac81986ccb70f54142fdd770 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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

View File

@ -0,0 +1,57 @@
From 6fe4e2e64df52c985acd6ccf02cc69415fbef116 Mon Sep 17 00:00:00 2001
From: Erin Shepherd <erin.shepherd@e43.eu>
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.

View File

@ -0,0 +1,89 @@
From eee22e76c180f9afb5401c8c5e9dcee55cd70c43 Mon Sep 17 00:00:00 2001
From: Erin Shepherd <erin.shepherd@e43.eu>
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;

View File

@ -0,0 +1,213 @@
From 6f5da55f271c36fbc55a6e5f343375b25978c2ed Mon Sep 17 00:00:00 2001
From: Erin Shepherd <erin.shepherd@e43.eu>
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 },
{}
};

View File

@ -0,0 +1,111 @@
From 9a92c83a44c68511b52302058a95de89239cbf15 Mon Sep 17 00:00:00 2001
From: Erin Shepherd <erin.shepherd@e43.eu>
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 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--uuid=</option></term>
+
+ <listitem><para>When used with the <command>user</command> or <command>group</command> command,
+ filters the output to the record with the specified UUID. If unspecified, no UUID-based filtering is applied.</para>
+
+ <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+ </varlistentry>
+
<varlistentry>
<term><option>--boundaries=</option></term>
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;

View File

@ -0,0 +1,26 @@
From 09282345b595d817725be5333baa08e16ce1f8de Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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"

View File

@ -0,0 +1,27 @@
From d4761084258e69b0f02d5802635ff943607b7625 Mon Sep 17 00:00:00 2001
From: Yu Watanabe <watanabe.yu+github@gmail.com>
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) {

View File

@ -0,0 +1,27 @@
From f1e64d4b3583e78e4fbbee9f21e663974d0af1fe Mon Sep 17 00:00:00 2001
From: Mike Yuan <me@yhndnzj.com>
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 @@
<listitem><para>When used with the <command>user</command> or <command>group</command> command,
filters the output to the record with the specified UUID. If unspecified, no UUID-based filtering is applied.</para>
- <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+ <xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<varlistentry>

View File

@ -0,0 +1,80 @@
From 41d06bdfa06e1a230661a4281b4003cb53ba3a71 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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 },

View File

@ -0,0 +1,647 @@
From 9df0f85a5a9a27a135f8caab544827932170c585 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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 @@
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><option>--match=</option></term>
+ <term><option>-A</option></term>
+ <term><option>-N</option></term>
+ <term><option>-T</option></term>
+
+ <listitem><para>Takes one of <literal>this</literal>, <literal>other</literal>,
+ <literal>any</literal> or <literal>auto</literal>. 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
+ <literal>this</literal> is specified the setting will only apply to the local system (positive
+ match), if <literal>other</literal> it will apply to all but the local system (negative match), if
+ <literal>any</literal> it will apply to all systems (unless there's a matching positive or negative
+ per-machine setting). If <literal>auto</literal> returns to the default logic: whether a setting
+ applies by default to the local system or all systems depends on the option in question.</para>
+
+ <para>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 <ulink
+ url="https://systemd.io/USER_RECORD">JSON User Records</ulink> for details on which settings may be
+ used with such per-machine matching and which ones may not.</para>
+
+ <para><option>-A</option> is a shortcut for <option>--match=any</option>, <option>-T</option> is
+ short for <option>--match=this</option> and <option>-N</option> is short for
+ <option>--match=other</option>.</para>
+
+ <para>Here's an example call that sets the storage field to <literal>luks</literal> on the local
+ system, but to <literal>cifs</literal> on all others:</para>
+
+ <programlisting># homectl update lennart -T --storage=luks -N --storage=cifs</programlisting>
+
+ <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+ </varlistentry>
+
<xi:include href="user-system-options.xml" xpointer="host" />
<xi:include href="user-system-options.xml" xpointer="machine" />
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;

View File

@ -0,0 +1,31 @@
From 119cca5054f04e1afefc15c626ed1b2353fdb080 Mon Sep 17 00:00:00 2001
From: Lennart Poettering <lennart@poettering.net>
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);

View File

@ -0,0 +1,179 @@
From a19c25b8b5edbfa9a47f6648862cdea6938ee455 Mon Sep 17 00:00:00 2001
From: Mike Yuan <me@yhndnzj.com>
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)

View File

@ -0,0 +1,36 @@
From 969bae840bf3c9be3697ca00e3ece8ec32a80a31 Mon Sep 17 00:00:00 2001
From: Mike Yuan <me@yhndnzj.com>
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 <<EOF
+ if systemd-analyze compare-versions "$(uname -r)" lt 6.9; then
+ systemctl edit --runtime --stdin systemd-logind.service --drop-in=fdstore-preserve.conf <<EOF
[Service]
FileDescriptorStorePreserve=yes
EOF
+ fi
systemctl restart systemd-logind.service
}

View File

@ -0,0 +1,52 @@
From aa181fefdd039d74a20ccce431f12f6b5fa9b637 Mon Sep 17 00:00:00 2001
From: Mike Yuan <me@yhndnzj.com>
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;
}

View File

@ -0,0 +1,41 @@
From b19c1dbe3962ba716555284136a23fb0b8da9e3a Mon Sep 17 00:00:00 2001
From: Michal Sekletar <msekleta@redhat.com>
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");

View File

@ -0,0 +1,40 @@
From 0f03838ff135c027c249e8212f51957a4109cfc0 Mon Sep 17 00:00:00 2001
From: Michal Sekletar <msekleta@redhat.com>
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.");

View File

@ -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 (90009999)
%endif
@ -1635,6 +1670,43 @@ rm -f .file-list-*
rm -f %{name}.lang
%changelog
* Tue Feb 17 2026 systemd maintenance team <systemd-maint@redhat.com> - 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 <systemd-maint@redhat.com> - 257-22
- fstab-generator: fix options in systemd.mount-extra= arg (RHEL-125822)
- hwdb: Add Accelerometer mount matrix for Irbis TW43 (RHEL-72702)