我正在写一个ebpf程序来读取tc上的数据包.下面的代码被简化,以更好地理解问题.假设我有一个ebpf map,其值是如下定义的 struct :

struct value {
    int index;
    char flags[MAX_FLAGS_LEN];
};

struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 32);
    __type(key, __u8); 
    __type(value, struct value); 
} output_map SEC(".maps");

如果我的ebpf程序在主要部分全部写出来,它会工作得很好,如下所示.

SEC("classifier")
int tc_ingress(struct __sk_buff *skb)
{
    __u8 key = 1;
    struct value *value;
    value = bpf_map_lookup_elem(&output_map, &key);

    if (value == NULL){
        struct value initial_value = {
            .index = 1,
            .flags = ","
        };
        bpf_map_update_elem(&output_map, &key, &initial_value, BPF_NOEXIST);
    } else {
        if (value->index >= sizeof(value->flags)){
            goto out;
        }

    }

 
    if (value->index < sizeof(value->flags)){
        value->flags[value->index] = ',';
        value->index++;
    }

    if (value->index < sizeof(value->flags)){
        value->flags[value->index] = ',';
        value->index++;
    }

    bpf_printk("Flags: %d", value->flags);

out: 
    return TC_ACT_OK;
}

但是,当我重构代码时,如下所示:

static __always_inline void store_flags(struct value *value){
    if (value->index < sizeof(value->flags)){
        value->flags[value->index] = ',';
        value->index++;
    }

    if (value->index < sizeof(value->flags)){
        value->flags[value->index] = ',';
        value->index++;
    }
}

SEC("classifier")
int tc_ingress(struct __sk_buff *skb)
{
    __u8 key = 1;
    struct value *value;
    value = bpf_map_lookup_elem(&output_map, &key);

    if (value == NULL){
        struct value initial_value = {
            .index = 1,
            .flags = ","
        };
        bpf_map_update_elem(&output_map, &key, &initial_value, BPF_NOEXIST);
    } else {
        if (value->index >= sizeof(value->flags)){
            goto out;
        }

    }

    store_flags(value);

    bpf_printk("Flags: %d", value->flags);

out: 
    return TC_ACT_OK;
}

我会得到这个错误:

permission denied:invalid access to map value,value_size = 20 off = 20 size = 1:R4 max value超出允许的内存范围(省略了52行)

有谁知道解决办法吗?

Edit: Dylan's answer contains detailed reasoning why it happened. Modifying the code to this fixed it.

static __always_inline void store_flags(struct value *value){
    int index = value->index;

    if (index < sizeof(value->flags)){
        value->flags[index] = ',';
        index++;
    }

    if (index < sizeof(value->flags)){
        value->flags[index] = ',';
        index++;
    }

    value->index=index;
}

SEC("classifier")
int tc_ingress(struct __sk_buff *skb)
{
    __u8 key = 1;
    struct value *value;
    value = bpf_map_lookup_elem(&output_map, &key);

    if (value == NULL){
        struct value initial_value = {
            .index = 1,
            .flags = ","
        };
        bpf_map_update_elem(&output_map, &key, &initial_value, BPF_NOEXIST);
    } else {
        store_flags(value);
    }

    bpf_printk("Flags: %d", value->flags);

    return TC_ACT_OK;
}

推荐答案

这似乎归结为不幸的字节码生成和验证器的限制.

首先,我将解释验证者的部分.验证者所做的很大一部分是值跟踪.因此,对于栈上和寄存器中的变量,它保留了栈的类型(标量,指向映射值的指针,指向上下文的指针,等等)和它们的值范围(min,max,bitfield等等).正是这种价值跟踪允许版本者断言某些操作是安全的.

假设MAX_FLAGS_LEN是15,这意味着如果你试图访问索引为15或更高的value->flags,你正在读取映射值之外的数据,并可能访问你不应该访问的数据.你做的很好的判断边界判断.但是,根据编译器如何生成字节码,验证者可能看不到这一点.这是因为验证器不跟踪映射中的值信息.

验证器在字节码级别运行,因此如果编译器生成:

R2 = <map pointer>ll 
R1 = *(u16*)(R2 + 0) ; value->index
If R1 > 15: goto out ; if (value->index < sizeof(value->flags))
R2 += 2              ; change pointer to point at value->flags
R2 += R1             ; Add index
*(u8*)(R2 + 0) = ',' ; value->flags[value->index] = ','
...
out:
...

如果是这一代,则将index写入R1、判断边界并使用.这样验证器就可以追踪它了.

但是,如果编译器出于某种原因生成了这样的代码:

R2 = <map pointer>ll 
R1 = *(u16*)(R2 + 0) ; value->index
If R1 > 15: goto out ; if (value->index < sizeof(value->flags))
R4 = *(u16*)(R2 + 0) ; value->index
R2 += 2              ; change pointer to point at value->flags
R2 += R4             ; Add index
*(u8*)(R2 + 0) = ',' ; value->flags[value->index] = ','
...
out:
...

然后验证者知道R1是安全的,并判断边界.但它不知道R4,即使它们都是value->index,只是在不同的时间从内存中获得.验证者假设R4可以包含大于15的索引.

事实上,这是公平的.由于映射是跨CPU共享的,所以映射值可能在第一次内存访问和第二次内存访问之间被更改.

因此,稍微更改代码以使用中间变量,将有希望通知编译器生成第一个实例.

static __always_inline void store_flags(struct value *value){
    int index = value->index;
    if (index < sizeof(value->flags)){
        value->flags[index] = ',';
        value->index = index + 1;
    }

    index = value->index;
    if (index < sizeof(value->flags)){
        value->flags[index] = ',';
        value->index = index + 1;
    }
}

C++相关问答推荐

ARM上的Modulo Sim Aarch 64(NEON)

如何通过Zephyr(Devicetree)在PR Pico上设置UTE 1?

try 使用sigqueue函数将指向 struct 体的指针数据传递到信号处理程序,使用siginfo_t struct 体从一个进程传递到另一个进程

C如何显示字符串数组中的第一个字母

不同到达时间的轮询实现

在#include中使用C宏变量

如何在C中从函数返回指向数组的指针?

自定义变参数函数的C预处置宏和警告 suppress ?

使用sscanf获取零个或多个长度的字符串

C++中PUTS函数的返回值

try 查找带有指针的数组的最小值和最大值

即使我在C++中空闲,也肯定会丢失内存

在C中交换字符串和数组的通用交换函数

如何使用 VLA 语法使用 const 指针声明函数

为什么写入关闭管道会返回成功

添加/删除链表中的第一个元素

获取 struct 中匿名 struct 的大小

int 与 size_t 与 long

如何让 unlinkat(dir_fd, ".", AT_REMOVEDIR) 工作?

计算 e^x 很好.但不是 x 的罪