使用Macroni增强C语言:宏感知的静态分析与安全构造

本文介绍了Macroni工具,它通过结合Clang宏和MLIR技术,实现了对C语言的渐进式增强。包括强类型定义、Linux内核Sparse增强、Rust风格不安全区域及信号处理安全等实际应用案例。

Holy Macroni! 渐进式语言增强的配方 - The Trail of Bits博客

Brent Pappas
2023年9月11日
编译器, linux, llvm, mlir, 静态分析, vast

尽管Clang被用于重构和静态分析工具,但它有一个巨大的缺陷:Clang AST不提供关于给定AST节点来自哪个CPP宏展开的溯源信息;也不将宏展开降级到LLVM中间表示(IR)代码。这使得构建宏感知的静态分析和转换极其困难,并成为一个持续的研究领域。

然而,不用再挣扎了,因为今年夏天在Trail of Bits,我创建了Macroni,使得创建宏感知的静态分析变得容易。

Macroni允许开发者用宏定义新的C语言构造的语法,并用MLIR为这些构造提供语义。Macroni使用VAST将C代码降级到MLIR,并使用PASTA获取宏到AST的溯源信息,也将宏降级到MLIR。开发者然后可以定义自定义MLIR转换器,将Macroni的输出转换为领域特定的MLIR方言,以进行更细致的分析。在这篇文章中,我将展示几个使用Macroni增强C语言以更安全的语言构造和构建C安全分析的例子。

更强的类型定义

C的typedef对于给底层类型赋予语义上有意义的名称很有用;然而,C编译器在类型检查时不使用这些名称,而只对底层类型进行类型检查。当语义类型代表不同的格式或度量时,这可能导致一种简单的类型混淆错误,如下所示:

1
2
3
4
5
typedef double fahrenheit;
typedef double celsius;
fahrenheit F;
celsius C;
F = C; // 没有编译器错误或警告

图1:C类型检查只考虑typedef的底层类型。

上面的代码成功通过类型检查,但类型fahrenheit和celsius之间的语义差异不应被忽略,因为它们代表不同温标的值。仅使用C的typedef无法强制执行这种强类型。

使用Macroni,我们可以用宏定义强类型定义的语法,并用MLIR为它们实现自定义类型检查。以下是一个使用宏定义代表华氏度和摄氏度温度的强类型定义的例子:

1
2
3
#define STRONG_TYPEDEF(name) name
typedef double STRONG_TYPEDEF(fahrenheit);
typedef double STRONG_TYPEDEF(celsius);

图2:使用宏定义强C类型定义的语法

用STRONG_TYPEDEF()宏包装typedef名称允许Macroni识别那些名称从STRONG_TYPEDEF()调用展开的typedef,并将它们转换为自定义MLIR方言的类型(例如,temp),如下所示:

1
2
3
4
5
6
%0 = hl.var "F" : !hl.lvalue<!temp.fahrenheit>
%1 = hl.var "C" : !hl.lvalue<!temp.celsius>
%2 = hl.ref %1 : !hl.lvalue<!temp.celsius>
%3 = hl.ref %0 : !hl.lvalue<!temp.fahrenheit>
%4 = hl.implicit_cast %3 LValueToRValue : !hl.lvalue<!temp.fahrenheit> -> !temp.fahrenheit
%5 = hl.assign %4 to %2 : !temp.fahrenheit, !hl.lvalue<!temp.celsius> -> !temp.celsius

图3:Macroni使我们能够将类型定义降级为MLIR类型并强制执行严格类型检查。

通过将这些宏属性类型定义集成到类型系统中,我们现在可以为它们定义自定义类型检查规则。例如,我们可以对温度值之间的操作强制执行严格类型检查,以便上述程序无法通过类型检查。我们还可以为温度值添加自定义类型转换逻辑,以便将一种温标的温度值转换为另一种温标时隐式插入转换指令。

使用宏添加强类型定义语法的原因是宏既向后兼容又可移植。虽然我们可以使用GNU或Clang的属性语法通过注释我们的typedef来用Clang识别我们的自定义类型,但我们不能保证annotate()在所有平台和编译器上的可用性,而我们可以对C预处理器的存在做出强有力的假设。

现在,你可能会想:C已经有一种称为struct的强类型定义形式。所以我们也可以通过将我们的typedef类型转换为结构体(例如,struct fahrenheit { double value; })来强制执行更严格的类型检查,但这会改变类型的API和ABI,破坏现有的客户端代码和向后兼容性。如果我们将typedef改为结构体,编译器可能会产生完全不同的汇编代码。例如,考虑以下函数定义:

1
fahrenheit convert(celsius temp) { return (temp * 9.0 / 5.0) + 32.0; }

图4:摄氏到华氏转换函数的定义

如果我们使用宏属性类型定义来定义我们的强类型定义,那么Clang为convert(25)调用发出以下LLVM IR。convert函数的LLVM IR表示与其C对应物匹配,接受一个double类型的参数并返回一个double类型的值。

1
tail call double @convert(double noundef 2.500000e+01)

图5:使用宏属性类型定义定义强类型定义的convert(25)的LLVM IR

与此对比的是当我们使用结构体定义我们的强类型定义时Clang产生的IR。函数调用现在接受四个参数而不是一个。第一个ptr参数表示convert将存储返回值的位置。想象一下,如果客户端代码根据原始调用约定调用这个新版本的convert会发生什么。

1
2
call void @convert(ptr nonnull sret(%struct.fahrenheit) align 8 %1,
                   i32 undef, i32 inreg 1077477376, i32 inreg 0)

图6:使用结构体定义强类型定义的convert(25)的LLVM IR

弱类型定义应该是强的在C代码库中普遍存在,包括关键基础设施如libc和Linux内核。如果你想为标准类型如time_t添加强类型检查,保持API和ABI兼容性至关重要。如果你将time_t包装在结构体中(例如,struct strict_time_t { time_t t; })以提供强类型检查,那么不仅所有访问time_t类型值的API需要更改,而且这些使用站点的ABI也需要更改。已经使用裸time_t值的客户端需要费力地更改其代码中所有使用time_t的地方,以改用你的结构体来激活更强的类型检查。另一方面,如果你使用宏属性类型定义来别名原始time_t(即,typedef time_t STRONG_TYPEDEF(time_t)),那么time_t的API和ABI将保持一致,正确使用time_t的客户端代码可以保持不变。

增强Linux内核中的Sparse

2003年,Linus Torvalds构建了一个自定义预处理器、C解析器和编译器,称为Sparse。Sparse执行Linux内核特定的类型检查。Sparse依赖于宏,如__user,散布在内核代码中,这些宏在正常构建配置下什么都不做,但在定义__CHECKER__宏时扩展为__attribute__((address_space(…)))的使用。

用__CHECKER__门控宏定义是必要的,因为大多数编译器不提供钩入宏或实现自定义安全检查的方法……直到今天。使用Macroni,我们可以钩入宏并执行类似Sparse的安全检查和

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计