]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
user-util: add lookup_pwent_in_files and lookup_grent_in_files
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 19 May 2026 14:22:07 +0000 (16:22 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Thu, 2 Jul 2026 15:19:18 +0000 (17:19 +0200)
Those functions will be used later. This commits adds the
implementations and tests for them.

glibc functions require ability to open nss modules, which requires
dlopen. Linking will fail if any of the nss-using functions are used.
So the following few commits will provide replacements for all of
them.

src/basic/user-util.c
src/basic/user-util.h
src/test/test-user-util.c

index a665a2d78018cf30bebdb59c121eb8092c209abf..8d9474fe214bb653fad7aa32a2658db6ecbdb889 100644 (file)
@@ -1090,6 +1090,87 @@ const char* get_home_root(void) {
         return "/home";
 }
 
+static int copy_struct_passwd(const struct passwd *pw, struct passwd **ret) {
+        assert(pw);
+        assert(ret);
+
+        size_t need_bytes = sizeof(struct passwd)
+                + strlen_ptr(pw->pw_name) + 1
+                + strlen_ptr(pw->pw_passwd) + 1
+                + strlen_ptr(pw->pw_gecos) + 1
+                + strlen_ptr(pw->pw_dir) + 1
+                + strlen_ptr(pw->pw_shell) + 1;
+
+        char *buf = malloc(need_bytes);
+        if (!buf)
+                return -ENOMEM;
+
+        struct passwd *newpw = (struct passwd *) buf;
+
+        /* The layout in our buffer:
+         * struct passwd, and then individual strings. */
+        char *p = buf + sizeof(struct passwd);
+
+        newpw->pw_name = p;
+        p = stpcpy(p, strempty(pw->pw_name)) + 1;
+
+        newpw->pw_passwd = p;
+        p = stpcpy(p, strempty(pw->pw_passwd)) + 1;
+
+        newpw->pw_uid = pw->pw_uid;
+        newpw->pw_gid = pw->pw_gid;
+
+        newpw->pw_gecos = p;
+        p = stpcpy(p, strempty(pw->pw_gecos)) + 1;
+
+        newpw->pw_dir = p;
+        p = stpcpy(p, strempty(pw->pw_dir)) + 1;
+
+        newpw->pw_shell = p;
+        p = stpcpy(p, strempty(pw->pw_shell)) + 1;
+
+        *ret = newpw;
+        return 0;
+}
+
+/* Iterate the given list of passwd-format files looking for an entry matching the predicate (by
+ * name if 'name' is non-NULL and by 'uid' if valid). Returns -ESRCH if no entry is found. */
+int lookup_pwent_in_files(
+                char * const *files,
+                const char *name,
+                uid_t uid,
+                struct passwd **ret) {
+
+        int r;
+
+        assert(files);
+        assert(name || uid_is_valid(uid));
+
+        STRV_FOREACH(fname, files) {
+                _cleanup_fclose_ FILE *f = NULL;
+                struct passwd *pw;
+
+                r = fopen_unlocked(*fname, "re", &f);
+                if (r == -ENOENT)
+                        continue;
+                if (r < 0)
+                        return r;
+
+                while ((r = fgetpwent_sane(f, &pw)) > 0) {
+                        if (name && !streq_ptr(pw->pw_name, name))
+                                continue;
+                        if (uid_is_valid(uid) && pw->pw_uid != uid)
+                                continue;
+                        if (ret)
+                                return copy_struct_passwd(pw, ret);
+                        return 0;
+                }
+                if (r < 0)
+                        return r;
+        }
+
+        return -ESRCH;
+}
 static size_t getpw_buffer_size(void) {
         long bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
         return bufsize <= 0 ? 4096U : (size_t) bufsize;
@@ -1188,6 +1269,90 @@ int getpwuid_malloc(uid_t uid, struct passwd **ret) {
         }
 }
 
+static int copy_struct_group(const struct group *gr, struct group **ret) {
+        assert(gr);
+        assert(ret);
+
+        size_t need_bytes = sizeof(struct group)
+                + strlen_ptr(gr->gr_name) + 1
+                + strlen_ptr(gr->gr_passwd) + 1,
+                n_mem = 0;
+        STRV_FOREACH(s, gr->gr_mem) {
+                need_bytes += sizeof(char*) + strlen(*s) + 1;
+                n_mem++;
+        }
+        need_bytes += sizeof(char*);  /* NULL terminator for gr_mem */
+
+        char *buf = malloc(need_bytes);
+        if (!buf)
+                return -ENOMEM;
+
+        struct group *newgr = (struct group *) buf;
+
+        /* The layout in our buffer:
+         * struct group, ->gr_mem pointers terminated by NULL, ->gr_name, ->gr_passwd, ->gr_mem items */
+        /* The ->gr_mem array is first, because it needs alignment. */
+        assert_cc(sizeof(struct group) % alignof(char*) == 0);
+
+        char *p = buf + sizeof(struct group) + (n_mem + 1) * sizeof(char*);
+
+        newgr->gr_name = p;
+        p = stpcpy(p, strempty(gr->gr_name)) + 1;
+
+        newgr->gr_passwd = p;
+        p = stpcpy(p, strempty(gr->gr_passwd)) + 1;
+
+        newgr->gr_gid = gr->gr_gid;
+
+        newgr->gr_mem = (char**) (buf + sizeof(struct group));
+        for (size_t i = 0; i < n_mem; i++) {
+                newgr->gr_mem[i] = p;
+                p = stpcpy(p, gr->gr_mem[i]) + 1;
+        }
+        newgr->gr_mem[n_mem] = NULL;
+
+        *ret = newgr;
+        return 0;
+}
+
+/* See lookup_pwent_in_files() for the analogous passwd-file version. */
+int lookup_grent_in_files(
+                char * const *files,
+                const char *name,
+                gid_t gid,
+                struct group **ret) {
+
+        int r;
+
+        assert(files);
+        assert(name || gid_is_valid(gid));
+
+        STRV_FOREACH(fname, files) {
+                _cleanup_fclose_ FILE *f = NULL;
+                struct group *gr;
+
+                r = fopen_unlocked(*fname, "re", &f);
+                if (r == -ENOENT)
+                        continue;
+                if (r < 0)
+                        return r;
+
+                while ((r = fgetgrent_sane(f, &gr)) > 0) {
+                        if (name && !streq_ptr(gr->gr_name, name))
+                                continue;
+                        if (gid_is_valid(gid) && gr->gr_gid != gid)
+                                continue;
+                        if (ret)
+                                return copy_struct_group(gr, ret);
+                        return 0;
+                }
+                if (r < 0)
+                        return r;
+        }
+
+        return -ESRCH;
+}
+
 static size_t getgr_buffer_size(void) {
         long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX);
         return bufsize <= 0 ? 4096U : (size_t) bufsize;
index 641bdc06b0999b85b2c04100402ae59310ff2ca3..f45e5bddfd2253398a18f28256d00c0a988631b4 100644 (file)
@@ -173,6 +173,17 @@ static inline bool hashed_password_is_locked_or_invalid(const char *password) {
  */
 #define PASSWORD_UNPROVISIONED "!unprovisioned"
 
+int lookup_pwent_in_files(
+                char * const *files,
+                const char *name,
+                uid_t uid,
+                struct passwd **ret);
+int lookup_grent_in_files(
+                char * const *files,
+                const char *name,
+                gid_t gid,
+                struct group **ret);
+
 int getpwuid_malloc(uid_t uid, struct passwd **ret);
 int getpwnam_malloc(const char *name, struct passwd **ret);
 
index 53ad9422f3406002ab5d5c2a7f0f3285c4f6fabd..e0c8b68fc124cce3042dd9e5e6b7a627e63bf330 100644 (file)
@@ -3,12 +3,15 @@
 #include <unistd.h>
 
 #include "alloc-util.h"
+#include "fd-util.h"
+#include "fileio.h"
 #include "format-util.h"
 #include "log.h"
 #include "memory-util.h"
 #include "path-util.h"
 #include "string-util.h"
 #include "tests.h"
+#include "tmpfile-util.h"
 #include "user-util.h"
 
 static void test_uid_to_name_one(uid_t uid, const char *name) {
@@ -447,4 +450,150 @@ TEST(mangle_gecos) {
         test_mangle_gecos_one("\xe2\x28\xa1", " ( ");
 }
 
+TEST(lookup_pwent_in_files) {
+        _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-user-util-passwd-XXXXXX";
+        _cleanup_fclose_ FILE *f = NULL;
+
+        ASSERT_OK(fmkostemp_safe(fn, "w", &f));
+        ASSERT_OK(write_string_stream(
+                                f,
+                                "testuser1:x:1234:5678:Test User 1:/home/test1:/bin/sh\n"
+                                "testuser2:x:5678:9999:Test User 2:/home/test2:/bin/bash\n",
+                                /* flags= */ 0));
+        ASSERT_OK(fflush_and_check(f));
+
+        char **files = STRV_MAKE(fn);
+
+        /* Lookup by name */
+        _cleanup_free_ struct passwd *pw = NULL;
+        ASSERT_OK(lookup_pwent_in_files(files, "testuser1", UID_INVALID, &pw));
+        ASSERT_STREQ(pw->pw_name, "testuser1");
+        ASSERT_EQ(pw->pw_uid, 1234u);
+        ASSERT_EQ(pw->pw_gid, 5678u);
+        ASSERT_STREQ(pw->pw_gecos, "Test User 1");
+        ASSERT_STREQ(pw->pw_dir, "/home/test1");
+        ASSERT_STREQ(pw->pw_shell, "/bin/sh");
+        pw = mfree(pw);
+
+        ASSERT_OK(lookup_pwent_in_files(STRV_MAKE(fn, fn, fn, fn), "testuser2", UID_INVALID, &pw));
+        ASSERT_STREQ(pw->pw_name, "testuser2");
+        ASSERT_EQ(pw->pw_uid, 5678u);
+        ASSERT_STREQ(pw->pw_shell, "/bin/bash");
+        pw = mfree(pw);
+
+        /* Caller doesn't care about contents */
+        ASSERT_OK(lookup_pwent_in_files(files, "testuser1", UID_INVALID, /* ret= */ NULL));
+
+        /* Missing entry */
+        ASSERT_ERROR(lookup_pwent_in_files(files, "nosuchuser", UID_INVALID, /* ret= */ NULL), ESRCH);
+        ASSERT_ERROR(lookup_pwent_in_files(files, "nosuchuser", UID_INVALID, &pw), ESRCH);
+        ASSERT_NULL(pw);
+
+        /* Lookup by uid */
+        ASSERT_OK(lookup_pwent_in_files(files, /* name= */ NULL, 1234, &pw));
+        ASSERT_STREQ(pw->pw_name, "testuser1");
+        pw = mfree(pw);
+
+        ASSERT_OK(lookup_pwent_in_files(files, /* name= */ NULL, 5678, &pw));
+        ASSERT_STREQ(pw->pw_name, "testuser2");
+        pw = mfree(pw);
+
+        /* Missing uid */
+        ASSERT_ERROR(lookup_pwent_in_files(files, /* name= */ NULL, 424242, &pw), ESRCH);
+        ASSERT_ERROR(lookup_pwent_in_files(files, /* name= */ NULL, 424242, NULL), ESRCH);
+
+        /* Non-existent file in the list is skipped */
+        ASSERT_OK(lookup_pwent_in_files(
+                                STRV_MAKE("/nonexistent-dir/passwd", fn),
+                                "testuser1",
+                                UID_INVALID,
+                                &pw));
+        ASSERT_STREQ(pw->pw_name, "testuser1");
+        pw = mfree(pw);
+
+        /* All files missing → -ESRCH */
+        ASSERT_ERROR(lookup_pwent_in_files(
+                                     STRV_MAKE("/nonexistent-dir/passwd"),
+                                     "testuser1",
+                                     UID_INVALID,
+                                     &pw), ESRCH);
+
+        /* First match wins: same name in two files, but the first file's entry should be picked */
+        _cleanup_(unlink_tempfilep) char fn2[] = "/tmp/test-user-util-passwd2-XXXXXX";
+        _cleanup_fclose_ FILE *f2 = NULL;
+
+        ASSERT_OK(fmkostemp_safe(fn2, "w", &f2));
+        ASSERT_OK(write_string_stream(
+                                f2,
+                                "testuser1:x:111:222:First:/h/first:/bin/zsh\n",
+                                /* flags= */ 0));
+        ASSERT_OK(fflush_and_check(f2));
+
+        ASSERT_OK(lookup_pwent_in_files(
+                                  STRV_MAKE(fn2, fn),
+                                  "testuser1",
+                                  UID_INVALID,
+                                  &pw));
+        ASSERT_EQ(pw->pw_uid, 111u);
+        ASSERT_STREQ(pw->pw_gecos, "First");
+}
+
+TEST(lookup_grent_in_files) {
+        _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-user-util-group-XXXXXX";
+        _cleanup_fclose_ FILE *f = NULL;
+
+        ASSERT_OK(fmkostemp_safe(fn, "w", &f));
+        ASSERT_OK(write_string_stream(
+                                f,
+                                "testgroup1:x:100:testuser1,testuser2\n"
+                                "testgroup2:x:200:\n",
+                                /* flags= */ 0));
+        ASSERT_OK(fflush_and_check(f));
+
+        char **files = STRV_MAKE(fn);
+
+        /* Lookup by name */
+        _cleanup_free_ struct group *gr = NULL;
+        ASSERT_OK(lookup_grent_in_files(files, "testgroup1", GID_INVALID, &gr));
+        ASSERT_STREQ(gr->gr_name, "testgroup1");
+        ASSERT_EQ(gr->gr_gid, 100u);
+        ASSERT_STREQ(gr->gr_mem[0], "testuser1");
+        ASSERT_STREQ(gr->gr_mem[1], "testuser2");
+        ASSERT_NULL(gr->gr_mem[2]);
+        gr = mfree(gr);
+
+        ASSERT_OK(lookup_grent_in_files(files, "testgroup2", GID_INVALID, &gr));
+        ASSERT_STREQ(gr->gr_name, "testgroup2");
+        ASSERT_EQ(gr->gr_gid, 200u);
+        ASSERT_NULL(gr->gr_mem[0]);
+        gr = mfree(gr);
+
+        /* Caller doesn't care about contents */
+        ASSERT_OK(lookup_grent_in_files(files, "testgroup1", GID_INVALID, /* ret= */ NULL));
+
+        /* Missing entry */
+        ASSERT_ERROR(lookup_grent_in_files(files, "nosuchgroup", GID_INVALID, &gr), ESRCH);
+        ASSERT_NULL(gr);
+
+        /* Lookup by gid */
+        ASSERT_OK(lookup_grent_in_files(files, /* name= */ NULL, 100, &gr));
+        ASSERT_STREQ(gr->gr_name, "testgroup1");
+        gr = mfree(gr);
+
+        ASSERT_OK(lookup_grent_in_files(files, /* name= */ NULL, 200, &gr));
+        ASSERT_STREQ(gr->gr_name, "testgroup2");
+        gr = mfree(gr);
+
+        /* Missing gid */
+        ASSERT_ERROR(lookup_grent_in_files(files, /* name= */ NULL, 424242, &gr), ESRCH);
+
+        /* Non-existent file is skipped */
+        ASSERT_OK(lookup_grent_in_files(
+                                STRV_MAKE("/nonexistent-dir/group", fn),
+                                "testgroup1",
+                                GID_INVALID,
+                                &gr));
+        ASSERT_STREQ(gr->gr_name, "testgroup1");
+}
+
 DEFINE_TEST_MAIN(LOG_INFO);