[libc-commits] [libc] [libc] Add getgrouplist entrypoint (PR #226958)
Jeff Bailey via libc-commits
libc-commits at lists.llvm.org
Mon Sep 28 12:54:30 PDT 2026
https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/226958
>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 1/3] [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 15a5367f5fc95..a9c0a2da999a2 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 1441146393c5f..357e512873ac5 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 b58956c166686..2633280127355 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 8f4dded27de68..5d8199a2ba69d 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 8ca73cc82cc2d..5b64a4c91dfbd 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 6399cd27d1101..45369f4e9a581 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 ee613e6f3217f..8eaa0e2f603a3 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 0000000000000..51bf3141ba5de
--- /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 0000000000000..a8ae38e010ef6
--- /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 14008da2c66b5..61aeb5482fa6c 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 1ff2f8bd618c8..70f85791ef79e 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 ad0bfb7df6669..32c532c917a6d 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 0000000000000..d49aedb1adc10
--- /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
>From c9a7a6cb62d25b6117c5241a7bc46df859a04f1d Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Mon, 28 Sep 2026 20:20:41 +0100
Subject: [PATCH 2/3] [libc] Remove bsd from top-level standards in grp.yaml
Assisted-by: Automated tooling, human reviewed.
---
libc/include/grp.yaml | 1 -
1 file changed, 1 deletion(-)
diff --git a/libc/include/grp.yaml b/libc/include/grp.yaml
index 45369f4e9a581..f22cd51f1ea2a 100644
--- a/libc/include/grp.yaml
+++ b/libc/include/grp.yaml
@@ -1,7 +1,6 @@
header: grp.h
standards:
- posix
- - bsd
types:
- type_name: struct_group
- type_name: gid_t
>From 2f15206c11a65da963d9a17784aa6811b28283d4 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Mon, 28 Sep 2026 20:54:10 +0100
Subject: [PATCH 3/3] [libc] Address review feedback on getgrouplist
- Change getgrouplist's standards entry in grp.yaml from bsd to gnu to
match its glibc-compatible return value semantics.
- Remove redundant null-pointer checks in getgrouplist after
LIBC_CRASH_ON_NULLPTR.
- Cast res.value() to int once in getgrouplist rather than repeating the
cast.
- Wrap the capacity multiplication overflow check in GidList::push_back
in LIBC_UNLIKELY.
Assisted-by: Automated tooling, human reviewed.
---
libc/include/grp.yaml | 2 +-
libc/src/grp/CMakeLists.txt | 1 +
libc/src/grp/getgrouplist.cpp | 12 ++++++------
libc/src/grp/grp_utils.cpp | 4 +++-
4 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/libc/include/grp.yaml b/libc/include/grp.yaml
index f22cd51f1ea2a..757c69d006533 100644
--- a/libc/include/grp.yaml
+++ b/libc/include/grp.yaml
@@ -52,7 +52,7 @@ functions:
- type: struct group **
- name: getgrouplist
standards:
- - bsd
+ - gnu
return_type: int
arguments:
- type: const char *
diff --git a/libc/src/grp/CMakeLists.txt b/libc/src/grp/CMakeLists.txt
index 8eaa0e2f603a3..85ad797b00e24 100644
--- a/libc/src/grp/CMakeLists.txt
+++ b/libc/src/grp/CMakeLists.txt
@@ -156,6 +156,7 @@ add_object_library(
libc.src.__support.libc_assert
libc.src.__support.macros.attributes
libc.src.__support.macros.config
+ libc.src.__support.macros.optimization
libc.src.__support.pwd.dynamic_buffer
libc.src.__support.pwd.field_tokenizer
libc.src.__support.pwd.flat_file_db
diff --git a/libc/src/grp/getgrouplist.cpp b/libc/src/grp/getgrouplist.cpp
index 51bf3141ba5de..29a92bcfc5758 100644
--- a/libc/src/grp/getgrouplist.cpp
+++ b/libc/src/grp/getgrouplist.cpp
@@ -27,10 +27,10 @@ LLVM_LIBC_FUNCTION(int, getgrouplist,
int *ngroups)) {
LIBC_CRASH_ON_NULLPTR(user);
LIBC_CRASH_ON_NULLPTR(ngroups);
- if (ngroups && *ngroups > 0)
+ if (*ngroups > 0)
LIBC_CRASH_ON_NULLPTR(groups);
- if (!user || !ngroups || *ngroups < 0 || (*ngroups > 0 && !groups)) {
+ if (*ngroups < 0) {
libc_errno = EINVAL;
return -1;
}
@@ -42,14 +42,14 @@ LLVM_LIBC_FUNCTION(int, getgrouplist,
return -1;
}
- const size_t total = res.value();
+ const int total = static_cast<int>(res.value());
const int requested = *ngroups;
- *ngroups = static_cast<int>(total);
+ *ngroups = total;
- if (requested < static_cast<int>(total))
+ if (requested < total)
return -1;
- return static_cast<int>(total);
+ return total;
}
} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/grp/grp_utils.cpp b/libc/src/grp/grp_utils.cpp
index 61aeb5482fa6c..771e4c0c721a4 100644
--- a/libc/src/grp/grp_utils.cpp
+++ b/libc/src/grp/grp_utils.cpp
@@ -29,6 +29,7 @@
#include "src/__support/libc_assert.h"
#include "src/__support/macros/attributes.h"
#include "src/__support/macros/config.h"
+#include "src/__support/macros/optimization.h"
#include "src/__support/pwd/dynamic_buffer.h"
#include "src/__support/pwd/field_tokenizer.h"
#include "src/__support/pwd/flat_file_db.h"
@@ -240,7 +241,8 @@ class GidList {
[[nodiscard]] LIBC_INLINE bool push_back(gid_t gid) {
if (count == cap) {
- if (cap > cpp::numeric_limits<size_t>::max() / (2 * sizeof(gid_t)))
+ if (LIBC_UNLIKELY(cap > cpp::numeric_limits<size_t>::max() /
+ (2 * sizeof(gid_t))))
return false;
size_t new_cap = cap * 2;
void *new_buf = nullptr;
More information about the libc-commits
mailing list