LWN:在kernel中更安全地使用可变长度数组!

关注了就能看到更多这么棒的文章哦~

Safer flexible arrays for the kernel

By Jake Edge
September 22, 2022
LSS EU
DeepL assisted translation
https://lwn.net/Articles/908817/

在 2022 年欧洲 Linux 安全峰会(LSS EU)上,Gustavo A. R. Silva 报告了他在内核 "flexible" array 方面的工作。虽然这些数组提供了一些所谓的“灵活性”,但它们也导致了不少 bug,往往会导致安全漏洞。他一直在研究如何让 flexible array 能更安全地在内核中使用。

Silva 的北京是在嵌入式系统方面,他从事实时操作系统(RTOS)和嵌入式 Linux 的工作。在过去的六年里,他的工作主要就是提交代码到 upstream。他与内核自我保护项目(KSPP, Kernel Self Protection Project)和谷歌开源安全团队的 Linux kernel 部门配合工作。

Trailing and flexible arrays

他首先介绍了 C 语言的 array 数组功能,从最简单的 int happy_array[10]; 开始,这声明了一个包含了 10 个 int 类型元素的数组,后面可以用 0 到 9 的 index 作为索引来访问。 happy_array "只要我们确保使用它时不越界,就会保持 happy"。但是 C 语言并没有强制确保不越界,所以开发者必须要自己确保;否则的话,他们就会落入他经常称为 "The Land of Possibilities" 的境地,也就是所谓的 undefined behavior(未定义行为)。

所谓的 "trailing" array,是指在一个 struct 声明中,处于最后一个 field 的这个 array 数组。它们可以是定义了具体的 size,就像 happy_array 那样,或者它们可以代表一个 "blob" 数据,在运行才被添加给这个 struct。比如说:

struct blob_holder {
    ...
    size_t count;
    unsigned char blob[];
}

通常情况下,structure 中的一些元素会记录这个 blob 的长度,比如这个例子里的 count。这样,在 C 语言中,经常会用 training array 来构造一个可变长度的对象(VLO, variable-length object)。因此,flexible array 其实就是一个作为 VLO 来使用的 training array;其 size 是在运行时确定的。所谓的 flexible structure,就是一个使用 flexible array 作为其最后一个元素的结构。Silva 说,有三种方法来声明 flexible array。其中两种被判定为 "假的, fake" flexible array,因为它们没有使用上述 C99 方式的空括号方式的声明(这是所谓的 "真" flexible array)。许多 fake 方式的使用在是在 C99 之前出现的,它们会要么声明一个 0 元素或 1 个元素的数组作为 flexible array 使用。这种用法会导致 bug。

声明一个单个元素的 flexible array 是一个 "buggy hack" 的方式。这里的问题是,这个元素也会被计入数组(和外包的 struct)的 size 里,这很容易导致错位 1 个(off-by-one)这种 bug。struct 中的 count 字段比应该分配的空间要大 1,所以很多地方需要使用 count-1。在分析使用这种 flexible structure 的代码时,必须始终注意 sizeof() 在数组和结构中的使用。通常经过分析就会发现代码中存在 off-by-one 或者其他 bug。

使用 0 元素的 fake flexible array 是 GNU 里的一个扩展,是为了解决当时语言中缺乏真正的 flexible array 的问题而加入的。它带来的问题比单个元素数组的要少一些,因为它们不会对包围结构的 size 产生影响。真正的 flexible array 必须在结构的最后才出现,这是由编译器强制确保的。不过,fake flexible array 则可能出现在结构中的任何地方,这当然也就会导致其他类型的问题。

Problems

sizeof() 操作符对这三种变种得到的 size 返回值是不同的。对于单个元素的数组这种情况,数组的 size 是数组类型中一个元素的 size;对于零元素数组,size 就是零。但是对于真正的 flexible array,sizeof() 会在编译时报错,因为 size 是未知的。

442e87a98753674b6491fd25b48787c2.png

[Gustavo A. R. Silva]。

作为 KSPP 工作的一部分,他所做的第一个 flexible-array-transformation fix 就展示了 fake flexible array 可能产生的问题。在结构的末尾声明了一个长度为 0 的数组,但今后有人在 flexible array 之后添加了一个用于 read-copy-update(RCU)的字段。编译器不会给出警告,所以这个 bug 就从 2011 年一直持续存在,直到他在 2019 年进行了 fix。他使用了一个真正的 flexible array 的声明(并将其移到 struct 最后);现在,如果有人在最后添加一个新的结构成员的话,编译器会报告错误。

人们一直在致力于用 -Warray-bounds 选项在编译器中来启用 array-bound (数组边界)检查,但 fake flexible array 造成了太多的误报(同时也会有一些真正的 bug 报出来)。一个 flexible array 在被访问时使用的 index 已经超出 array 的末尾,这种情况经常出现。在开启边界检查之前,这些都需要先被 fix。

他在 2021 年中期的 fix 就是一个简单的例子。其中有一个单个元素的数组在被访问时使用了[1] 作为 index,这显然越界了;只要把它改成一个真正的 flexible array,就可以去除 warning。其他的例子就比较复杂了,但归根结底都是换成了真正的 flexible array;此外移除这些单个元素的数组也就可以去除一些分配 size-1 的计算的代码。

flexible array 可以是 memcpy() 操作的源地址或者目标地址,因此在加固 memcpy() 的工作中也许要考虑这部分。在内核启用 CONFIG_FORTIFY_SOURCE 时,memcpy() 会使用 __builtin_object_size() 函数(type 参数为 1)来计算运行时 source 和 destination 的大小。

然而,对于真正的 flexible array 来说,这个函数返回-1,因为它不能确定 size。fake flexible array 确实会有一个 size,但事实上 __builtin_object_size() 对这些数组也会返回 -1。正如他在幻灯片中所展示的那样,这个行为如果跟 sizeof() 的行为结合起来,会使事情变得有点混乱。

__builtin_object_size(flex_struct->one_element_array, 1) == -1
__builtin_object_size(flex_struct->zero_length_array, 1) == -1
__builtin_object_size(flex_struct->flex_array_member, 1) == -1

sizeof(flex_struct->one_element_array) == size-of-element-type
sizeof(flex_struct->zero_length_array) == 0
sizeof(flex_struct->flex_array_member) == ? /* Error */

因为 __builtin_object_size() 不能确定 trailing array 的 size,所以今天在 memcpy() 中没有对这些数组进行边界检查(使用了 CONFIG_FORTIFY_SOURCE)。更让人奇怪的是,__builtin_object_size() 对任何 trailing array 都会返回-1,哪怕它指定了大于 1 的 size。因为 __builtin_object_size() 没有返回 trailing array 的 size (哪怕是那些它表面上可以确定 size 的数组),如今在 memcpy() 中就没有对这些数组进行边界检查 (在使用 CONFIG_FORTIFY_SOURCE 的情况下) 。__builtin_object_size() 采用这种行为的原因,就是要支持历史遗留代码,那些以固定的长度来声明 trailing array 但又将它们视为 flexible array 的代码。他展示了一个 BSD 版本的 struct sockaddr,其中有一个 trailing array 是 char sa_data[14],在运行时实际上可以容纳 255 字节。

为了让 memcpy() 能够对 trailing array 进行合理性检查,就需要消除 flexible array 声明中的这种模糊性。所有要作为 flexible array 使用的数组都应该用 [] 来声明为真正的 flexible array;然后,可以指示编译器将固定长度的 trailing array 当作普通的固定长度数组。他提到了一个 GCC 的 bug 报告,其中包含了解决这个问题的编译器改动。

Compiler flag

在即将发布的 GCC 13 和 Clang 16 版本中,有一个新的编译器标志,允许开发者设置 flexible array 的严格程度。-fstrict-flex-arrays[=n] (通常缩写为-fsfa)。n的默认设置是 0,这意味着跟当前的行为比起来没有什么变化,所有 trailing array 都被 __builtin_object_size()视为 flexible array。n 的值从 1 到 3,通过改变__builtin_object_size() 的行为来逐步提高这里检查的严格程度:

  • -fsfa=1: 只有用 [1], [0], 和 [] 声明的尾部数组被视为 flexible array;__builtin_object_size() 对其它数组返回适当的长度。

  • -fsfa=2: 只有用[0]和[]声明的尾部数组被视为 flexible array;__builtin_object_size()对其他数组返回适当的长度。

  • -fsfa=3: 只有用[]声明的尾部数组被视为 flexible array;__builtin_object_size()对任何有具体大小的数组返回适当的长度。

不幸的是,Clang 的开发者没有(暂时还没有?)被说服加入 -fsfa=3;Silva 说,关于这个问题正在进行讨论。将内核中的 flexible array 转变为真正的 flexible array 的工作已经进行了好几年,还有更多的工作要做。对零元素的数组的使用所进行的改造是相当直接明确的,但是单个元素数组的改造更加困难,因为它们需要更仔细地人为检查代码,从而找到 off-by-one 的问题。在这样做之后,并且编译器也准备好了的话,memcpy() 就能够对所有不是 flexible array 的 trailing array 进行边界检查,所以所有固定大小的数组最终都可以在内核中进行边界检查了。

那么我们有方案可以让所有这种数组都得到边界检查了,那么对实际的 flexible array 的检查呢?Silva 说,这是一个更具挑战性的问题,但是有一些方案提出来了。这里的关键是要确定持有数组长度的结构成员。这可以通过对数组加上一个 attribute 来完成,如下所示:

struct bounded_flex_struct {
...
size_t elements;
struct foo flex_array[]
  __attribute__((__element_count__(elements)));
  };

然而,在从单个元素的 flexible array 转换到真正的 flexible array 时,有一些用户空间的 API 问题需要解决。最开始是尝试同时支持现有的 API 以及新的机制,在面向 user space 的结构中来重复这个字段,并将它们放在一个 union 中,这样用户空间可以采用一种方式来使用数组,kernel 可以采用另一种方式:

struct farray {
    union {
        struct {
            ... /* renamed versions of the members */
            size_t renamed_count;
            int orig_array_name[1];
        };
        struct {
            ... /* members with existing names */
            size_t count;
            int orig_array_name_flex[];
        };
    };
};

这样做会在大量代码中造麻烦,所以添加了__DECLARE_FLEX_ARRAY() 这个 helper 宏,会被放入一个只包含数组的 union 中:

struct farray {
    ...
    size_t count;
    union {
        int orig_array_name[1];
        __DECLARE_FLEX_ARRAY(int, orig_array_name_flex);
    };
};

在这两种情况下,用户空间都会继续使用 orig_array_name,而内核将使用 orig_array_name_flex。有一点需要注意的是,structure 的 size 并没有改变;单个元素的数组仍然会对 structure 的 size 有贡献。

Status, conclusions, and questions

目前,内核中的大多数零长度的数组已经被改造完了,其中包括处理用户空间的 API 问题。但是没有什么方法可以避免今后加入的代码中又加进来了,所以他提醒内核开发者不要引入新的使用点。对单个元素的数组的改造仍然是一项正在进行的工作;这项工作更具挑战性,需要确保被改变的代码的相应维护者能感到放心,能确保这些改动并没有破坏任何功能。为此,他正在使用各种 diff 类型的工具,试图验证改造过程中是否有明显的行为改变。

他重申,重点是把所有内核中使用的 flexible array 都变成真正的 flexible array,然后确保不增加零长度或单个元素的 flexible array 的使用。内核的安全性可以通过 -fstrict-flex-arrays=3 得到显著改善,这就意味着很需要说服 Clang 开发者来支持这个设置。这项工作已经发现了内核中的一些漏洞,而且随着工作的继续进行,肯定会发现更多的漏洞。这还需要一些时间,但我们有一个清晰的愿景,那就是希望达到让所有的 trailing array, 不管是固定大小还是 flexible 的,在 memcpy()中都要进行边界检查。

Silva 在演讲结束时接受了一些评论和提问。LSS EU 组织者 Elena Reshetova 指出,当从 atomic_t 转换到 recount_t 时,那些开发者也面临着类似的问题,也就是阻止开发者在新代码中使用他们正在进行转换的类型。他们最后在 0-day test robot 中整合了一个 test,捕捉这种新增代码,并发送电子邮件。这种方式很有效,她鼓励 Silva 也尝试类似的做法。

编者提问, Clang 的开发者有什么理由反对新编译器标志中的最严格设置。Silva 说,他们的立场是 "不要使用零长度数组",但他把这个问题留给了 Kees Cook 来回答,Cook 说他可以介绍一下 "这方面的细节问题"。Cook 说,Clang 开发者们指出,根据标准,零长度的数组不是合法的 C 语言,所以如果移除这个对零长度数组支持的 GNU 扩展,那么零长度数组就不再存在了,所以 =2 级别就足够了。添加另一个选项来支持不是 flexible array 的零长度数组,在 Clang 社区看来似乎毫无意义。

"不幸的是,这不是我们世界的现实情况。" 在 GNU 扩展被添加时,一些代码将零长度数组作为 flexible array 使用,而其他代码则将其作为没有元素的真实数组,从而方便比如在结构内放置 marker 标记之类的操作。此外,在内核中有些数组通常有一些固定的 size,但是在某些配置中,这个 size 可能会降为零。

Cook 说,可能有办法解决 Clang 缺乏这种选项的问题,但是 Clang 的开发者更容易接受零长度数组已经存在的这个现实,以及内核(至少)希望能够不再把它们当作 flexible array。Silva 说,有一个 flag 可以对零长度数组的使用代码给出 warning,但是它在内核代码上产生了 6 万个 warning,所以这也不是一个明智的做法。很明显,希望 Clang 社区在这一点上能改变观点。

[我想感谢 LWN 的订阅者支持我去都柏林参加欧洲 Linux 安全峰会。]

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值