userdb: suppress lookups outside of indicated ranges

When doing a userdb lookup we might end up issuing a lot of IPC calls in
parallel to backends and cause them all to do work, with us waiting for
it. Let's optimize this a bit, and indicate on the socket inodes via
xattrs hints which kind of records are provided by a backend. That way
we can suppress lookups to them and optimize runtime behaviour.

This only works on Linux 7.1 and newer where socket inodes gained
support for extended attributes.
This commit is contained in:
Lennart Poettering
2026-07-12 23:09:01 +02:00
parent 65796105a9
commit 00a7b1d2d8
2 changed files with 260 additions and 9 deletions
+31
View File
@@ -327,6 +327,37 @@ Services which are asked for enumeration may return the `EnumerationNotSupported
And that's really all there is to it.
## Reducing IPC Requests
In many cases backends are only interested in responding to certain UID/GID
ranges, or only to user/group names matching a specific pattern. To optimize
look-ups in this case it is possible to indicate on the Varlink entrypoint
socket inode of the service which ranges or patterns these are: if the kernel
supports extended attributes on socket inodes (Linux >= 7.1), the following
extended attributes may be set:
* `user.userdb.uid` takes a comma separated list of UIDs or dash-separated UID
range pairs. If set, then the service will not be contacted for any UID
look-ups outside of this range. Example: setting the extended attribute to
`61184-65519,524288-1879048191,4711` will have the effect that a lookup for
UID 1051 will not be attempted against this service, but one for 62334 will be.
* `user.userdb.gid` is similar, but for GIDs and GID ranges.
* `user.userdb.username` takes a NUL separated list of `fnmatch()` style glob
expressions. If set only look-ups for user names matching at least one of
these expressions will be routed to this service. Example: setting the
extended attribute to `foo-*\0www*h?` (where `\0` shall indicate the NUL
byte) will route only look-ups for `foo-bar` and `wwwooosh7` to this service,
but not `waldo`.
* `user.userdb.groupname` is similar, but for group names.
Note that these extended attributes are intended for optimization only: any
backend *must* be prepared to answer look-ups for any UID, GID, user and group
name, and respond with `NoRecordFound` for requests outside of the
ranges/patterns defined this way.
## Command Line Access
For command line access you can use the
+229 -9
View File
@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fnmatch.h>
#include <gshadow.h>
#include <stdlib.h>
@@ -18,6 +19,7 @@
#include "log.h"
#include "parse-util.h"
#include "set.h"
#include "socket-util.h"
#include "string-util.h"
#include "strv.h"
#include "uid-classification.h"
@@ -25,6 +27,7 @@
#include "user-util.h"
#include "userdb.h"
#include "userdb-dropin.h"
#include "xattr-util.h"
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(link_hash_ops, void, trivial_hash_func, trivial_compare_func, sd_varlink, sd_varlink_unref);
@@ -35,6 +38,14 @@ typedef enum LookupWhat {
_LOOKUP_WHAT_MAX,
} LookupWhat;
typedef struct SocketFilter {
/* Restricts the sockets to connect to */
uid_t uid;
gid_t gid;
const char *username;
const char *groupname;
} SocketFilter;
struct UserDBIterator {
LookupWhat what;
UserDBFlags flags;
@@ -505,11 +516,138 @@ static int userdb_connect(
return r;
}
static int socket_filter_id_test(
id_t id,
const char *xattr,
int dir_fd,
const char *fname,
const char *full_path /* for logging only */) {
int r;
assert(xattr);
assert(dir_fd >= 0);
assert(fname);
assert(full_path);
/* Generic implementation for both UIDs and GIDs */
if (!uid_is_valid(id))
return true;
/* Looks for an xattr of the specified name on the referenced socket entrypoint inode. Parses this
* as a pair of start UID/end UID. Matches the specified UID against it. If it matches yay. If it
* doesn't nay. If no UID is specified or no xattr set, also yay. */
_cleanup_free_ char *t = NULL;
r = getxattr_at_malloc(dir_fd, fname, xattr, AT_SYMLINK_FOLLOW, &t, /* ret_size= */ NULL);
if (r == -ENODATA)
return true; /* no such xattr? match! */
if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
log_debug_errno(r, "Extended attributes on sockets not supported after all, ignoring: %m");
return true; /* if sockets don't support xattrs here, then this is a match */
}
if (r < 0)
return log_debug_errno(r, "Unexpected error while reading extended attribute '%s' of '%s', ignoring: %m", xattr, full_path);
_cleanup_strv_free_ char **l = strv_split(t, ",");
if (!l)
return log_oom_debug();
STRV_FOREACH(i, l) {
/* Single UID or UID range */
uid_t x, y;
r = parse_uid_range(*i, &x, &y);
if (r < 0)
return log_debug_errno(r, "Malformed extended attribute '%s' on '%s', ignoring.", xattr, full_path);
if (id >= x && id <= y)
return true;
}
return false;
}
static int socket_filter_name_test(
const char *name,
const char *xattr,
int dir_fd,
const char *fname,
const char *full_path /* for logging only */) {
int r;
assert(xattr);
assert(dir_fd >= 0);
assert(fname);
assert(full_path);
/* Looks for an xattr of the specified name on the referenced socket entrypoint inode. Parses this
* as a NUL terminated string list. Matches the specified name against it via fnmatch(). If it matches
* yay. If it doesn't nay. If no name is specified or no xattr set, also yay. */
if (!name)
return true;
_cleanup_strv_free_ char **l = NULL;
r = getxattr_at_strv(dir_fd, fname, xattr, AT_SYMLINK_FOLLOW, &l);
if (r == -ENODATA)
return true; /* no such xattr? match! */
if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
log_debug_errno(r, "Extended attributes on sockets not supported after all, ignoring: %m");
return true; /* if sockets don't support xattrs here, then this is a match */
}
if (r < 0)
return log_debug_errno(r, "Unexpected error while reading extended attribute '%s' of '%s', ignoring: %m", xattr, full_path);
return strv_fnmatch_full(l, name, FNM_NOESCAPE, /* ret_matched_pos= */ NULL);
}
static int socket_filter_test(
const SocketFilter *sfilter,
int dir_fd,
const char *fname,
const char *full_path /* for logging only */) {
assert(dir_fd >= 0);
assert(fname);
assert(full_path);
/* Before we contact a Varlink service, let's see if there's any chance it might answer our questions
* at all. For such pre-filtering a userdb backend can install xattrs on their entrypoint inode to
* indicate that it's never going to answer outside of a certain UID/GID range or username/groupname
* pattern */
/* No filter means all sockets match */
if (!sfilter)
return 1;
/* No xattr support? then all sockets match. This check mostly exists to take benefit of the internal caching
* of the test. Either way we'll check below again. */
if (socket_xattr_supported() == 0)
return 1;
if (socket_filter_id_test(sfilter->uid, "user.userdb.uid", dir_fd, fname, full_path) == 0)
return 0;
if (socket_filter_id_test(sfilter->gid, "user.userdb.gid", dir_fd, fname, full_path) == 0)
return 0;
if (socket_filter_name_test(sfilter->username, "user.userdb.username", dir_fd, fname, full_path) == 0)
return 0;
if (socket_filter_name_test(sfilter->groupname, "user.userdb.groupname", dir_fd, fname, full_path) == 0)
return 0;
return 1;
}
static int userdb_start_query(
UserDBIterator *iterator,
const char *method, /* must be a static string, we are not going to copy this here! */
bool more,
sd_json_variant *query,
const SocketFilter *sfilter,
UserDBFlags flags) {
_cleanup_strv_free_ char **except = NULL, **only = NULL;
@@ -601,6 +739,12 @@ static int userdb_start_query(
if (!p)
return -ENOMEM;
r = socket_filter_test(sfilter, dirfd(d), de->d_name, p);
if (r < 0)
log_debug_errno(r, "Failed to filter sockets, ignoring: %m");
else if (r == 0)
continue;
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 */
@@ -928,8 +1072,20 @@ int userdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flags
if (!iterator)
return -ENOMEM;
SocketFilter sfilter = {
.uid = UID_INVALID,
.gid = GID_INVALID,
.username = name,
};
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ false, query, flags);
r = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetUserRecord",
/* more= */ false,
query,
&sfilter,
flags);
if (r >= 0) {
r = userdb_process(iterator, &ur, /* ret_group_record= */ NULL, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL);
if (r == -ENOEXEC) /* Found a user matching UID or name, but not filter. In this case the
@@ -1018,8 +1174,19 @@ int userdb_by_uid(uid_t uid, const UserDBMatch *match, UserDBFlags flags, UserRe
if (!iterator)
return -ENOMEM;
SocketFilter sfilter = {
.uid = uid,
.gid = GID_INVALID,
};
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ false, query, flags);
r = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetUserRecord",
/* more= */ false,
query,
&sfilter,
flags);
if (r >= 0) {
r = userdb_process(iterator, &ur, /* ret_group_record= */ NULL, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL);
if (r == -ENOEXEC)
@@ -1055,7 +1222,13 @@ int userdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **ret
if (!iterator)
return -ENOMEM;
qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", /* more= */ true, query, flags);
qr = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetUserRecord",
/* more= */ true,
query,
/* sfilter= */ NULL,
flags);
if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) {
r = userdb_iterator_block_nss_systemd(iterator);
@@ -1384,8 +1557,20 @@ int groupdb_by_name(const char *name, const UserDBMatch *match, UserDBFlags flag
if (!iterator)
return -ENOMEM;
SocketFilter sfilter = {
.uid = UID_INVALID,
.gid = GID_INVALID,
.groupname = name,
};
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ false, query, flags);
r = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetGroupRecord",
/* more= */ false,
query,
&sfilter,
flags);
if (r >= 0) {
r = userdb_process(iterator, /* ret_user_record= */ NULL, &gr, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL);
if (r == -ENOEXEC)
@@ -1470,8 +1655,19 @@ int groupdb_by_gid(gid_t gid, const UserDBMatch *match, UserDBFlags flags, Group
if (!iterator)
return -ENOMEM;
SocketFilter sfilter = {
.uid = UID_INVALID,
.gid = gid,
};
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ false, query, flags);
r = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetGroupRecord",
/* more= */ false,
query,
&sfilter,
flags);
if (r >= 0) {
r = userdb_process(iterator, /* ret_user_record= */ NULL, &gr, /* ret_user_name= */ NULL, /* ret_group_name= */ NULL);
if (r == -ENOEXEC)
@@ -1507,7 +1703,13 @@ int groupdb_all(const UserDBMatch *match, UserDBFlags flags, UserDBIterator **re
if (!iterator)
return -ENOMEM;
qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", /* more= */ true, query, flags);
qr = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetGroupRecord",
/* more= */ true,
query,
/* sfilter= */ NULL,
flags);
if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) {
r = userdb_iterator_block_nss_systemd(iterator);
@@ -1698,7 +1900,13 @@ int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **r
if (!iterator->filter_user_name)
return -ENOMEM;
qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags);
qr = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetMemberships",
/* more= */ true,
query,
/* sfilter= */ NULL,
flags);
if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) {
r = userdb_iterator_block_nss_systemd(iterator);
@@ -1743,7 +1951,13 @@ int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **
if (!iterator->filter_group_name)
return -ENOMEM;
qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags);
qr = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetMemberships",
/* more= */ true,
query,
/* sfilter= */ NULL,
flags);
if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) {
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
@@ -1789,7 +2003,13 @@ int membershipdb_all(UserDBFlags flags, UserDBIterator **ret) {
if (!iterator)
return -ENOMEM;
qr = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, NULL, flags);
qr = userdb_start_query(
iterator,
"io.systemd.UserDatabase.GetMemberships",
/* more= */ true,
/* query= */ NULL,
/* sfilter= */ NULL,
flags);
if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && (qr < 0 || !iterator->nss_covered)) {
r = userdb_iterator_block_nss_systemd(iterator);