FORTIFY 是 C 标准库的扩展集,用于拦截对 memset、sprintf 和 open 和其他标准函数的错误使用。它有三大功能:
如果在编译时 FORTIFY 检测到错误调用标准库函数,则在错误得到修复前将不允许编译您的代码。
如果 FORTIFY 未获取足够的信息,或者如果确定代码是安全的,FORTIFY 将不会对任何信息进行编译。这意味着,当 FORTIFY 在找不到错误的上下文中使用时,它的运行时开销为 0。
否则,FORTIFY 会进一步进行检查,动态确定可疑代码是否存在错误。如果检测到错误,FORTIFY 将输出部分调试信息并中止程序运行。
思考下例,它是 FORTIFY 在真实代码中捕获的一个错误:
struct Foo {
int val;
struct Foo *next;
};
void initFoo(struct Foo *f) {
memset(&f, 0, sizeof(struct Foo));
}
FORTIFY 发现,我们错误地将 &f 作为 memset 的第一个参数进行传递,而实际上应为 f。通常,很难追踪此类错误,因为它表面上可能将 8 个字节的附加 0 写入任意的堆叠部分,而实际上对 *f 不进行任何操作。因此,取决于您的编译器优化设置、initFoo 的用法和您的项目的测试标准,此错误可能长时间被忽略。有了 FORTIFY,您会收到如下编译时错误:
/path/to/file.c: call to unavailable function 'memset': memset called with size bigger than buffer
memset(&f, 0, sizeof(struct Foo));
^~~~~~
以下列函数为例,说明如何进行运行时检查:
// 2147483648 == pow(2, 31). Use sizeof so we get the nul terminator,
// as well.
#define MAX_INT_STR_SIZE sizeof("2147483648")
struct IntAsStr {
char asStr[MAX_INT_STR_SIZE];
int num;
};
void initAsStr(struct IntAsStr *ias) {
sprintf(ias->asStr, "%d", ias->num);
}
此代码适用于所有正数。但是,当您传入 num <= -1000000 的 IntAsStr 时,sprintf 会将 MAX_INT_STR_SIZE+1 个字节写入到 ias->asStr 中。如果不使用 FORTIFY,此差一错误(结果会清除 num 中的一个字节)可能被静默忽略。而有了它,程序可以输出堆叠追踪信息、内存映射,并在中止运行时转储内核信息。
FORTIFY 还可以执行其他几项检查,例如确保对 open 的调用具有适当的参数,而它的主要用途是捕获上述与内存有关的错误。
但是,FORTIFY 并不能捕获当前与内存有关的 所有 错误。以下列代码为例:
__attribute__((noinline)) // Tell the compiler to never inline this function.
inline void intToStr(int i, char *asStr) { sprintf(asStr, “%d”, num); }
char *intToDupedStr(int i) {
const int MAX_INT_STR_SIZE = sizeof(“2147483648”);
char buf[MAX_INT_STR_SIZE];
intToStr(i, buf);
return strdup(buf);
}
由于 FORTIFY 根据缓冲区的类型及其分配位置(如果可见)确定缓冲区的大小,因此它无法捕获此错误。在本例中,FORTIFY 放弃检测是因为:
如果您想知道为什么这里有非内联属性,那是因为,如果 intToStr 内联到 intToDupedStr 中,FORTIFY 可能可以捕获此错误。这是因为,编译器可以因此确定 asStr 指向同一个内存作为缓冲区,这是一个 sizeof(buf) 字节大小的内存区。