feat(passwd): getpw*/getgr*/setpwent/endpwent family

This commit is contained in:
2026-09-05 23:25:24 -04:00
parent 9b576757cf
commit 523045b5ae
6 changed files with 1549 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
/*
* vlibc — pwd.h/grp.h test (todo 37).
*
* Exercises the password and group databases end to end without assuming
* anything about their CONTENTS: every lookup is asserted either to
* return a sane record (string fields non-NULL, ids echo the key) or
* NULL, and every stream walk is asserted only to terminate cleanly. The
* one environment precondition is that /etc/passwd and /etc/group EXIST
* and are readable; when either is absent the whole test skips (rc 0),
* detected through a raw SYS_faccessat so no vlibc errno slot is touched
* on the skip path.
*
* Scenarios (all content-independent):
*
* 1. current-uid: getpwuid(getuid()) is NULL or a record whose pw_uid
* echoes the key and whose pw_name is set; when a record comes back,
* getpwnam(pw_name) finds it again (name round-trip).
* 2. current-gid: the same pair against the group database.
* 3. stream walk: setpwent/getpwent/endpwent; two consecutive full
* walks return the same count (the stream really rewinds), every
* record is sane, and the walk terminates.
* 4. group members: every parsed record has a non-NULL gr_mem array
* (the empty list is a single NULL entry), and member arrays
* terminate.
* 5. _r forms: getpwuid_r/getgrgid_r into a 4096-byte buffer return 0
* and either *result == NULL (key absent) or a filled struct echoing
* the key; getpwent_r/getgrent_r likewise fill one record.
* 6. NULL keys: getpwnam(NULL)/getgrnam(NULL) return NULL (defensive
* contract, no crash).
* 7. -f mode: impossible keys (uid/gid 2147483647, a name no real
* database carries) come back NULL without crashing, then the test
* leaves via a raw SYS_exit_group (house pattern: after a vlibc
* errno-capable call the host TCB may be dirty).
* 8. fgetgrent (level 2 only): parse one record from an explicitly
* fopen'd stream.
*
* Host headers are never included; <pwd.h>/<grp.h>/<unistd.h> are
* vlibc's own (-Iinclude shadows the system copies). getuid/getgid come
* from src/process/uid.c.
*/
#include <grp.h>
#include <pwd.h>
#include <unistd.h>
#include <vlibc/internal/test.h>
/* Local kernel-ABI constants for the skip probe (no public header pulled). */
#define PG_AT_FDCWD (-100)
#define PG_R_OK 4
/* Both databases readable (the only environment assumption). */
static int
pg_db_present(void)
{
return __syscall4(SYS_faccessat, PG_AT_FDCWD, (long)"/etc/passwd", PG_R_OK, 0) == 0 &&
__syscall4(SYS_faccessat, PG_AT_FDCWD, (long)"/etc/group", PG_R_OK, 0) == 0;
}
static int
test_current_uid(void)
{
uid_t uid = getuid();
struct passwd *p = getpwuid(uid);
if (p == NULL)
{
return 0; /* key absent from the database: acceptable */
}
TEST_ASSERT_TRUE(p->pw_name != NULL);
TEST_ASSERT_EQ((unsigned long)p->pw_uid, (unsigned long)uid);
if (p->pw_name != NULL)
{
struct passwd *q = getpwnam(p->pw_name);
TEST_ASSERT_TRUE(q != NULL);
if (q != NULL)
{
TEST_ASSERT_EQ((unsigned long)q->pw_uid, (unsigned long)uid);
}
}
return 0;
}
static int
test_current_gid(void)
{
gid_t gid = getgid();
struct group *g = getgrgid(gid);
if (g == NULL)
{
return 0; /* key absent from the database: acceptable */
}
TEST_ASSERT_TRUE(g->gr_name != NULL);
TEST_ASSERT_EQ((unsigned long)g->gr_gid, (unsigned long)gid);
if (g->gr_name != NULL)
{
struct group *h = getgrnam(g->gr_name);
TEST_ASSERT_TRUE(h != NULL);
if (h != NULL)
{
TEST_ASSERT_EQ((unsigned long)h->gr_gid, (unsigned long)gid);
}
}
return 0;
}
/* Count the records of one full pwd walk, sanity-checking each. */
static unsigned long
pg_pwd_walk_count(void)
{
struct passwd *p;
unsigned long n = 0;
setpwent();
while ((p = getpwent()) != NULL)
{
TEST_ASSERT_TRUE(p->pw_name != NULL);
if (++n > 100000UL)
{
break; /* runaway guard: the walk must terminate */
}
}
endpwent();
return n;
}
static int
test_pwd_stream(void)
{
unsigned long first = pg_pwd_walk_count();
unsigned long second = pg_pwd_walk_count();
/* The walk terminates and setpwent really rewinds (0 == 0 for an
* empty database is still a valid equality). */
TEST_ASSERT_TRUE(first < 100000UL);
TEST_ASSERT_EQ(first, second);
/* After endpwent a fresh getpwent reopens from the start: the result
* is the first record, or NULL only when the database is empty. */
if (first != 0)
{
struct passwd *p = getpwent();
TEST_ASSERT_TRUE(p != NULL);
endpwent();
}
return 0;
}
/* Every parsed group has a terminating gr_mem array; empty lists appear
* as a single NULL entry, so gr_mem itself is never NULL. */
static int
test_group_members(void)
{
struct group *g;
unsigned long n = 0;
setgrent();
while ((g = getgrent()) != NULL)
{
char **m;
unsigned long members = 0;
TEST_ASSERT_TRUE(g->gr_name != NULL);
TEST_ASSERT_TRUE(g->gr_mem != NULL);
for (m = g->gr_mem; m != NULL && *m != NULL; m++)
{
TEST_ASSERT_TRUE(**m != '\0');
if (++members > 100000UL)
{
break; /* runaway guard: member arrays must terminate */
}
}
TEST_ASSERT_TRUE(members < 100000UL);
if (++n > 100000UL)
{
break;
}
}
endgrent();
TEST_ASSERT_TRUE(n < 100000UL);
return 0;
}
static int
test_pwd_r(void)
{
struct passwd pw;
struct passwd *res = NULL;
char buf[4096];
int rc = getpwuid_r(getuid(), &pw, buf, sizeof buf, &res);
TEST_ASSERT_EQ(rc, 0);
TEST_ASSERT_TRUE(res == NULL || res == &pw);
if (res == &pw)
{
TEST_ASSERT_EQ((unsigned long)pw.pw_uid, (unsigned long)getuid());
TEST_ASSERT_TRUE(pw.pw_name != NULL);
}
return 0;
}
static int
test_grp_r(void)
{
struct group gr;
struct group *res = NULL;
char buf[4096];
int rc = getgrgid_r(getgid(), &gr, buf, sizeof buf, &res);
TEST_ASSERT_EQ(rc, 0);
TEST_ASSERT_TRUE(res == NULL || res == &gr);
if (res == &gr)
{
TEST_ASSERT_EQ((unsigned long)gr.gr_gid, (unsigned long)getgid());
TEST_ASSERT_TRUE(gr.gr_name != NULL);
TEST_ASSERT_TRUE(gr.gr_mem != NULL);
}
return 0;
}
static int
test_null_keys(void)
{
TEST_ASSERT_TRUE(getpwnam(NULL) == NULL);
TEST_ASSERT_TRUE(getgrnam(NULL) == NULL);
return 0;
}
#if VLIBC_LEVEL_GE(2)
static int
test_fgetgrent(void)
{
FILE *f = fopen("/etc/group", "r");
struct group *g;
if (f == NULL)
{
return 0; /* opened by the caller, closed by the caller */
}
g = fgetgrent(f);
if (g != NULL)
{
TEST_ASSERT_TRUE(g->gr_name != NULL);
TEST_ASSERT_TRUE(g->gr_mem != NULL);
}
(void)fclose(f);
return 0;
}
#endif /* VLIBC_LEVEL_GE(2) */
static const struct vlibc_test tests[] = {
{"current-uid", test_current_uid},
{"current-gid", test_current_gid},
{"pwd-stream", test_pwd_stream},
{"group-members", test_group_members},
{"pwd-r", test_pwd_r},
{"grp-r", test_grp_r},
{"null-keys", test_null_keys},
#if VLIBC_LEVEL_GE(2)
{"fgetgrent", test_fgetgrent},
#endif
};
/*
* Own main (not TEST_MAIN): supports the -f failure mode. Everything else
* follows the TEST_MAIN contract — same output shape, 0 on all-pass.
*/
int
main(int argc, char **argv)
{
const size_t count = sizeof tests / sizeof tests[0];
size_t passed = 0;
size_t i;
if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'f' && argv[1][2] == '\0')
{
/* Impossible keys must come back NULL without crashing. */
int before = vlibc_test_failures;
uid_t uid = (uid_t)2147483647UL;
gid_t gid = (gid_t)2147483647UL;
vlibc_test_say(1, "RUN missing-lookups: ");
TEST_ASSERT_TRUE(getpwuid(uid) == NULL);
TEST_ASSERT_TRUE(getgrgid(gid) == NULL);
TEST_ASSERT_TRUE(getpwnam("vlibc-no-such-user-xyz") == NULL);
TEST_ASSERT_TRUE(getgrnam("vlibc-no-such-group-xyz") == NULL);
if (vlibc_test_failures == before)
{
vlibc_test_say(1, "PASS\n");
vlibc_test_say(1, "SUMMARY: 1/1 passed, 0 assertion failure(s)\n");
}
else
{
vlibc_test_say(1, "FAIL\n");
}
/* Raw exit: the NULL-return paths may have written the library's
* errno slot, which collides with the host TCB (house pattern). */
__syscall1(SYS_exit_group, vlibc_test_failures == before ? 0 : 1);
/* not reached */
}
if (!pg_db_present())
{
vlibc_test_say(1, "SKIP: /etc/passwd or /etc/group absent, nothing to look up\n");
__syscall1(SYS_exit_group, 0);
/* not reached */
}
for (i = 0; i < count; i++)
{
int before = vlibc_test_failures;
vlibc_test_say(1, "RUN ");
vlibc_test_say(1, tests[i].name);
vlibc_test_say(1, ": ");
if (tests[i].run() == 0 && vlibc_test_failures == before)
{
vlibc_test_say(1, "PASS\n");
passed++;
}
else
{
vlibc_test_say(1, "FAIL\n");
}
}
vlibc_test_say(1, "SUMMARY: ");
vlibc_test_say_dec(1, (unsigned long)passed);
vlibc_test_say(1, "/");
vlibc_test_say_dec(1, (unsigned long)count);
vlibc_test_say(1, " passed, ");
vlibc_test_say_dec(1, (unsigned long)vlibc_test_failures);
vlibc_test_say(1, " assertion failure(s)\n");
return passed == count ? 0 : 1;
}