[libc-commits] [libc] [libc] Add getgrouplist entrypoint (PR #226958)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Mon Sep 28 04:06:21 PDT 2026


https://github.com/kaladron created https://github.com/llvm/llvm-project/pull/226958

Add the getgrouplist entrypoint from <grp.h> (BSD extension), which scans the group database to obtain the list of groups to which a user belongs.

The base group passed by the caller is included unconditionally, and supplementary groups for the user are gathered with duplicate group IDs suppressed. A small-buffer-optimised container avoids heap allocations for users belonging to up to 32 groups, falling back to dynamic allocation when more groups are present. Note that this fallback does not use AllocChecker because it relies on realloc to grow the buffer. The lookup uses a scoped database stream to avoid disturbing concurrent iteration.

Assisted-by: Automated tooling, human reviewed.

>From 1fce2cfd38f488081ea54634fd754ab0e2f7d97c Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 17 Sep 2026 16:04:10 +0100
Subject: [PATCH] [libc] Add getgrouplist entrypoint

Add the getgrouplist entrypoint from <grp.h> (BSD extension), which
scans the group database to obtain the list of groups to which a user
belongs.

The base group passed by the caller is included unconditionally, and
supplementary groups for the user are gathered with duplicate group IDs
suppressed. A small-buffer-optimised container avoids heap allocations
for users belonging to up to 32 groups, falling back to dynamic
allocation when more groups are present. Note that this fallback does
not use AllocChecker because it relies on realloc to grow the buffer.
The lookup uses a scoped database stream to avoid disturbing concurrent
iteration.

Assisted-by: Automated tooling, human reviewed.
---
 libc/config/linux/aarch64/entrypoints.txt |   1 +
 libc/config/linux/arm/entrypoints.txt     |   1 +
 libc/config/linux/i386/entrypoints.txt    |   1 +
 libc/config/linux/riscv/entrypoints.txt   |   1 +
 libc/config/linux/x86_64/entrypoints.txt  |   1 +
 libc/include/grp.yaml                     |  10 +
 libc/src/grp/CMakeLists.txt               |  23 +++
 libc/src/grp/getgrouplist.cpp             |  55 ++++++
 libc/src/grp/getgrouplist.h               |  26 +++
 libc/src/grp/grp_utils.cpp                | 119 ++++++++++++
 libc/src/grp/grp_utils.h                  |   6 +
 libc/test/src/grp/CMakeLists.txt          |  29 +++
 libc/test/src/grp/getgrouplist_test.cpp   | 216 ++++++++++++++++++++++
 13 files changed, 489 insertions(+)
 create mode 100644 libc/src/grp/getgrouplist.cpp
 create mode 100644 libc/src/grp/getgrouplist.h
 create mode 100644 libc/test/src/grp/getgrouplist_test.cpp

diff --git a/libc/config/linux/aarch64/entrypoints.txt b/libc/config/linux/aarch64/entrypoints.txt
index 15a5367f5fc95d..a9c0a2da999a2f 100644
--- a/libc/config/linux/aarch64/entrypoints.txt
+++ b/libc/config/linux/aarch64/entrypoints.txt
@@ -51,6 +51,7 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.grp.getgrent
     libc.src.grp.getgrgid
     libc.src.grp.getgrgid_r
+    libc.src.grp.getgrouplist
     libc.src.grp.getgrnam
     libc.src.grp.getgrnam_r
     libc.src.grp.setgrent
diff --git a/libc/config/linux/arm/entrypoints.txt b/libc/config/linux/arm/entrypoints.txt
index 1441146393c5fd..357e512873ac5e 100644
--- a/libc/config/linux/arm/entrypoints.txt
+++ b/libc/config/linux/arm/entrypoints.txt
@@ -35,6 +35,7 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.grp.getgrent
     libc.src.grp.getgrgid
     libc.src.grp.getgrgid_r
+    libc.src.grp.getgrouplist
     libc.src.grp.getgrnam
     libc.src.grp.getgrnam_r
     libc.src.grp.setgrent
diff --git a/libc/config/linux/i386/entrypoints.txt b/libc/config/linux/i386/entrypoints.txt
index b58956c1666869..2633280127355d 100644
--- a/libc/config/linux/i386/entrypoints.txt
+++ b/libc/config/linux/i386/entrypoints.txt
@@ -7,6 +7,7 @@ set(TARGET_LIBC_ENTRYPOINTS
   libc.src.grp.getgrent
   libc.src.grp.getgrgid
   libc.src.grp.getgrgid_r
+  libc.src.grp.getgrouplist
   libc.src.grp.getgrnam
   libc.src.grp.getgrnam_r
   libc.src.grp.setgrent
diff --git a/libc/config/linux/riscv/entrypoints.txt b/libc/config/linux/riscv/entrypoints.txt
index 8f4dded27de68c..5d8199a2ba69d7 100644
--- a/libc/config/linux/riscv/entrypoints.txt
+++ b/libc/config/linux/riscv/entrypoints.txt
@@ -62,6 +62,7 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.grp.getgrent
     libc.src.grp.getgrgid
     libc.src.grp.getgrgid_r
+    libc.src.grp.getgrouplist
     libc.src.grp.getgrnam
     libc.src.grp.getgrnam_r
     libc.src.grp.setgrent
diff --git a/libc/config/linux/x86_64/entrypoints.txt b/libc/config/linux/x86_64/entrypoints.txt
index 8ca73cc82cc2db..5b64a4c91dfbd2 100644
--- a/libc/config/linux/x86_64/entrypoints.txt
+++ b/libc/config/linux/x86_64/entrypoints.txt
@@ -62,6 +62,7 @@ set(TARGET_LIBC_ENTRYPOINTS
     libc.src.grp.getgrent
     libc.src.grp.getgrgid
     libc.src.grp.getgrgid_r
+    libc.src.grp.getgrouplist
     libc.src.grp.getgrnam
     libc.src.grp.getgrnam_r
     libc.src.grp.setgrent
diff --git a/libc/include/grp.yaml b/libc/include/grp.yaml
index 6399cd27d11011..45369f4e9a5811 100644
--- a/libc/include/grp.yaml
+++ b/libc/include/grp.yaml
@@ -1,6 +1,7 @@
 header: grp.h
 standards:
   - posix
+  - bsd
 types:
   - type_name: struct_group
   - type_name: gid_t
@@ -50,6 +51,15 @@ functions:
       - type: char *
       - type: size_t
       - type: struct group **
+  - name: getgrouplist
+    standards:
+      - bsd
+    return_type: int
+    arguments:
+      - type: const char *
+      - type: gid_t
+      - type: gid_t *
+      - type: int *
   - name: setgrent
     standards:
       - posix
diff --git a/libc/src/grp/CMakeLists.txt b/libc/src/grp/CMakeLists.txt
index ee613e6f3217f4..8eaa0e2f603a30 100644
--- a/libc/src/grp/CMakeLists.txt
+++ b/libc/src/grp/CMakeLists.txt
@@ -65,6 +65,23 @@ add_entrypoint_object(
     .grp_utils
 )
 
+add_entrypoint_object(
+  getgrouplist
+  SRCS
+    getgrouplist.cpp
+  HDRS
+    getgrouplist.h
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.types.gid_t
+    libc.src.__support.common
+    libc.src.__support.libc_errno
+    libc.src.__support.macros.config
+    libc.src.__support.macros.null_check
+    libc.src.errno.errno
+    .grp_utils
+)
+
 add_entrypoint_object(
   getgrnam
   SRCS
@@ -121,16 +138,22 @@ add_object_library(
     grp_utils.cpp
   DEPENDS
     libc.hdr.errno_macros
+    libc.hdr.func.free
+    libc.hdr.func.malloc
+    libc.hdr.func.realloc
     libc.hdr.stdint_proxy
     libc.hdr.types.gid_t
     libc.hdr.types.size_t
     libc.hdr.types.struct_group
+    libc.src.__support.CPP.array
+    libc.src.__support.CPP.limits
     libc.src.__support.CPP.span
     libc.src.__support.CPP.string_view
     libc.src.__support.File.file
     libc.src.__support.File.platform_file
     libc.src.__support.ctype_utils
     libc.src.__support.error_or
+    libc.src.__support.libc_assert
     libc.src.__support.macros.attributes
     libc.src.__support.macros.config
     libc.src.__support.pwd.dynamic_buffer
diff --git a/libc/src/grp/getgrouplist.cpp b/libc/src/grp/getgrouplist.cpp
new file mode 100644
index 00000000000000..51bf3141ba5de8
--- /dev/null
+++ b/libc/src/grp/getgrouplist.cpp
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Implementation of getgrouplist.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/grp/getgrouplist.h"
+#include "hdr/errno_macros.h"
+#include "hdr/types/gid_t.h"
+#include "src/__support/common.h"
+#include "src/__support/libc_errno.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/macros/null_check.h"
+#include "src/grp/grp_utils.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+LLVM_LIBC_FUNCTION(int, getgrouplist,
+                   (const char *user, gid_t group, gid_t *groups,
+                    int *ngroups)) {
+  LIBC_CRASH_ON_NULLPTR(user);
+  LIBC_CRASH_ON_NULLPTR(ngroups);
+  if (ngroups && *ngroups > 0)
+    LIBC_CRASH_ON_NULLPTR(groups);
+
+  if (!user || !ngroups || *ngroups < 0 || (*ngroups > 0 && !groups)) {
+    libc_errno = EINVAL;
+    return -1;
+  }
+
+  const auto res =
+      grp::get_group_list(user, group, groups, static_cast<size_t>(*ngroups));
+  if (!res.has_value()) {
+    libc_errno = res.error();
+    return -1;
+  }
+
+  const size_t total = res.value();
+  const int requested = *ngroups;
+  *ngroups = static_cast<int>(total);
+
+  if (requested < static_cast<int>(total))
+    return -1;
+
+  return static_cast<int>(total);
+}
+
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/grp/getgrouplist.h b/libc/src/grp/getgrouplist.h
new file mode 100644
index 00000000000000..a8ae38e010ef63
--- /dev/null
+++ b/libc/src/grp/getgrouplist.h
@@ -0,0 +1,26 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Header file for getgrouplist function.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_GRP_GETGROUPLIST_H
+#define LLVM_LIBC_SRC_GRP_GETGROUPLIST_H
+
+#include "hdr/types/gid_t.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups);
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_GRP_GETGROUPLIST_H
diff --git a/libc/src/grp/grp_utils.cpp b/libc/src/grp/grp_utils.cpp
index 14008da2c66b50..61aeb5482fa6c6 100644
--- a/libc/src/grp/grp_utils.cpp
+++ b/libc/src/grp/grp_utils.cpp
@@ -13,14 +13,20 @@
 
 #include "src/grp/grp_utils.h"
 #include "hdr/errno_macros.h"
+#include "hdr/func/free.h"
+#include "hdr/func/malloc.h"
+#include "hdr/func/realloc.h"
 #include "hdr/stdint_proxy.h"
 #include "hdr/types/gid_t.h"
 #include "hdr/types/size_t.h"
 #include "hdr/types/struct_group.h"
+#include "src/__support/CPP/array.h"
+#include "src/__support/CPP/limits.h"
 #include "src/__support/CPP/span.h"
 #include "src/__support/CPP/string_view.h"
 #include "src/__support/ctype_utils.h"
 #include "src/__support/error_or.h"
+#include "src/__support/libc_assert.h"
 #include "src/__support/macros/attributes.h"
 #include "src/__support/macros/config.h"
 #include "src/__support/pwd/dynamic_buffer.h"
@@ -200,6 +206,70 @@ ErrorOr<bool> lookup_by_gid(gid_t gid, struct group *grp, BufferType &buffer,
   return local_db.lookup(matcher, grp, buffer);
 }
 
+// Small-buffer-optimised container for collecting group IDs without duplicates.
+// Initial storage is stack-local; falls back to dynamic allocation if the
+// user belongs to more than 32 groups.
+class GidList {
+  static constexpr size_t STATIC_CAP = 32;
+  cpp::array<gid_t, STATIC_CAP> static_buf = {};
+  gid_t *buf = static_buf.data();
+  size_t count = 0;
+  size_t cap = STATIC_CAP;
+
+public:
+  LIBC_INLINE GidList() = default;
+  LIBC_INLINE ~GidList() {
+    if (buf != static_buf.data())
+      ::free(buf);
+  }
+
+  GidList(const GidList &) = delete;
+  GidList &operator=(const GidList &) = delete;
+
+  [[nodiscard]] LIBC_INLINE cpp::span<const gid_t> span() const {
+    return {buf, count};
+  }
+
+  [[nodiscard]] LIBC_INLINE bool contains(gid_t gid) const {
+    for (gid_t g : span()) {
+      if (g == gid)
+        return true;
+    }
+    return false;
+  }
+
+  [[nodiscard]] LIBC_INLINE bool push_back(gid_t gid) {
+    if (count == cap) {
+      if (cap > cpp::numeric_limits<size_t>::max() / (2 * sizeof(gid_t)))
+        return false;
+      size_t new_cap = cap * 2;
+      void *new_buf = nullptr;
+      if (buf == static_buf.data()) {
+        new_buf = ::malloc(new_cap * sizeof(gid_t));
+        if (!new_buf)
+          return false;
+        for (size_t i = 0; i < count; ++i)
+          static_cast<gid_t *>(new_buf)[i] = static_buf[i];
+      } else {
+        new_buf = ::realloc(buf, new_cap * sizeof(gid_t));
+        if (!new_buf)
+          return false;
+      }
+      buf = static_cast<gid_t *>(new_buf);
+      cap = new_cap;
+    }
+    buf[count++] = gid;
+    return true;
+  }
+
+  [[nodiscard]] LIBC_INLINE size_t size() const { return count; }
+
+  [[nodiscard]] LIBC_INLINE gid_t operator[](size_t i) const {
+    LIBC_ASSERT(i < count);
+    return buf[i];
+  }
+};
+
 } // namespace
 
 void TESTONLY_set_group_path(const char *path) {
@@ -258,5 +328,54 @@ ErrorOr<struct group *> find_by_gid(gid_t gid) {
   return &grp_entry;
 }
 
+ErrorOr<size_t> get_group_list(cpp::string_view user, gid_t group,
+                               gid_t *groups, size_t ngroups,
+                               const char *path) {
+  GidList gid_list;
+  if (!gid_list.push_back(group))
+    return Error(ENOMEM);
+
+  pwd::ScopedFlatFileDatabase<struct group> local_db(path ? path
+                                                          : group_file_path);
+  pwd::ScopedDynamicBuffer buffer;
+  struct group entry = {};
+
+  const auto open_res = local_db.setdb();
+  if (open_res.has_value()) {
+    while (true) {
+      const auto next_res = local_db.getnext(&entry, buffer);
+      if (!next_res.has_value()) {
+        if (next_res.error() == ENOMEM)
+          return Error(ENOMEM);
+        break;
+      }
+      if (!next_res.value())
+        break;
+
+      bool is_member = false;
+      if (entry.gr_mem) {
+        for (char **m = entry.gr_mem; *m != nullptr; ++m) {
+          if (cpp::string_view(*m) == user) {
+            is_member = true;
+            break;
+          }
+        }
+      }
+
+      if (is_member && !gid_list.contains(entry.gr_gid)) {
+        if (!gid_list.push_back(entry.gr_gid))
+          return Error(ENOMEM);
+      }
+    }
+  }
+
+  const size_t copy_count =
+      ngroups < gid_list.size() ? ngroups : gid_list.size();
+  for (size_t i = 0; i < copy_count; ++i)
+    groups[i] = gid_list[i];
+
+  return gid_list.size();
+}
+
 } // namespace grp
 } // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/grp/grp_utils.h b/libc/src/grp/grp_utils.h
index 1ff2f8bd618c87..70f85791ef79ee 100644
--- a/libc/src/grp/grp_utils.h
+++ b/libc/src/grp/grp_utils.h
@@ -55,6 +55,12 @@ ErrorOr<bool> find_by_gid(gid_t gid, struct group *grp, cpp::span<char> buffer,
 ErrorOr<struct group *> find_by_name(cpp::string_view name);
 ErrorOr<struct group *> find_by_gid(gid_t gid);
 
+// Fills up to ngroups into the groups array and returns the total number of
+// groups found for user, or an Error if reading the database failed.
+ErrorOr<size_t> get_group_list(cpp::string_view user, gid_t group,
+                               gid_t *groups, size_t ngroups,
+                               const char *path = nullptr);
+
 } // namespace grp
 } // namespace LIBC_NAMESPACE_DECL
 
diff --git a/libc/test/src/grp/CMakeLists.txt b/libc/test/src/grp/CMakeLists.txt
index ad0bfb7df6669f..32c532c917a6d5 100644
--- a/libc/test/src/grp/CMakeLists.txt
+++ b/libc/test/src/grp/CMakeLists.txt
@@ -150,3 +150,32 @@ add_libc_test(
     libc.test.UnitTest.ErrnoCheckingTest
     libc.test.UnitTest.ErrnoSetterMatcher
 )
+
+add_libc_test(
+  getgrouplist_test
+  SUITE
+    libc_grp_unittests
+  HDRS
+    grp_test_utils.h
+  SRCS
+    getgrouplist_test.cpp
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.signal_macros
+    libc.hdr.types.gid_t
+    libc.hdr.types.size_t
+    libc.hdr.types.struct_group
+    libc.src.__support.CPP.array
+    libc.src.__support.CPP.stringstream
+    libc.src.__support.File.file
+    libc.src.__support.File.platform_file
+    libc.src.grp.endgrent
+    libc.src.grp.getgrent
+    libc.src.grp.getgrouplist
+    libc.src.grp.grp_utils
+    libc.src.grp.setgrent
+    libc.src.stdio.remove
+    libc.src.string.string_utils
+    libc.test.UnitTest.ErrnoCheckingTest
+    libc.test.UnitTest.ErrnoSetterMatcher
+)
diff --git a/libc/test/src/grp/getgrouplist_test.cpp b/libc/test/src/grp/getgrouplist_test.cpp
new file mode 100644
index 00000000000000..d49aedb1adc10a
--- /dev/null
+++ b/libc/test/src/grp/getgrouplist_test.cpp
@@ -0,0 +1,216 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unit tests for getgrouplist.
+///
+//===----------------------------------------------------------------------===//
+
+#include "hdr/errno_macros.h"
+#include "hdr/signal_macros.h"
+#include "hdr/types/gid_t.h"
+#include "hdr/types/size_t.h"
+#include "hdr/types/struct_group.h"
+#include "src/__support/CPP/array.h"
+#include "src/__support/CPP/stringstream.h"
+#include "src/grp/endgrent.h"
+#include "src/grp/getgrent.h"
+#include "src/grp/getgrouplist.h"
+#include "src/grp/grp_utils.h"
+#include "src/grp/setgrent.h"
+#include "test/UnitTest/ErrnoSetterMatcher.h"
+#include "test/UnitTest/Test.h"
+#include "test/src/grp/grp_test_utils.h"
+
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Fails;
+using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds;
+
+TEST_F(LlvmLibcGrpTest, GetgrouplistSuccess) {
+  const char *content = "root:x:0:root\n"
+                        "bin:x:1:bin,daemon\n"
+                        "users:x:100:user1,user2\n"
+                        "developers:x:200:user2,user3\n"
+                        "admins:x:300:user1,user3\n";
+  ScopedGroupFile test_file(
+      libc_make_test_file_path("getgrouplist_success.test"), content);
+
+  gid_t groups[10];
+  int ngroups = 10;
+
+  ASSERT_THAT(LIBC_NAMESPACE::getgrouplist("user1", 1000, groups, &ngroups),
+              Succeeds(3));
+  EXPECT_EQ(ngroups, 3);
+  EXPECT_EQ(groups[0], static_cast<gid_t>(1000));
+  EXPECT_EQ(groups[1], static_cast<gid_t>(100));
+  EXPECT_EQ(groups[2], static_cast<gid_t>(300));
+}
+
+TEST_F(LlvmLibcGrpTest, BaseGroupAlreadyInDatabase) {
+  const char *content = "staff:x:50:user1\n"
+                        "devel:x:60:user1\n";
+  ScopedGroupFile test_file(
+      libc_make_test_file_path("getgrouplist_base_dup.test"), content);
+
+  gid_t groups[10];
+  int ngroups = 10;
+
+  ASSERT_THAT(LIBC_NAMESPACE::getgrouplist("user1", 50, groups, &ngroups),
+              Succeeds(2));
+  EXPECT_EQ(ngroups, 2);
+  EXPECT_EQ(groups[0], static_cast<gid_t>(50));
+  EXPECT_EQ(groups[1], static_cast<gid_t>(60));
+}
+
+TEST_F(LlvmLibcGrpTest, DuplicateGidInDatabase) {
+  const char *content = "team1:x:100:user1\n"
+                        "team2:x:100:user1\n"
+                        "team3:x:200:user1\n";
+  ScopedGroupFile test_file(
+      libc_make_test_file_path("getgrouplist_dup_gid.test"), content);
+
+  gid_t groups[10];
+  int ngroups = 10;
+
+  ASSERT_THAT(LIBC_NAMESPACE::getgrouplist("user1", 500, groups, &ngroups),
+              Succeeds(3));
+  EXPECT_EQ(ngroups, 3);
+  EXPECT_EQ(groups[0], static_cast<gid_t>(500));
+  EXPECT_EQ(groups[1], static_cast<gid_t>(100));
+  EXPECT_EQ(groups[2], static_cast<gid_t>(200));
+}
+
+TEST_F(LlvmLibcGrpTest, BufferTooSmall) {
+  const char *content = "users:x:100:user1\n"
+                        "admins:x:200:user1\n";
+  ScopedGroupFile test_file(libc_make_test_file_path("getgrouplist_small.test"),
+                            content);
+
+  gid_t groups[1] = {0};
+  int ngroups = 1;
+
+  EXPECT_EQ(LIBC_NAMESPACE::getgrouplist("user1", 10, groups, &ngroups), -1);
+  EXPECT_EQ(ngroups, 3);
+  EXPECT_EQ(groups[0], static_cast<gid_t>(10));
+  ASSERT_ERRNO_SUCCESS();
+}
+
+TEST_F(LlvmLibcGrpTest, ZeroBufferQuery) {
+  const char *content = "users:x:100:user1\n"
+                        "admins:x:200:user1\n";
+  ScopedGroupFile test_file(libc_make_test_file_path("getgrouplist_zero.test"),
+                            content);
+
+  int ngroups = 0;
+
+  EXPECT_EQ(LIBC_NAMESPACE::getgrouplist("user1", 10, nullptr, &ngroups), -1);
+  EXPECT_EQ(ngroups, 3);
+  ASSERT_ERRNO_SUCCESS();
+}
+
+TEST_F(LlvmLibcGrpTest, UserNotFound) {
+  const char *content = "wheel:x:10:root\n";
+  ScopedGroupFile test_file(
+      libc_make_test_file_path("getgrouplist_not_found.test"), content);
+
+  gid_t groups[5];
+  int ngroups = 5;
+
+  ASSERT_THAT(
+      LIBC_NAMESPACE::getgrouplist("nonexistent", 1000, groups, &ngroups),
+      Succeeds(1));
+  EXPECT_EQ(ngroups, 1);
+  EXPECT_EQ(groups[0], static_cast<gid_t>(1000));
+}
+
+TEST_F(LlvmLibcGrpTest, NonexistentFile) {
+  LIBC_NAMESPACE::grp::TESTONLY_set_group_path(
+      libc_make_test_file_path("getgrouplist_missing.test"));
+
+  gid_t groups[5];
+  int ngroups = 5;
+
+  ASSERT_THAT(LIBC_NAMESPACE::getgrouplist("anyuser", 1000, groups, &ngroups),
+              Succeeds(1));
+  EXPECT_EQ(ngroups, 1);
+  EXPECT_EQ(groups[0], static_cast<gid_t>(1000));
+}
+
+TEST_F(LlvmLibcGrpTest, DoesNotDisturbIteration) {
+  const char *content = "group1:x:1:user1\n"
+                        "group2:x:2:user2\n"
+                        "group3:x:3:user1\n";
+  ScopedGroupFile test_file(libc_make_test_file_path("getgrouplist_iter.test"),
+                            content);
+
+  LIBC_NAMESPACE::setgrent();
+  const auto first = LIBC_NAMESPACE::grp::read_next();
+  ASSERT_TRUE(first.has_value());
+  ASSERT_NE(first.value(), nullptr);
+  EXPECT_STREQ(first.value()->gr_name, "group1");
+
+  gid_t groups[5];
+  int ngroups = 5;
+  ASSERT_THAT(LIBC_NAMESPACE::getgrouplist("user2", 50, groups, &ngroups),
+              Succeeds(2));
+  EXPECT_EQ(ngroups, 2);
+
+  const auto second = LIBC_NAMESPACE::grp::read_next();
+  ASSERT_TRUE(second.has_value());
+  ASSERT_NE(second.value(), nullptr);
+  EXPECT_STREQ(second.value()->gr_name, "group2");
+
+  LIBC_NAMESPACE::endgrent();
+}
+
+TEST_F(LlvmLibcGrpTest, UserWithMoreThan32Groups) {
+  // Construct a group file containing 35 groups with testuser.
+  // This exercises dynamic reallocation in GidList.
+  LIBC_NAMESPACE::cpp::array<char, 2048> content;
+  LIBC_NAMESPACE::cpp::StringStream stream(content);
+  for (int i = 1; i <= 35; ++i)
+    stream << "grp" << i << ":x:" << (1000 + i) << ":testuser\n";
+  stream << LIBC_NAMESPACE::cpp::StringStream::ENDS;
+  ASSERT_FALSE(stream.overflow());
+
+  ScopedGroupFile test_file(libc_make_test_file_path("getgrouplist_many.test"),
+                            content.data());
+
+  gid_t groups[64];
+  int ngroups = 64;
+
+  ASSERT_THAT(LIBC_NAMESPACE::getgrouplist("testuser", 5000, groups, &ngroups),
+              Succeeds(36));
+  EXPECT_EQ(ngroups, 36);
+  EXPECT_EQ(groups[0], static_cast<gid_t>(5000));
+  for (int i = 1; i <= 35; ++i)
+    EXPECT_EQ(groups[i], static_cast<gid_t>(1000 + i));
+}
+
+TEST_F(LlvmLibcGrpTest, NegativeNgroupsReturnsEinval) {
+  int ngroups = -1;
+  gid_t groups[5];
+  EXPECT_THAT(LIBC_NAMESPACE::getgrouplist("user", 100, groups, &ngroups),
+              Fails(EINVAL));
+}
+
+#if defined(LIBC_ADD_NULL_CHECKS)
+TEST_F(LlvmLibcGrpTest, NullPointerCrash) {
+  int ngroups = 5;
+  gid_t groups[5];
+
+  ASSERT_DEATH(
+      [&] { LIBC_NAMESPACE::getgrouplist(nullptr, 100, groups, &ngroups); },
+      WITH_SIGNAL(-1));
+  ASSERT_DEATH(
+      [&] { LIBC_NAMESPACE::getgrouplist("user", 100, groups, nullptr); },
+      WITH_SIGNAL(-1));
+  ASSERT_DEATH(
+      [&] { LIBC_NAMESPACE::getgrouplist("user", 100, nullptr, &ngroups); },
+      WITH_SIGNAL(-1));
+}
+#endif // LIBC_ADD_NULL_CHECKS



More information about the libc-commits mailing list