eBPF中如何获取当前进程的补充组ID信息?

来源:搜索优化作者:唐振业头衔:网络博主
导读:本期聚焦于唐振业创作的《eBPF中如何获取当前进程的补充组ID信息?》,敬请观看详情。Linux进程的补充组ID存储在内核cred结构体的group_info中,用户态通常通过getgroups系统调用读取。在eBPF程序中获取这些信息有两种主流方式:直接调用bpf_get_current_ancillary_group_ids辅助函数,或者通过bpf_get_current_task_btf获取task_struct后解析cred与group_info字段。前者简单高效但返回数量固定且需要内核版本支持,后者灵活可穿透任意深度但要求开启BTF并注意内存读取安全。本文将剖析两种方法的底层原理、适用场景与代码实现,并对比它们在可移植性、性能和可维护性上的差异,帮助开发者根据实际需求选择合适方案。

补充组ID(supplementary group IDs)是Linux进程权限模型中的重要组成部分,它允许一个用户同时属于多个组,从而获得多个组的资源访问权限。用户态程序通常使用getgroups系统调用获取当前进程的补充组列表,而在内核态或eBPF观测场景中,需要直接读取内核数据结构。eBPF提供了专门的辅助函数以及通用的BTF解析能力来完成这一任务,本文将深入讨论这两种途径的实现细节与选择依据。

eBPF中如何获取当前进程的补充组ID信息?

Linux补充组ID的存储结构与getgroups系统调用

在Linux内核中,每个进程的凭据信息保存在struct cred结构体中,该结构体由task_struct的cred指针引用。补充组ID并不直接平铺在cred里,而是通过一个名为group_info的结构体间接管理。group_info定义在include/linux/cred.h中,核心字段包括atomic_t usage用于引用计数、int ngroups表示当前组数量,以及一个灵活的small_block数组,实际存放gid_t类型的组ID值。

当进程的补充组数量较少时(通常不超过NGROUPS_SMALL),组ID直接存储在group_info内嵌的small_block数组中,避免额外的内存分配。一旦组数量超过阈值,内核会分配一个更大的内存块并让group_info的blocks指针指向它。这种设计使得读取补充组ID时必须同时关注ngroups和存储位置,不能简单地假设数据连续存放在某个固定地址。

用户态调用getgroups系统调用时,内核会从current进程的cred中取出group_info,然后把ngroups个gid_t值复制到用户提供的缓冲区。如果用户传入的大小参数为0,系统调用只返回实际的组数量而不复制数据,这是常见的查询长度用法。以下代码展示了标准用户态获取补充组ID的流程:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int ngroups = getgroups(0, NULL);
    if (ngroups < 0) {
        perror("getgroups");
        return 1;
    }
    gid_t *groups = malloc(ngroups * sizeof(gid_t));
    if (!groups) {
        perror("malloc");
        return 1;
    }
    int ret = getgroups(ngroups, groups);
    if (ret < 0) {
        perror("getgroups");
        free(groups);
        return 1;
    }
    printf("补充组数量: %d\n", ret);
    for (int i = 0; i < ret; i++) {
        printf("组ID: %u\n", groups[i]);
    }
    free(groups);
    return 0;
}

虽然系统调用简单易用,但在eBPF上下文中无法直接调用getgroups,因为eBPF程序运行在内核态,不能发起系统调用,只能读取内核数据结构或使用内核提供的辅助函数。

使用bpf_get_current_ancillary_group_ids辅助函数

为了简化eBPF程序中获取补充组ID的操作,内核提供了一个专门的辅助函数bpf_get_current_ancillary_group_ids。该函数定义在include/uapi/linux/bpf.h中,原型为long bpf_get_current_ancillary_group_ids(void *groups, int size)。调用时,eBPF程序需要提供一个缓冲区指针和缓冲区能容纳的组ID数量上限,辅助函数会把当前进程的补充组ID复制到该缓冲区,并返回实际复制的组数量。

与用户态getgroups类似,如果size参数为0,函数只返回当前进程的补充组总数而不写入任何数据。这个特性可以用于先探测数量再分配合适大小的BPF map存储结果。需要注意的是,该辅助函数从内核5.7版本开始引入,并且要求目标内核编译时开启CONFIG_BPF_SYSCALL以及相应的eBPF支持。在使用前最好通过内核特性探测确认可用性。

下面是一个完整的eBPF程序示例,它使用该辅助函数在每次进程执行时记录其补充组ID列表,并通过perf event输出到用户态:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/ptrace.h>

#define MAX_GROUPS 64

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
} events SEC(".maps");

struct event_data {
    u32 pid;
    u32 count;
    u32 groups[MAX_GROUPS];
};

SEC("tp/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx) {
    struct event_data data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    long ret = bpf_get_current_ancillary_group_ids(data.groups, MAX_GROUPS);
    if (ret < 0) {
        data.count = 0;
    } else {
        data.count = (u32)ret;
    }
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, sizeof(data));
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

该方法的优点在于代码简洁,内核已经封装好了所有复杂的group_info解析逻辑,开发者无需关心cred结构布局的变化。缺点是缓冲区大小必须在编译时确定,无法动态适应所有场景;同时该辅助函数只返回gid_t数组,不包含其他cred相关信息。此外,旧版本内核无法使用,需要提前做好兼容性处理。

通过task_struct解析group_info的底层方法

如果目标内核版本较老,或者需要获取更多关于cred的信息而不仅仅是组ID,开发者可以使用BPF CO-RE和BTF来直接读取task_struct及其关联结构。第一步是通过bpf_get_current_task_btf辅助函数获取当前进程的task_struct指针。该函数返回一个指向struct task_struct的指针,该结构体类型信息由内核BTF提供,使得eBPF程序可以安全地使用成员访问语法。

拿到task_struct后,需要沿着cred指针找到struct cred,再通过group_info指针找到struct group_info。由于这些结构体在不同内核版本中字段偏移可能变化,必须使用BPF CO-RE的内置函数(如bpf_core_read)进行读取。特别是group_info中的blocks数组和small_block内嵌数组的访问方式差异较大,需要根据ngroups与NGROUPS_SMALL的比较结果判断数据实际存储位置。

以下示例展示了一个使用BPF CO-RE读取当前进程前若干个补充组ID的程序片段:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <linux/sched.h>

#define MAX_GROUPS 32

SEC("kprobe/__x64_sys_getuid")
int trace_getuid(struct pt_regs *ctx) {
    struct task_struct *task = bpf_get_current_task_btf();
    struct cred *cred;
    struct group_info *group_info;
    unsigned int ngroups;
    u32 groups[MAX_GROUPS];
    int i, ret;

    if (!task)
        return 0;

    cred = BPF_CORE_READ(task, cred);
    if (!cred)
        return 0;

    group_info = BPF_CORE_READ(cred, group_info);
    if (!group_info)
        return 0;

    ngroups = BPF_CORE_READ(group_info, ngroups);
    if (ngroups > MAX_GROUPS)
        ngroups = MAX_GROUPS;

    for (i = 0; i < ngroups; i++) {
        gid_t gid;
        if (i < NGROUPS_SMALL) {
            gid = BPF_CORE_READ(group_info, small_block[i]);
        } else {
            gid_t *blocks = BPF_CORE_READ(group_info, blocks[0]);
            if (!blocks)
                return 0;
            bpf_probe_read_kernel(&gid, sizeof(gid), &blocks[i]);
        }
        groups[i] = (u32)gid;
    }

    // 此处可以将groups输出到map或perf buffer
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

这段代码完整演示了手动遍历group_info的全部逻辑。需要注意几点:NGROUPS_SMALL的值通常为4,但不同架构可能不同,最好通过内核头文件获取;blocks数组是二级指针结构,group_info中的blocks字段是gid_t *blocks[ ],读取时需要先取blocks[0]得到指向实际数据块的指针,再按偏移读取。另外,bpf_probe_read_kernel用于安全读取任意内核内存地址,避免直接解引用导致验证器报错。

两种方案的对比与实际应用选择

对比bpf_get_current_ancillary_group_ids辅助函数和手动解析task_struct的方法,各有优劣。辅助函数方式代码量小、可读性强,内核抽象屏蔽了底层细节,适合大多数只需要补充组ID列表的场景;但它要求内核版本在5.7及以上,且无法获取除组ID外的其他cred字段。手动解析方式灵活度极高,可以同时读取uid、gid、capabilities等所有cred相关信息,也兼容更多内核版本,前提是内核开启BTF且BPF CO-RE可用。

在性能方面,辅助函数内部实现也是直接读取cred和group_info,但经过内核优化,通常比用户在eBPF中手动逐字段读取略快。手动解析方式因为包含多次bpf_core_read和条件判断,指令数较多,但在大多数观测场景中差异可以忽略不计。如果eBPF程序运行在热路径(如每次系统调用),建议优先使用辅助函数以减少指令开销。

实际选择时可以从三个维度考虑:目标内核版本、信息需求范围、维护成本。如果只关心补充组ID且内核较新,直接调用bpf_get_current_ancillary_group_ids是最优解;如果需要同时监控多个凭据字段,或者需要兼容大量旧内核,则采用BPF CO-RE手动解析方式。无论选择哪种,都需要充分理解内核cred与group_info的结构设计,这样才能在出现异常时快速定位问题。

eBPF补充组IDgetgroups修改时间:2026-08-22 17:01:04

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。