From 5bf42ddadd6d46cb773834d7b9e19efcc6b1da01 Mon Sep 17 00:00:00 2001 From: Krister Johansen Date: Fri, 25 Sep 2026 14:12:38 -0700 Subject: [PATCH] nss_cache: implement initgroups_dyn glibc's libnss has a callback for initgroups(3) which it uses preferentially to getgrent_r. The getgrent_r fallback is used if initgroups_dyn is not implemented, but glibc does not have any serialization around the callback. This means that if multiple threads call initgroups simultaneously and the nss backend does not implment initgroups_dyn, then it's possible the supplemental group lists will become truncated as multiple getgrent_r calls race with one another. Fix this in libnss_cache by implementing an initgroups_dyn callback. This callback is written in the style of nss-files but with the libnss-cache idioms. It holds the cache lock across the getgrent_r iteration to ensure no supplemental group calls are truncated. This was tested by reproducing the truncation race with an fresh nscd that is bombarded with id -G calls. Without this fix present, it's possible to observe truncated results. However, with initgroups_dyn users always get their correct supplemental groups. --- nss_cache.c | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/nss_cache.c b/nss_cache.c index 0b2a3b8..a74029a 100644 --- a/nss_cache.c +++ b/nss_cache.c @@ -746,6 +746,108 @@ enum nss_status _nss_cache_getgrnam_r(const char *name, struct group *result, return ret; } +// _nss_cache_initgroups_dyn() +// Find the supplementary groups for a user. + +#define NSS_CACHE_INITGROUPS_BUFLEN (1 << 20) + +enum nss_status _nss_cache_initgroups_dyn(const char *user, gid_t group, + long int *start, long int *size, + gid_t **groupsp, long int limit, + int *errnop) { + char *buffer; + size_t buflen = NSS_CACHE_INITGROUPS_BUFLEN; + enum nss_status ret; + int any = 0; + + buffer = malloc(buflen); + if (buffer == NULL) { + *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + NSS_CACHE_LOCK(); + ret = _nss_cache_setgrent_locked(); + if (ret != NSS_STATUS_SUCCESS) { + *errnop = errno; + } + + while (ret == NSS_STATUS_SUCCESS) { + struct group result; + char **member; + + ret = _nss_cache_getgrent_r_locked(&result, buffer, buflen, errnop); + if (ret == NSS_STATUS_TRYAGAIN && *errnop == ERANGE) { + free(buffer); + buflen *= 2; + buffer = malloc(buflen); + if (buffer == NULL) { + *errnop = ENOMEM; + ret = NSS_STATUS_TRYAGAIN; + break; + } + ret = NSS_STATUS_SUCCESS; + continue; + } + if (ret != NSS_STATUS_SUCCESS) { + break; + } + + // The caller has already included the primary group. + if (result.gr_gid == group) { + continue; + } + + for (member = result.gr_mem; *member != NULL; member++) { + if (strcmp(*member, user) == 0) { + gid_t *groups = *groupsp; + + if (*start == *size) { + gid_t *newgroups; + long int newsize; + + if (limit > 0 && *size == limit) { + goto out; + } + + if (limit <= 0) + newsize = 2 * *size; + else + newsize = MIN(limit, 2 * *size); + + newgroups = realloc(groups, newsize * sizeof(*groups)); + if (newgroups == NULL) { + *errnop = ENOMEM; + ret = NSS_STATUS_TRYAGAIN; + goto out; + } + *groupsp = groups = newgroups; + *size = newsize; + } + + groups[*start] = result.gr_gid; + *start += 1; + any = 1; + break; + } + } + } + + if (ret == NSS_STATUS_NOTFOUND) { + ret = NSS_STATUS_SUCCESS; + } + +out: + _nss_cache_endgrent_locked(); + NSS_CACHE_UNLOCK(); + free(buffer); + + if (ret == NSS_STATUS_SUCCESS && !any) { + ret = NSS_STATUS_NOTFOUND; + } + return ret; +} + // // Routines for shadow map defined here. //