[libc-commits] [libc] [libc] Add struct group header and group line parser (PR #224208)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Thu Sep 17 09:59:53 PDT 2026


https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/224208

>From 4b95fc10fdb4c551a9d720dee619add41e4d8402 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Tue, 15 Sep 2026 18:05:33 +0100
Subject: [PATCH 1/2] [libc] Add struct group header and group line parser

Introduce the POSIX struct group header and grp_utils parser module for
colon-delimited group records.

Unlike struct passwd, struct group carries a null-terminated char**
member pointer array (gr_mem). parse_line<struct group> handles placing
the gr_mem pointer array into the aligned scratch span directly:
* Fixed-buffer callers (such as reentrant lookups) report ERANGE if the
  provided scratch span cannot hold the member pointers.
* Growable DynamicBuffer callers grow the buffer when parse_line returns
  ERANGE and retry before parsing in place.

* Add struct_group header in hdr/types/struct_group.h
* Expose libc.include.grp in Linux arm and i386 headers.txt
* Add grp_utils parser and database helpers in src/grp/
* Add unit tests in test/src/grp/grp_utils_test.cpp

Assisted-by: Automated tooling, human reviewed.
---
 libc/config/linux/arm/headers.txt    |   1 +
 libc/config/linux/i386/headers.txt   |   3 +-
 libc/hdr/types/CMakeLists.txt        |   8 ++
 libc/hdr/types/struct_group.h        |  27 ++++
 libc/src/CMakeLists.txt              |   1 +
 libc/src/grp/CMakeLists.txt          |  29 ++++
 libc/src/grp/grp_utils.cpp           | 199 +++++++++++++++++++++++++++
 libc/src/grp/grp_utils.h             |  50 +++++++
 libc/test/src/CMakeLists.txt         |   1 +
 libc/test/src/grp/CMakeLists.txt     |  21 +++
 libc/test/src/grp/grp_utils_test.cpp | 191 +++++++++++++++++++++++++
 11 files changed, 530 insertions(+), 1 deletion(-)
 create mode 100644 libc/hdr/types/struct_group.h
 create mode 100644 libc/src/grp/CMakeLists.txt
 create mode 100644 libc/src/grp/grp_utils.cpp
 create mode 100644 libc/src/grp/grp_utils.h
 create mode 100644 libc/test/src/grp/CMakeLists.txt
 create mode 100644 libc/test/src/grp/grp_utils_test.cpp

diff --git a/libc/config/linux/arm/headers.txt b/libc/config/linux/arm/headers.txt
index f8426f32a3bfc..af0631843a157 100644
--- a/libc/config/linux/arm/headers.txt
+++ b/libc/config/linux/arm/headers.txt
@@ -7,6 +7,7 @@ set(TARGET_PUBLIC_HEADERS
     libc.include.errno
     libc.include.fenv
     libc.include.float
+    libc.include.grp
     libc.include.inttypes
     libc.include.langinfo
     libc.include.libgen
diff --git a/libc/config/linux/i386/headers.txt b/libc/config/linux/i386/headers.txt
index 1235db6e0b119..21c792b046e19 100644
--- a/libc/config/linux/i386/headers.txt
+++ b/libc/config/linux/i386/headers.txt
@@ -1,7 +1,8 @@
 set(TARGET_PUBLIC_HEADERS
   libc.include.alloca
   libc.include.assert
-  libc.include.pwd
   libc.include.cpio
+  libc.include.grp
   libc.include.langinfo
+  libc.include.pwd
 )
diff --git a/libc/hdr/types/CMakeLists.txt b/libc/hdr/types/CMakeLists.txt
index 36819d531d164..6949e73a78daa 100644
--- a/libc/hdr/types/CMakeLists.txt
+++ b/libc/hdr/types/CMakeLists.txt
@@ -185,6 +185,14 @@ add_proxy_header_library(
     libc.include.llvm-libc-types.struct_passwd
 )
 
+add_proxy_header_library(
+  struct_group
+  HDRS
+    struct_group.h
+  FULL_BUILD_DEPENDS
+    libc.include.llvm-libc-types.struct_group
+)
+
 add_proxy_header_library(
   rsize_t
   HDRS
diff --git a/libc/hdr/types/struct_group.h b/libc/hdr/types/struct_group.h
new file mode 100644
index 0000000000000..ce4d382e8f19d
--- /dev/null
+++ b/libc/hdr/types/struct_group.h
@@ -0,0 +1,27 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Proxy for struct group.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_HDR_TYPES_STRUCT_GROUP_H
+#define LLVM_LIBC_HDR_TYPES_STRUCT_GROUP_H
+
+#ifdef LIBC_FULL_BUILD
+
+#include "include/llvm-libc-types/struct_group.h"
+
+#else
+
+#include <grp.h>
+
+#endif // LIBC_FULL_BUILD
+
+#endif // LLVM_LIBC_HDR_TYPES_STRUCT_GROUP_H
diff --git a/libc/src/CMakeLists.txt b/libc/src/CMakeLists.txt
index ad2716cdc0724..0c2d776e5faae 100644
--- a/libc/src/CMakeLists.txt
+++ b/libc/src/CMakeLists.txt
@@ -31,6 +31,7 @@ if(${LIBC_TARGET_OS} STREQUAL "linux")
   add_subdirectory(dirent)
   add_subdirectory(err)
   add_subdirectory(fcntl)
+  add_subdirectory(grp)
   add_subdirectory(poll)
   add_subdirectory(pthread)
   add_subdirectory(pwd)
diff --git a/libc/src/grp/CMakeLists.txt b/libc/src/grp/CMakeLists.txt
new file mode 100644
index 0000000000000..9e460e376a14f
--- /dev/null
+++ b/libc/src/grp/CMakeLists.txt
@@ -0,0 +1,29 @@
+if(NOT TARGET libc.src.__support.File.file OR
+   NOT TARGET libc.src.__support.File.platform_file)
+  return()
+endif()
+
+add_object_library(
+  grp_utils
+  HDRS
+    grp_utils.h
+  SRCS
+    grp_utils.cpp
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.stdint_proxy
+    libc.hdr.types.gid_t
+    libc.hdr.types.size_t
+    libc.hdr.types.struct_group
+    libc.src.__support.CPP.span
+    libc.src.__support.File.file
+    libc.src.__support.File.platform_file
+    libc.src.__support.ctype_utils
+    libc.src.__support.error_or
+    libc.src.__support.macros.attributes
+    libc.src.__support.macros.config
+    libc.src.__support.pwd.dynamic_buffer
+    libc.src.__support.pwd.field_tokenizer
+    libc.src.__support.pwd.flat_file_db
+    libc.src.__support.str_to_integer
+)
diff --git a/libc/src/grp/grp_utils.cpp b/libc/src/grp/grp_utils.cpp
new file mode 100644
index 0000000000000..6439070e30055
--- /dev/null
+++ b/libc/src/grp/grp_utils.cpp
@@ -0,0 +1,199 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 helper functions and parser for grp.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/grp/grp_utils.h"
+#include "hdr/errno_macros.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/span.h"
+#include "src/__support/ctype_utils.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/pwd/dynamic_buffer.h"
+#include "src/__support/pwd/field_tokenizer.h"
+#include "src/__support/pwd/flat_file_db.h"
+#include "src/__support/str_to_integer.h"
+
+#ifndef LIBC_COPT_GROUP_FILE_PATH
+#define LIBC_COPT_GROUP_FILE_PATH "/etc/group"
+#endif
+
+namespace LIBC_NAMESPACE_DECL {
+namespace {
+
+// TODO: Replace with cpp::count when available in
+// src/__support/CPP/algorithm.h.
+size_t count_group_members(cpp::span<const char> line) {
+  size_t max_members = 1;
+  for (char c : line) {
+    if (c == ',')
+      ++max_members;
+  }
+  return max_members;
+}
+
+// Parse fixed fields (name, passwd, gid).
+bool parse_group_fields(cpp::span<char> line, struct group *grp,
+                        cpp::span<char> *members_out) {
+  if (line.empty() || !grp || !members_out)
+    return false;
+
+  pwd::FieldTokenizer tokenizer(line, ':');
+
+  auto name = tokenizer.next_field();
+  if (!name || name->empty() || name->front() == '\0')
+    return false;
+  grp->gr_name = name->data();
+
+  auto passwd = tokenizer.next_field();
+  if (!passwd)
+    return false;
+  grp->gr_passwd = passwd->data();
+
+  auto gid_str = tokenizer.next_field();
+  if (!gid_str || gid_str->empty() || !internal::isdigit(gid_str->front()))
+    return false;
+  auto gid_res = internal::strtointeger<gid_t>(gid_str->data(), 10);
+  if (gid_res.has_error() || gid_res.parsed_len <= 0 ||
+      static_cast<size_t>(gid_res.parsed_len) + 1 != gid_str->size() ||
+      (*gid_str)[gid_res.parsed_len] != '\0')
+    return false;
+  grp->gr_gid = gid_res.value;
+
+  auto members_field = tokenizer.next_field();
+  if (!members_field)
+    return false;
+
+  // Trailing delimiters or fields are invalid.
+  if (tokenizer.next_field())
+    return false;
+
+  *members_out = *members_field;
+  return true;
+}
+
+} // namespace
+
+namespace pwd {
+
+template <>
+ErrorOr<void> parse_line<struct group>(cpp::span<char> line,
+                                       cpp::span<char> scratch,
+                                       struct group *grp) {
+  if (!grp || line.empty() || line.back() != '\0')
+    return Error(EINVAL);
+
+  // Line excluding terminating null byte.
+  cpp::span<char> text = line.first(line.size() - 1);
+  for (char c : text) {
+    if (c == '\0')
+      return Error(EINVAL);
+  }
+
+  size_t max_members = count_group_members(text);
+  uintptr_t tail = reinterpret_cast<uintptr_t>(scratch.data());
+  size_t pad = (alignof(char *) - tail % alignof(char *)) % alignof(char *);
+  if (scratch.size() < pad)
+    return Error(ERANGE);
+  size_t remaining_bytes = scratch.size() - pad;
+  if (remaining_bytes / sizeof(char *) < max_members + 1)
+    return Error(ERANGE);
+
+  cpp::span<char *> mem_ptrs(
+      reinterpret_cast<char **>(scratch.subspan(pad).data()),
+      remaining_bytes / sizeof(char *));
+  if (!grp::parse_group_line(line, grp, mem_ptrs))
+    return Error(EINVAL);
+  return {};
+}
+
+} // namespace pwd
+
+namespace grp {
+
+bool parse_group_line(cpp::span<char> line, struct group *grp,
+                      cpp::span<char *> mem_ptrs) {
+  if (mem_ptrs.empty())
+    return false;
+
+  cpp::span<char> members_field;
+  if (!parse_group_fields(line, grp, &members_field))
+    return false;
+
+  size_t member_count = 0;
+  if (!members_field.empty() && members_field.front() != '\0') {
+    pwd::FieldTokenizer member_tokenizer(members_field, ',');
+    while (auto member = member_tokenizer.next_field()) {
+      if (member->empty() || member->front() == '\0')
+        continue;
+      if (member_count + 1 >= mem_ptrs.size())
+        return false;
+      mem_ptrs[member_count++] = member->data();
+    }
+  }
+
+  if (member_count >= mem_ptrs.size())
+    return false;
+  mem_ptrs[member_count] = nullptr;
+  grp->gr_mem = mem_ptrs.data();
+
+  return true;
+}
+
+namespace {
+
+LIBC_CONSTINIT pwd::FlatFileDatabase<struct group>
+    db(LIBC_COPT_GROUP_FILE_PATH);
+// Note: These static buffers are process-global and NOT protected by a mutex
+// at this stage. POSIX getgrent is non-reentrant.
+//
+// A single static buffer is reused across non-reentrant group calls via
+// realloc, growing only to the high-water mark of the largest record seen.
+// endgrent() closes the file stream without freeing the buffer so that
+// pointers returned prior to endgrent() remain valid until the next
+// non-reentrant call.
+LIBC_CONSTINIT pwd::DynamicBuffer line_buffer;
+struct group grp_entry;
+
+} // namespace
+
+void TESTONLY_set_group_path(const char *path) {
+  close();
+  line_buffer.release();
+  db.set_path(path ? path : LIBC_COPT_GROUP_FILE_PATH);
+}
+
+void TESTONLY_reset_group_path() {
+  close();
+  line_buffer.release();
+  db.set_path(LIBC_COPT_GROUP_FILE_PATH);
+}
+
+ErrorOr<void> open() { return db.setdb(); }
+
+ErrorOr<void> close() { return db.enddb(); }
+
+ErrorOr<struct group *> read_next() {
+  auto res = db.getnext(&grp_entry, line_buffer);
+  if (!res.has_value())
+    return Error(res.error());
+  if (!res.value())
+    return nullptr;
+  return &grp_entry;
+}
+
+} // namespace grp
+} // namespace LIBC_NAMESPACE_DECL
diff --git a/libc/src/grp/grp_utils.h b/libc/src/grp/grp_utils.h
new file mode 100644
index 0000000000000..4f5a269a855a1
--- /dev/null
+++ b/libc/src/grp/grp_utils.h
@@ -0,0 +1,50 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Declarations of helper functions and parser for grp.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC_GRP_GRP_UTILS_H
+#define LLVM_LIBC_SRC_GRP_GRP_UTILS_H
+
+#include "hdr/types/size_t.h"
+#include "hdr/types/struct_group.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/error_or.h"
+#include "src/__support/macros/config.h"
+#include "src/__support/pwd/flat_file_db.h"
+
+namespace LIBC_NAMESPACE_DECL {
+namespace pwd {
+
+template <>
+ErrorOr<void> parse_line<struct group>(cpp::span<char> line,
+                                       cpp::span<char> scratch,
+                                       struct group *grp);
+
+} // namespace pwd
+
+namespace grp {
+
+// Parse a colon-separated group line into a struct group.
+bool parse_group_line(cpp::span<char> line, struct group *grp,
+                      cpp::span<char *> mem_ptrs);
+
+void TESTONLY_set_group_path(const char *path);
+void TESTONLY_reset_group_path();
+
+ErrorOr<void> open();
+ErrorOr<void> close();
+ErrorOr<struct group *> read_next();
+
+} // namespace grp
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC_GRP_GRP_UTILS_H
diff --git a/libc/test/src/CMakeLists.txt b/libc/test/src/CMakeLists.txt
index f8758930817ae..463485768dcde 100644
--- a/libc/test/src/CMakeLists.txt
+++ b/libc/test/src/CMakeLists.txt
@@ -100,6 +100,7 @@ add_subdirectory(inttypes)
 if(${LIBC_TARGET_OS} STREQUAL "linux")
   add_subdirectory(err)
   add_subdirectory(fcntl)
+  add_subdirectory(grp)
   add_subdirectory(poll)
   add_subdirectory(sched)
   add_subdirectory(sys)
diff --git a/libc/test/src/grp/CMakeLists.txt b/libc/test/src/grp/CMakeLists.txt
new file mode 100644
index 0000000000000..dbc1ac765455b
--- /dev/null
+++ b/libc/test/src/grp/CMakeLists.txt
@@ -0,0 +1,21 @@
+add_custom_target(libc_grp_unittests)
+
+if(NOT TARGET libc.src.grp.grp_utils)
+  return()
+endif()
+
+add_libc_test(
+  grp_utils_test
+  SUITE
+    libc_grp_unittests
+  SRCS
+    grp_utils_test.cpp
+  DEPENDS
+    libc.hdr.errno_macros
+    libc.hdr.types.gid_t
+    libc.hdr.types.size_t
+    libc.hdr.types.struct_group
+    libc.src.__support.CPP.span
+    libc.src.__support.pwd.flat_file_db
+    libc.src.grp.grp_utils
+)
diff --git a/libc/test/src/grp/grp_utils_test.cpp b/libc/test/src/grp/grp_utils_test.cpp
new file mode 100644
index 0000000000000..d07ce965b411c
--- /dev/null
+++ b/libc/test/src/grp/grp_utils_test.cpp
@@ -0,0 +1,191 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+/// Unittests for parse_group_line.
+///
+//===----------------------------------------------------------------------===//
+
+#include "hdr/errno_macros.h"
+#include "hdr/types/gid_t.h"
+#include "hdr/types/size_t.h"
+#include "hdr/types/struct_group.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/pwd/flat_file_db.h"
+#include "src/grp/grp_utils.h"
+#include "test/UnitTest/Test.h"
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_Success) {
+  char line[] = "wheel:x:10:root,admin,user1";
+  char *mem_ptrs[8];
+  struct group grp;
+  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  ASSERT_TRUE(ok);
+  EXPECT_STREQ(grp.gr_name, "wheel");
+  EXPECT_STREQ(grp.gr_passwd, "x");
+  EXPECT_EQ(grp.gr_gid, static_cast<gid_t>(10));
+  ASSERT_NE(grp.gr_mem, nullptr);
+  EXPECT_STREQ(grp.gr_mem[0], "root");
+  EXPECT_STREQ(grp.gr_mem[1], "admin");
+  EXPECT_STREQ(grp.gr_mem[2], "user1");
+  EXPECT_EQ(grp.gr_mem[3], nullptr);
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_EmptyMembers) {
+  char line[] = "nogroup:x:65534:";
+  char *mem_ptrs[4];
+  struct group grp;
+  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  ASSERT_TRUE(ok);
+  EXPECT_STREQ(grp.gr_name, "nogroup");
+  EXPECT_STREQ(grp.gr_passwd, "x");
+  EXPECT_EQ(grp.gr_gid, static_cast<gid_t>(65534));
+  ASSERT_NE(grp.gr_mem, nullptr);
+  EXPECT_EQ(grp.gr_mem[0], nullptr);
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_LeadingTrailingAndConsecutiveCommas) {
+  char line[] = "test:x:100:,user1,,user2,";
+  char *mem_ptrs[8];
+  struct group grp;
+  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  ASSERT_TRUE(ok);
+  EXPECT_STREQ(grp.gr_name, "test");
+  EXPECT_STREQ(grp.gr_passwd, "x");
+  EXPECT_EQ(grp.gr_gid, static_cast<gid_t>(100));
+  ASSERT_NE(grp.gr_mem, nullptr);
+  EXPECT_STREQ(grp.gr_mem[0], "user1");
+  EXPECT_STREQ(grp.gr_mem[1], "user2");
+  EXPECT_EQ(grp.gr_mem[2], nullptr);
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_SingleMember) {
+  char line[] = "bin:x:1:bin";
+  char *mem_ptrs[4];
+  struct group grp;
+  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  ASSERT_TRUE(ok);
+  EXPECT_STREQ(grp.gr_name, "bin");
+  EXPECT_STREQ(grp.gr_passwd, "x");
+  EXPECT_EQ(grp.gr_gid, static_cast<gid_t>(1));
+  ASSERT_NE(grp.gr_mem, nullptr);
+  EXPECT_STREQ(grp.gr_mem[0], "bin");
+  EXPECT_EQ(grp.gr_mem[1], nullptr);
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_InvalidNumeric) {
+  char *mem_ptrs[4];
+  struct group grp;
+
+  char line1[] = "root:x:abc:root";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line1, &grp, mem_ptrs));
+
+  char line2[] = "root:x:-1:root";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line2, &grp, mem_ptrs));
+
+  char line3[] = "root:x::root";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line3, &grp, mem_ptrs));
+
+  char line4[] = "root:x:999999999999999999999999999999:root";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line4, &grp, mem_ptrs));
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_MissingFields) {
+  char *mem_ptrs[4];
+  struct group grp;
+
+  char line1[] = "root:x";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line1, &grp, mem_ptrs));
+
+  char line2[] = "root:x:0";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line2, &grp, mem_ptrs));
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_EmptyGroupName) {
+  char *mem_ptrs[4];
+  struct group grp;
+  char line[] = ":x:0:root";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs));
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_TrailingGarbage) {
+  char *mem_ptrs[4];
+  struct group grp;
+  char line[] = "root:x:0:root:extra";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs));
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_NullInput) {
+  char *mem_ptrs[4];
+  struct group grp;
+  LIBC_NAMESPACE::cpp::span<char> empty;
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(empty, &grp, mem_ptrs));
+
+  char line[] = "root:x:0:root";
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line, nullptr, mem_ptrs));
+
+  LIBC_NAMESPACE::cpp::span<char *> empty_mem;
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line, &grp, empty_mem));
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_MemberBufferTooSmall) {
+  char line[] = "wheel:x:10:root,admin";
+  char *mem_ptrs[2]; // Can only hold 1 member + 1 nullptr
+  struct group grp;
+  EXPECT_FALSE(LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs));
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseLine_FixedBufferErangeWhenNoScratchSpace) {
+  // The line fits in buffer, but there is no space left for gr_mem pointers.
+  char buffer[] = "wheel:x:10:root,admin";
+  struct group grp;
+  auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
+      LIBC_NAMESPACE::cpp::span<char>(buffer, sizeof(buffer)), {}, &grp);
+  ASSERT_FALSE(res.has_value());
+  EXPECT_EQ(res.error(), ERANGE);
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseLine_EmbeddedNullByteRejected) {
+  constexpr const char RAW[] = "wheel:x:10:root\0admin";
+  char buffer[128];
+  for (size_t i = 0; i < sizeof(RAW); ++i)
+    buffer[i] = RAW[i];
+  struct group grp;
+  auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
+      LIBC_NAMESPACE::cpp::span<char>(buffer, sizeof(RAW)),
+      LIBC_NAMESPACE::cpp::span<char>(buffer + sizeof(RAW),
+                                      sizeof(buffer) - sizeof(RAW)),
+      &grp);
+  ASSERT_FALSE(res.has_value());
+  EXPECT_EQ(res.error(), EINVAL);
+}
+
+TEST(LlvmLibcGrpUtilsTest, ParseLine_SuccessWithTailForMemberPointers) {
+  constexpr const char *LINE = "wheel:x:10:root,admin,user1,user2";
+  constexpr size_t LEN = 33;
+  char buffer[128];
+  for (size_t i = 0; i <= LEN; ++i)
+    buffer[i] = LINE[i];
+
+  struct group grp;
+  auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
+      LIBC_NAMESPACE::cpp::span<char>(buffer, LEN + 1),
+      LIBC_NAMESPACE::cpp::span<char>(buffer + LEN + 1,
+                                      sizeof(buffer) - (LEN + 1)),
+      &grp);
+  ASSERT_TRUE(res.has_value());
+  EXPECT_STREQ(grp.gr_name, "wheel");
+  EXPECT_STREQ(grp.gr_passwd, "x");
+  EXPECT_EQ(grp.gr_gid, static_cast<gid_t>(10));
+  ASSERT_NE(grp.gr_mem, nullptr);
+  EXPECT_STREQ(grp.gr_mem[0], "root");
+  EXPECT_STREQ(grp.gr_mem[1], "admin");
+  EXPECT_STREQ(grp.gr_mem[2], "user1");
+  EXPECT_STREQ(grp.gr_mem[3], "user2");
+  EXPECT_EQ(grp.gr_mem[4], nullptr);
+}

>From 67ab2478a1768286d29a6f83f12138d4e18efe51 Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 17 Sep 2026 17:42:35 +0100
Subject: [PATCH 2/2] [libc] Address PR 224208 review feedback in grp_utils

Add an explanatory comment describing the padding calculation used to
align scratch storage for gr_mem pointer array elements in parse_line.
Mark immutable local variables as const in grp_utils and its unit
tests.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/grp/grp_utils.cpp           | 29 +++++++++++++++-------------
 libc/test/src/grp/grp_utils_test.cpp | 14 +++++++-------
 2 files changed, 23 insertions(+), 20 deletions(-)

diff --git a/libc/src/grp/grp_utils.cpp b/libc/src/grp/grp_utils.cpp
index 6439070e30055..f1c4890179f12 100644
--- a/libc/src/grp/grp_utils.cpp
+++ b/libc/src/grp/grp_utils.cpp
@@ -53,17 +53,17 @@ bool parse_group_fields(cpp::span<char> line, struct group *grp,
 
   pwd::FieldTokenizer tokenizer(line, ':');
 
-  auto name = tokenizer.next_field();
+  const auto name = tokenizer.next_field();
   if (!name || name->empty() || name->front() == '\0')
     return false;
   grp->gr_name = name->data();
 
-  auto passwd = tokenizer.next_field();
+  const auto passwd = tokenizer.next_field();
   if (!passwd)
     return false;
   grp->gr_passwd = passwd->data();
 
-  auto gid_str = tokenizer.next_field();
+  const auto gid_str = tokenizer.next_field();
   if (!gid_str || gid_str->empty() || !internal::isdigit(gid_str->front()))
     return false;
   auto gid_res = internal::strtointeger<gid_t>(gid_str->data(), 10);
@@ -73,7 +73,7 @@ bool parse_group_fields(cpp::span<char> line, struct group *grp,
     return false;
   grp->gr_gid = gid_res.value;
 
-  auto members_field = tokenizer.next_field();
+  const auto members_field = tokenizer.next_field();
   if (!members_field)
     return false;
 
@@ -97,22 +97,25 @@ ErrorOr<void> parse_line<struct group>(cpp::span<char> line,
     return Error(EINVAL);
 
   // Line excluding terminating null byte.
-  cpp::span<char> text = line.first(line.size() - 1);
-  for (char c : text) {
+  const cpp::span<char> text = line.first(line.size() - 1);
+  for (const char c : text) {
     if (c == '\0')
       return Error(EINVAL);
   }
 
-  size_t max_members = count_group_members(text);
-  uintptr_t tail = reinterpret_cast<uintptr_t>(scratch.data());
-  size_t pad = (alignof(char *) - tail % alignof(char *)) % alignof(char *);
+  const size_t max_members = count_group_members(text);
+  const uintptr_t tail = reinterpret_cast<uintptr_t>(scratch.data());
+  // Calculate padding needed to align scratch to alignof(char *) so
+  // that gr_mem pointer array elements can be safely stored.
+  const size_t pad =
+      (alignof(char *) - tail % alignof(char *)) % alignof(char *);
   if (scratch.size() < pad)
     return Error(ERANGE);
-  size_t remaining_bytes = scratch.size() - pad;
+  const size_t remaining_bytes = scratch.size() - pad;
   if (remaining_bytes / sizeof(char *) < max_members + 1)
     return Error(ERANGE);
 
-  cpp::span<char *> mem_ptrs(
+  const cpp::span<char *> mem_ptrs(
       reinterpret_cast<char **>(scratch.subspan(pad).data()),
       remaining_bytes / sizeof(char *));
   if (!grp::parse_group_line(line, grp, mem_ptrs))
@@ -136,7 +139,7 @@ bool parse_group_line(cpp::span<char> line, struct group *grp,
   size_t member_count = 0;
   if (!members_field.empty() && members_field.front() != '\0') {
     pwd::FieldTokenizer member_tokenizer(members_field, ',');
-    while (auto member = member_tokenizer.next_field()) {
+    while (const auto member = member_tokenizer.next_field()) {
       if (member->empty() || member->front() == '\0')
         continue;
       if (member_count + 1 >= mem_ptrs.size())
@@ -187,7 +190,7 @@ ErrorOr<void> open() { return db.setdb(); }
 ErrorOr<void> close() { return db.enddb(); }
 
 ErrorOr<struct group *> read_next() {
-  auto res = db.getnext(&grp_entry, line_buffer);
+  const auto res = db.getnext(&grp_entry, line_buffer);
   if (!res.has_value())
     return Error(res.error());
   if (!res.value())
diff --git a/libc/test/src/grp/grp_utils_test.cpp b/libc/test/src/grp/grp_utils_test.cpp
index d07ce965b411c..b78d4e49ee3b4 100644
--- a/libc/test/src/grp/grp_utils_test.cpp
+++ b/libc/test/src/grp/grp_utils_test.cpp
@@ -24,7 +24,7 @@ TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_Success) {
   char line[] = "wheel:x:10:root,admin,user1";
   char *mem_ptrs[8];
   struct group grp;
-  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  const bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
   ASSERT_TRUE(ok);
   EXPECT_STREQ(grp.gr_name, "wheel");
   EXPECT_STREQ(grp.gr_passwd, "x");
@@ -40,7 +40,7 @@ TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_EmptyMembers) {
   char line[] = "nogroup:x:65534:";
   char *mem_ptrs[4];
   struct group grp;
-  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  const bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
   ASSERT_TRUE(ok);
   EXPECT_STREQ(grp.gr_name, "nogroup");
   EXPECT_STREQ(grp.gr_passwd, "x");
@@ -53,7 +53,7 @@ TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_LeadingTrailingAndConsecutiveCommas) {
   char line[] = "test:x:100:,user1,,user2,";
   char *mem_ptrs[8];
   struct group grp;
-  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  const bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
   ASSERT_TRUE(ok);
   EXPECT_STREQ(grp.gr_name, "test");
   EXPECT_STREQ(grp.gr_passwd, "x");
@@ -68,7 +68,7 @@ TEST(LlvmLibcGrpUtilsTest, ParseGroupLine_SingleMember) {
   char line[] = "bin:x:1:bin";
   char *mem_ptrs[4];
   struct group grp;
-  bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
+  const bool ok = LIBC_NAMESPACE::grp::parse_group_line(line, &grp, mem_ptrs);
   ASSERT_TRUE(ok);
   EXPECT_STREQ(grp.gr_name, "bin");
   EXPECT_STREQ(grp.gr_passwd, "x");
@@ -144,7 +144,7 @@ TEST(LlvmLibcGrpUtilsTest, ParseLine_FixedBufferErangeWhenNoScratchSpace) {
   // The line fits in buffer, but there is no space left for gr_mem pointers.
   char buffer[] = "wheel:x:10:root,admin";
   struct group grp;
-  auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
+  const auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
       LIBC_NAMESPACE::cpp::span<char>(buffer, sizeof(buffer)), {}, &grp);
   ASSERT_FALSE(res.has_value());
   EXPECT_EQ(res.error(), ERANGE);
@@ -156,7 +156,7 @@ TEST(LlvmLibcGrpUtilsTest, ParseLine_EmbeddedNullByteRejected) {
   for (size_t i = 0; i < sizeof(RAW); ++i)
     buffer[i] = RAW[i];
   struct group grp;
-  auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
+  const auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
       LIBC_NAMESPACE::cpp::span<char>(buffer, sizeof(RAW)),
       LIBC_NAMESPACE::cpp::span<char>(buffer + sizeof(RAW),
                                       sizeof(buffer) - sizeof(RAW)),
@@ -173,7 +173,7 @@ TEST(LlvmLibcGrpUtilsTest, ParseLine_SuccessWithTailForMemberPointers) {
     buffer[i] = LINE[i];
 
   struct group grp;
-  auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
+  const auto res = LIBC_NAMESPACE::pwd::parse_line<struct group>(
       LIBC_NAMESPACE::cpp::span<char>(buffer, LEN + 1),
       LIBC_NAMESPACE::cpp::span<char>(buffer + LEN + 1,
                                       sizeof(buffer) - (LEN + 1)),



More information about the libc-commits mailing list