diff --git a/src/basic/user-util.c b/src/basic/user-util.c index a665a2d780..8d9474fe21 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -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; diff --git a/src/basic/user-util.h b/src/basic/user-util.h index 641bdc06b0..f45e5bddfd 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -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); diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c index 53ad9422f3..e0c8b68fc1 100644 --- a/src/test/test-user-util.c +++ b/src/test/test-user-util.c @@ -3,12 +3,15 @@ #include #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);