userdb: suppress userdb queries for backends indicating uid/gid/name range info via xattrs on entrypoint sockets (#42961)

Let's optimize userdb queries a bit: by encoding the covered UID/GID
ranges and user/group name patterns on the varlink entrypoint sockets
for userdb backends we can make them wake up less and reduce the work
triggered by queries.
This commit is contained in:
Luca Boccassi
2026-07-14 00:19:32 +01:00
committed by GitHub
8 changed files with 313 additions and 16 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
+17 -4
View File
@@ -4,12 +4,14 @@
#include "constants.h"
#include "errno-util.h"
#include "format-util.h"
#include "job.h"
#include "json-util.h"
#include "manager.h"
#include "metrics.h"
#include "path-util.h"
#include "pidref.h"
#include "stdio-util.h"
#include "string-util.h"
#include "strv.h"
#include "unit.h"
@@ -27,6 +29,7 @@
#include "varlink-serialize.h"
#include "varlink-unit.h"
#include "varlink-util.h"
#include "xattr-util.h"
static const char* const managed_oom_mode_properties[] = {
"ManagedOOMSwap",
@@ -481,10 +484,14 @@ int manager_setup_varlink_metrics_server(Manager *m) {
if (MANAGER_IS_SYSTEM(m))
flags |= SD_VARLINK_SERVER_ACCOUNT_UID;
return metrics_setup_varlink_server(&m->metrics_varlink_server, flags,
m->event, EVENT_PRIORITY_IPC,
vl_method_list_metrics, vl_method_describe_metrics,
m);
return metrics_setup_varlink_server(
&m->metrics_varlink_server,
flags,
m->event,
EVENT_PRIORITY_IPC,
vl_method_list_metrics,
vl_method_describe_metrics,
m);
}
static int varlink_server_listen_many_idempotent_sentinel(
@@ -549,6 +556,12 @@ static int manager_varlink_init_system_api(Manager *m) {
VARLINK_PATH_MANAGED_OOM_SYSTEM);
if (r < 0)
return r;
/* Reduce the noise routed to us: advertise that only look-ups for the dynamic UID range should go to us */
char text[DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(gid_t) + 1];
xsprintf(text, UID_FMT "-" UID_FMT, (uid_t) DYNAMIC_UID_MIN, (uid_t) DYNAMIC_UID_MAX);
FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid")
(void) xsetxattr(AT_FDCWD, "/run/systemd/userdb/io.systemd.DynamicUser", /* at_flags= */ 0, xattr, text);
}
return 0;
+8
View File
@@ -65,6 +65,7 @@
#include "varlink-io.systemd.UserDatabase.h"
#include "varlink-io.systemd.service.h"
#include "varlink-util.h"
#include "xattr-util.h"
/* Where to look for private/public keys that are used to sign the user records. We are not using
* CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that
@@ -1102,6 +1103,13 @@ static int manager_bind_varlink(Manager *m) {
if (r < 0)
return log_error_errno(r, "Failed to bind to varlink socket '%s': %m", socket_path);
/* Reduce the noise routed to us: advertise that only look-ups for non-system users in the 16bit
* range are routed to us (excluding 'nobody', i.e. 0xfffe) */
char text[DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(gid_t) + 1];
xsprintf(text, UID_FMT "-" UID_FMT, (uid_t) (SYSTEM_UID_MAX + 1), UID_NOBODY - 1);
FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid")
(void) xsetxattr(AT_FDCWD, socket_path, /* at_flags= */ 0, xattr, text);
r = sd_varlink_server_attach_event(m->varlink_server, m->event, SD_EVENT_PRIORITY_NORMAL);
if (r < 0)
return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
+9
View File
@@ -31,6 +31,7 @@
#include "varlink-io.systemd.Resolve.Hook.h"
#include "varlink-io.systemd.service.h"
#include "varlink-util.h"
#include "xattr-util.h"
typedef struct LookupParameters {
const char *user_name;
@@ -774,6 +775,14 @@ static int manager_varlink_init_userdb(Manager *m) {
if (r < 0)
return log_error_errno(r, "Failed to bind to varlink socket %s: %m", VARLINK_PATH_MACHINED_USERDB);
/* Reduce the noise routed to us: advertise that only look-ups for the higher UID range */
char text[DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(gid_t) + 1];
xsprintf(text, UID_FMT "-" UID_FMT, (uid_t) 0x10000U, (uid_t) UINT32_C(0xfffffffe));
FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid")
(void) xsetxattr(AT_FDCWD, VARLINK_PATH_MACHINED_USERDB, /* at_flags= */ 0, xattr, text);
(void) xsetxattr(AT_FDCWD, VARLINK_PATH_MACHINED_USERDB, /* at_flags= */ 0, "user.userdb.username", "vu-*");
(void) xsetxattr(AT_FDCWD, VARLINK_PATH_MACHINED_USERDB, /* at_flags= */ 0, "user.userdb.groupname", "vg-*");
r = sd_varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL);
if (r < 0)
return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
+12
View File
@@ -33,10 +33,12 @@
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "uid-classification.h"
#include "umask-util.h"
#include "unaligned.h"
#include "userns-registry.h"
#include "userns-restrict.h"
#include "xattr-util.h"
#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC)
@@ -450,6 +452,16 @@ static int manager_make_listen_socket(Manager *m) {
if (r < 0)
return log_error_errno(r, "Failed to symlink userdb socket: %m");
/* Reduce the noise routed to us: advertise that only look-ups for the higher UID range */
char text[2 * (DECIMAL_STR_MAX(uid_t) + 1 + DECIMAL_STR_MAX(uid_t) + 1)];
xsprintf(text, UID_FMT "-" UID_FMT "," UID_FMT "-" UID_FMT,
CONTAINER_UID_MIN, CONTAINER_UID_MAX,
(uid_t) DYNAMIC_UID_MIN, (uid_t) DYNAMIC_UID_MAX);
FOREACH_STRING(xattr, "user.userdb.uid", "user.userdb.gid")
(void) xsetxattr(AT_FDCWD, sockaddr.un.sun_path, /* at_flags= */ 0, xattr, text);
(void) xsetxattr(AT_FDCWD, sockaddr.un.sun_path, /* at_flags= */ 0, "user.userdb.username", "ns-*");
(void) xsetxattr(AT_FDCWD, sockaddr.un.sun_path, /* at_flags= */ 0, "user.userdb.groupname", "ns-*");
if (listen(m->listen_fd, SOMAXCONN) < 0)
return log_error_errno(errno, "Failed to listen on socket: %m");
+231 -11
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)
@@ -1418,13 +1603,13 @@ static int groupdb_by_gid_fallbacks(
assert(iterator);
assert(ret);
if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !(iterator && iterator->dropin_covered)) {
if (!FLAGS_SET(flags, USERDB_EXCLUDE_DROPIN) && !iterator->dropin_covered) {
r = dropin_group_record_by_gid(gid, NULL, flags, ret);
if (r >= 0)
return r;
}
if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && !(iterator && iterator->nss_covered)) {
if (!FLAGS_SET(flags, USERDB_EXCLUDE_NSS) && !iterator->nss_covered) {
r = userdb_iterator_block_nss_systemd(iterator);
if (r >= 0) {
r = nss_group_record_by_gid(gid, !FLAGS_SET(flags, USERDB_SUPPRESS_SHADOW), ret);
@@ -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);
+1 -1
View File
@@ -1040,7 +1040,7 @@ units = [
'conditions' : ['ENABLE_NSRESOURCED'],
},
{
'file' : 'systemd-nsresourced.socket',
'file' : 'systemd-nsresourced.socket.in',
'conditions' : ['ENABLE_NSRESOURCED'],
},
{
@@ -22,6 +22,10 @@ FileDescriptorName=varlink
SocketMode=0666
XAttrEntryPoint=user.varlink=entrypoint
XAttrListen=user.varlink=listen
XAttrEntryPoint=user.userdb.uid={{DYNAMIC_UID_MIN}}-{{DYNAMIC_UID_MAX}},{{CONTAINER_UID_BASE_MIN}}-{{CONTAINER_UID_BASE_MAX+65535}}
XAttrEntryPoint=user.userdb.gid={{DYNAMIC_UID_MIN}}-{{DYNAMIC_UID_MAX}},{{CONTAINER_UID_BASE_MIN}}-{{CONTAINER_UID_BASE_MAX+65535}}
XAttrEntryPoint=user.userdb.username=ns-*
XAttrEntryPoint=user.userdb.groupname=ns-*
[Install]
WantedBy=sockets.target