之前在工作中涉及对 2022 年 Linux Security Summit North America(LSS-NA)的调研,并向同事们介绍了其中一个演讲:Meaningful Bounds Checking in the Linux Kernel (演讲材料 )。在分享过程中,有这样一段内容:
1__FORTIFY_INLINE void *memcpy(void *dst, const void *src, size_t size)
2{
3 size_t dst_size = __builtin_object_size(dst, 1);
4 size_t src_size = __builtin_object_size(src, 1);
5
6 if (__builtin_constant_p(size)) { /* Compile-time */
7 if (dst_size < size)
8 __write_overflow();
9 if (src_size < size)
10 __read_overflow2();
11 }
12 if (dst_size < size || src_size < size)
13 fortify_panic(__func__); /* Run-time */
14 return __underlying_memcpy(dst, src, size);
15}
这里讲的是开了 CONFIG_FORTIFY_SOURCE
后的 memcpy()
的实现,会对参数增加一些编译时(Compile-time)和运行时(Run-time)的检查。有同事就问到:这里的编译时检查是怎么实现的?就这个问题作了一番探究后,写成本文记录一下。
总结
内核代码中的编译时检查基于以下两点实现:
-
error ("message")
函数属性(Function Attributes)1,参考 GCC 手册 6.33.1 节 。假如(在消除死代码后)代码中(仍)存在对带有该属性的函数的调用,那么编译就会报错。
-
__OPTIMIZE__
预置宏(Predefined Macro),参考 CPP 手册 3.7.2 节 。该宏在编译器开启了任意级别的优化时都会被定义。
error ("message")
函数属性
先来看看前文提到的编译时检查的代码:
1if (__builtin_constant_p(size)) { /* Compile-time */
2 if (dst_size < size)
3 __write_overflow();
4 if (src_size < size)
5 __read_overflow2();
6}
__write_overflow()
和 __read_overflow2()
的定义如下:
1/* include/linux/fortify-string.h */
2void __read_overflow2(void) __compiletime_error("detected read beyond size of object (2nd parameter)");
3void __write_overflow(void) __compiletime_error("detected write beyond size of object (1st parameter)");
1/* include/linux/compiler_attributes.h */
2#if __has_attribute(__error__)
3# define __compiletime_error(msg) __attribute__((__error__(msg)))
4#else
5# define __compiletime_error(msg)
6#endif
可见,__write_overflow()
和 __read_overflow2()
就是两个带有 error(msg)
属性的函数。
__OPTIMIZE__
预置宏
对带 error(msg)
属性的函数无条件的调用,会让编译器无条件地报错,这不是我们想要的。若可借助编译器优化,把对这类函数的调用作为死代码的一部分消除掉,这样编译器就不会报错了。当前 Linux 内核默认开启 -O2
的编译优化:
1# init/Kconfig
2choice
3 prompt "Compiler optimization level"
4 default CC_OPTIMIZE_FOR_PERFORMANCE
5
6config CC_OPTIMIZE_FOR_PERFORMANCE
7 bool "Optimize for performance (-O2)"
8 ...
9
10config CC_OPTIMIZE_FOR_PERFORMANCE_O3
11 bool "Optimize more for performance (-O3)"
12 ...
13
14config CC_OPTIMIZE_FOR_SIZE
15 bool "Optimize for size (-Os)"
16 ...
17
18endchoice
1# Makefile
2ifdef CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE
3KBUILD_CFLAGS += -O2
4else ifdef CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE_O3
5KBUILD_CFLAGS += -O3
6else ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
7KBUILD_CFLAGS += -Os
8endif
可见内核代码具备这样的条件。回顾前文提到的编译时检查代码:
1size_t dst_size = __builtin_object_size(dst, 1);
2size_t src_size = __builtin_object_size(src, 1);
3
4if (__builtin_constant_p(size)) {
5 if (dst_size < size)
6 __write_overflow();
7 if (src_size < size)
8 __read_overflow2();
9}
通过 __builtin_constant_p()
确定 size
是常量,即编译时已知的量后,才有可能进行编译时检查。若 size
不是常量,则
1if (__builtin_constant_p(size)) {
2 ...
3}
就等同于
1if (false) {
2 ...
3}
在开启优化后,整个编译时检查的代码块都会作为死代码被优化掉。同理,在确定 size
是常量后,__builtin_object_size()
2 返回的 {dst,src}_size
也是常量,那么 if (dst_size < size)
和 if (src_size < size)
也可被优化为 if (true)
或 if (false)
,从而令 __write_overflow()
和 __read_overflow2()
被优化或遗留。若被遗留则最终触发编译器报错。
等等,到目前为止好像没 __OPTIMIZE__
什么事儿啊?实际上,上述的一切都依赖编译器优化,而该宏的存在就指示了编译器优化的开启:
1/* include/linux/string.h */
2#if !defined(__NO_FORTIFY) && defined(__OPTIMIZE__) && defined(CONFIG_FORTIFY_SOURCE)
3#include <linux/fortify-string.h>
4#endif
可见,整个 include/linux/fortify-string.h
都是以开启优化为前提的。
结语
这里列一些内核中与编译时检查相关的代码,可供搜寻相关上下文:
BUILD_BUG_ON_MSG
__write_overflow
- 但不包括
check_*_overflow()
(这些属于运行时检查)
后续:偶然看到了个简单粗暴的编译时检查设计 。挺有趣的😄
-
用法
__attribute__((__error__(msg)))
。这里__error__()
等同于error()
,详见 GCC 手册 6.39 节 。 ↩︎ -
参考 GCC 手册 6.58 节 。 ↩︎