]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
userdb: suppress lookups outside of indicated ranges
authorLennart Poettering <lennart@amutable.com>
Thu, 9 Jul 2026 13:35:38 +0000 (15:35 +0200)
committerLennart Poettering <lennart@amutable.com>
Sun, 12 Jul 2026 21:09:01 +0000 (23:09 +0200)
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.

docs/USER_GROUP_API.md
src/shared/userdb.c

index cf83f19f2224d0d72329bb0c88425fa489ea16a4..c4e187c66690e8dcb00e937af26dc0015072bb60 100644 (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
index 7e028baf0a969e0153474264583813b56bd7a1ee..7dd9ac7496fa5e08de1094b5e1dffc8c1453917e 100644 (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);