关注了就能看到更多这么棒的文章哦~
GCC features to help harden the kernel
By Jonathan Corbet
October 5, 2023
Cauldron
ChatGPT translation
https://lwn.net/Articles/946041/
加固Linux内核是一项无休止的任务,需要在多个方面努力推动。有时,并不需要在内核本身里进行修改,而是通过修改其他工具比如编译器来对加固工作提供重要帮助。在2023年GNU工具大会上,Qing Zhao介绍了GCC编译器在内核加固方面已经完成的工作,以及仍需完成的内容。
她首先提到,Kernel Self Protection Projection project 是许多内核加固工作的主要集中地。加固可以通过多种方式实现,包括修复已知的安全漏洞,这些漏洞可以通过静态检查器、模糊测试或代码检查来发现。不过,修复漏洞是一个永无止境的任务;如果可能的话,更好的方法是彻底消除某一整个类别的漏洞。因此,内核中的许多工作集中在消除诸如堆栈溢出、整数溢出、格式字符串注入、指针泄漏、使用未初始化变量、释放后继续使用等问题。
她表示,GCC 11(2021年4月)版本包括了函数返回时对函数使用的寄存器进行清零的能力,这有助于防止信息泄漏。现在这是默认开启的。相比之下,GCC 12(2022年5月)添加了栈变量的自动初始化;这也已经在内核构建选项中默认开启。GCC 13(2023年4月)增加了对灵活数组成员的更严格的处理。
Zhao简要提到了内核社区希望在未来的编译器版本中看到的一些功能,包括更好的支持灵活数组检查、减少-warray-bounds选项导致的假阳性警告、更好的整数溢出检查、支持控制流完整性检查等等。
回到灵活数组,Zhao指出,内核中的越界数组访问是一个主要的漏洞来源。这些问题可以通过边界检查来防止,如果已知所访问的数组的大小的话。对于固定数组,大小在编译时是已知的,因此可以在编译时(如果可能)或在运行时进行数组访问检查。然而,对于动态大小数组来说,问题更加复杂。在C语言中,这些数组采用两种形式:可变长度数组,或者结构体中的灵活数组成员;目前内核只使用了后者。
灵活数组成员是嵌入在结构体中作为最后一个元素的数组。它通常被声明为元素数量为零或一(尽管后者往往是一个常见的漏洞来源),或者只是写作 array[]
。在分配结构体实例的空间时,必须将其大小调整到足够大,从而可以容纳实际的数组,因为数组的长度将在不同实例中也不一样。
在GCC 12中,无论数组声明的size多大,所有被定义为结构体的最后成员的数组都被视为是灵活数组。因此,哪怕是这样写的数组也是一样:
struct foo {
int int_field;
int array[10];
};
编译器也会认为它的大小是可变的,即使开发人员的本意可能并非如此;因此,对这些数组的访问不会执行边界检查。在GCC 13中,`-fstrict-flex-arrays`选项允许控制哪些数组被视为灵活数组;LWN https://lwn.net/Articles/908817/ 提供了它的工作原理概述。结果是,边界检查可以更容易地应用于本来不打算改变大小的数组。
不过,仍然存在一些问题;Zhao提到了一个包含了灵活数组成员的结构体嵌套到另一种结构体类型中的情况:
struct s1 {
int flex_array[0];
};
struct s2 {
type_t some_field;
struct s1 flex_struct;
}
即使灵活结构体是结构体的最后成员(如上面的`s2`),GCC版本小于14的版本也会错误地将数组视为固定的。Zhao已经为这个特定问题提供了fix。当灵活结构体不是包含结构的最后字段时,会出现另一个问题。在这种情况下,不确定编译器应该做什么,GCC之前会直接接受这种结构。新的`-Wflex-array-member-not-at-end`选项将对这样的代码发出警告。
联合体(Union)中的灵活数组成员又是另一个问题;GCC会接受声明为`array[0]`的成员,但不接受`array[]`(合法)的形式。这使得在最严格的-fstrict-flex-array模式下编译包含这样成员的联合体就变成不可能的任务了。只包含灵活数组成员的联合体会引发另一个问题:它们最终可能成为一个零长度的对象,这不是C标准允许的。目前通过添加一个固定长度成员来解决这个问题;未来可能会尝试允许完全灵活的联合体作为GCC的扩展。
灵活数组的使用目前会干扰边界检查,但是在运行时,任何特定数组的实际长度是已知的(或者至少应该是已知的)。如果可以将这个大小传达给编译器,那么边界检查就可以被添加上来了。有两种潜在的声明这些信息的方式;一种是在类型本身内嵌它:
struct foo {
size_t len;
char array[.len*4];
};
这种语法允许使用表达式(在本例中是"len字段的值的四倍")。她表示,这是更清晰的做法,但它有可能会改变数组的维度,从而破坏现有代码的ABI,因此更难被采纳,而且这种语法的改变肯定需要经过大量的讨论才能被接受。
另一种选择是在灵活数组成员上添加一个属性。这可以保持现有的ABI,更容易采纳,也可以扩展到其他类型(例如指针)。不过,它更难扩展到更复杂的表达式。GCC 14已经添加了没有表达式支持的`counted_by()`属性;它目前只能引用同一结构体中的另一个字段。
struct foo {
size_t len;
char array[] __counted_by(len);
};
在这种情况下,`len`字段只能用于直接确定`array`的size,不允许使用表达式。这个属性目前仅适用于灵活数组本身的size;未来的工作可能会使它达到更进一步的程度,例如在结构体的分配size不足以容纳数组时给出warning。
目前还有一些讨论,希望将这种检查扩展到指针值;苹果提出了一个更复杂的`-fbounds-safety`标志的提案(适用于LLVM),实现了这个想法。它是现有`counted_by()`行为的一个超集;如果得到认可,将更费力来实现和采纳,但以后会考虑它。
边界检查只有在检查是正确的情况下才有用,不过有一个问题是存在假阳性warning。具体来说,使用jump threading方式进行优化的代码可能会产生假阳性警告。其中一些问题已在GCC 13中得到解决,另一些问题仍未解决。这个问题阻止了在内核构建中默认启用-Warray-bounds选项。人们已经有了一些关于如何标记已使用jump threading的代码和初步想法,可以消除由此产生的warning。
还有一个完全不同的问题是整数溢出(overflow)检测。在C标准中,对于无符号整数值是有定义overflow的,但对于有符号值和指针则没有定义。对于未定义的情况,GCC提供了选项,可以定义在检测到溢出时的期望的行为。然而,对于无符号溢出的情况,由于行为是明确定义的,所以没有相关选项。但无符号溢出通常是无意出现的,也是值得检测的。也许需要一个新的选项来允许在这种情况下进行检测。
至于有符号溢出,她指出,-fwrapv选项就可以让行为明确定义下来;在溢出时,变量将发生回绕(wrap around)。但是,尽管内核需要在大多数情况下捕获溢出,但偶尔也需要允许。Florian Weimer指出,现在有一个内置机制可以用来禁用特定操作的检查;Zhao表示她会研究一下。
这时时间不多了,Zhao无法继续讨论控制流完整性相关选项。但这个讨论使得情况很清楚了,人们已经付出了相当多的工作来改进GCC,使其能够有助于内核加固(当然还有对其他程序的加固)。但就像许多其他工作一样,保护内核免受攻击者的攻击似乎永无止境。在可预见的将来,编译器和内核开发人员将有很多工作要做。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~