From: Lennart Poettering Date: Thu, 9 Jul 2026 13:35:38 +0000 (+0200) Subject: userdb: suppress lookups outside of indicated ranges X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=00a7b1d2d8d5de7548da69123832b913467235c0;p=thirdparty%2Fsystemd.git 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. --- diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md index cf83f19f222..c4e187c6669 100644 --- a/docs/USER_GROUP_API.md +++ b/docs/USER_GROUP_API.md @@ -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 diff --git a/src/shared/userdb.c b/src/shared/userdb.c index 7e028baf0a9..7dd9ac7496f 100644 --- a/src/shared/userdb.c +++ b/src/shared/userdb.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include @@ -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);