| //===----------------------------------------------------------------------===// | |
| // | |
| // 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/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/OSUtil/linux/syscall_wrappers/setgroups.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/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" | |
| #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, ':'); | |
| const auto name = tokenizer.next_field(); | |
| if (!name || name->empty() || name->front() == '\0') | |
| return false; | |
| grp->gr_name = name->data(); | |
| const auto passwd = tokenizer.next_field(); | |
| if (!passwd) | |
| return false; | |
| grp->gr_passwd = passwd->data(); | |
| 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); | |
| 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; | |
| const 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. | |
| const cpp::span<char> text = line.first(line.size() - 1); | |
| for (const char c : text) { | |
| if (c == '\0') | |
| return Error(EINVAL); | |
| } | |
| 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); | |
| const size_t remaining_bytes = scratch.size() - pad; | |
| if (remaining_bytes / sizeof(char *) < max_members + 1) | |
| return Error(ERANGE); | |
| 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)) | |
| 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 (const 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 { | |
| // Tracked alongside the database's own copy because the reentrant lookups open | |
| // their own scoped database rather than sharing the iteration stream. | |
| const char *group_file_path = LIBC_COPT_GROUP_FILE_PATH; | |
| 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. | |
| // | |
| // getgrent, getgrnam, and getgrgid share a single static buffer and struct | |
| // group, as allowed by POSIX. The buffer grows as needed to fit the largest | |
| // record read so far, and is reused without shrinking. endgrent() closes the | |
| // file stream without freeing the buffer so that pointers returned before | |
| // endgrent() remain valid until the next non-reentrant call. | |
| LIBC_CONSTINIT pwd::DynamicBuffer line_buffer; | |
| LIBC_CONSTINIT struct group grp_entry = {}; | |
| // The lookups are shared between the caller-supplied fixed buffer used by the | |
| // reentrant entrypoints and the process-global growable buffer used by the | |
| // non-reentrant ones. Both scan a scoped stream of their own so that a lookup | |
| // does not disturb an in-progress getgrent iteration. | |
| template <typename BufferType> | |
| ErrorOr<bool> lookup_by_name(cpp::string_view name, struct group *grp, | |
| BufferType &buffer, const char *path) { | |
| pwd::ScopedFlatFileDatabase<struct group> local_db(path); | |
| const auto matcher = [name](const struct group &entry) { | |
| return cpp::string_view(entry.gr_name) == name; | |
| }; | |
| return local_db.lookup(matcher, grp, buffer); | |
| } | |
| template <typename BufferType> | |
| ErrorOr<bool> lookup_by_gid(gid_t gid, struct group *grp, BufferType &buffer, | |
| const char *path) { | |
| pwd::ScopedFlatFileDatabase<struct group> local_db(path); | |
| const auto matcher = [gid](const struct group &entry) { | |
| return entry.gr_gid == gid; | |
| }; | |
| 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 (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; | |
| 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 const gid_t *data() const { return buf; } | |
| [[nodiscard]] LIBC_INLINE gid_t operator[](size_t i) const { | |
| LIBC_ASSERT(i < count); | |
| return buf[i]; | |
| } | |
| }; | |
| ErrorOr<void> populate_gid_list(cpp::string_view user, gid_t group, | |
| GidList &gid_list, const char *path) { | |
| 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); | |
| } | |
| } | |
| } | |
| return {}; | |
| } | |
| } // namespace | |
| void TESTONLY_set_group_path(const char *path) { | |
| close(); | |
| line_buffer.release(); | |
| group_file_path = path ? path : LIBC_COPT_GROUP_FILE_PATH; | |
| db.set_path(group_file_path); | |
| } | |
| void TESTONLY_reset_group_path() { | |
| close(); | |
| line_buffer.release(); | |
| group_file_path = LIBC_COPT_GROUP_FILE_PATH; | |
| db.set_path(group_file_path); | |
| } | |
| ErrorOr<void> open() { return db.setdb(); } | |
| ErrorOr<void> close() { return db.enddb(); } | |
| ErrorOr<struct group *> read_next() { | |
| const auto res = db.getnext(&grp_entry, line_buffer); | |
| if (!res.has_value()) | |
| return Error(res.error()); | |
| if (!res.value()) | |
| return nullptr; | |
| return &grp_entry; | |
| } | |
| ErrorOr<bool> find_by_name(cpp::string_view name, struct group *grp, | |
| cpp::span<char> buffer, const char *path) { | |
| return lookup_by_name(name, grp, buffer, path ? path : group_file_path); | |
| } | |
| ErrorOr<bool> find_by_gid(gid_t gid, struct group *grp, cpp::span<char> buffer, | |
| const char *path) { | |
| return lookup_by_gid(gid, grp, buffer, path ? path : group_file_path); | |
| } | |
| ErrorOr<struct group *> find_by_name(cpp::string_view name) { | |
| const auto res = | |
| lookup_by_name(name, &grp_entry, line_buffer, group_file_path); | |
| if (!res.has_value()) | |
| return Error(res.error()); | |
| if (!res.value()) | |
| return nullptr; | |
| return &grp_entry; | |
| } | |
| ErrorOr<struct group *> find_by_gid(gid_t gid) { | |
| const auto res = lookup_by_gid(gid, &grp_entry, line_buffer, group_file_path); | |
| if (!res.has_value()) | |
| return Error(res.error()); | |
| if (!res.value()) | |
| return nullptr; | |
| 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; | |
| const auto res = populate_gid_list(user, group, gid_list, path); | |
| if (!res.has_value()) | |
| return Error(res.error()); | |
| 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(); | |
| } | |
| ErrorOr<int> init_groups(cpp::string_view user, gid_t group, const char *path) { | |
| GidList gid_list; | |
| const auto res = populate_gid_list(user, group, gid_list, path); | |
| if (!res.has_value()) | |
| return Error(res.error()); | |
| // TODO: Implement setxid propagation across threads. | |
| return linux_syscalls::setgroups(gid_list.size(), gid_list.data()); | |
| } | |
| } // namespace grp | |
| } // namespace LIBC_NAMESPACE_DECL |