Macroni:利用宏和MLIR增强C语言类型安全与静态分析

本文介绍Macroni工具,通过结合Clang宏扩展和MLIR中间表示,实现C语言的强类型检查、Linux内核安全增强、类Rust不安全区域及信号处理安全,提升静态分析能力。

Holy Macroni! 渐进式语言增强的配方

尽管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识别哪些typedef名称是从STRONG_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的安全检查和分析。但Sparse仅限于C(通过实现自定义C预处理器和解析器),而Macroni适用于任何可由Clang解析的代码(即C、C++和Objective C)。

我们将钩入的第一个Sparse宏是__user。内核当前将__user定义为Sparse识别的属性:

1
# define __user     __attribute__((noderef, address_space(__user)))

图7:Linux内核的__user宏

Sparse钩入此属性以查找来自用户空间的指针,如下例所示。noderef告诉Sparse这些指针不能被解引用(例如*uaddr = 1),因为它们的来源不可信。

1
u32 __user *uaddr;

图8:使用__user宏注释变量来自用户空间的示例

Macroni可以钩入宏和扩展属性,将声明降级为MLIR,如下所示:

1
%0 = hl.var "uaddr" : !hl.lvalue<!sparse.user<!hl.ptr<!hl.elaborated<!hl.typedef<"u32">>>>>

图9:被Macroni降级为MLIR的内核代码

降级的MLIR代码通过将来自用户空间的声明包装在sparse.user类型中,将注释嵌入类型系统。现在我们可以为用户空间变量添加自定义类型检查逻辑,类似于我们之前创建强类型定义的方式。我们甚至可以钩入Sparse特定的宏__force,以按需禁用强类型检查,正如开发人员当前所做:

1
2
3
4
raw_copy_to_user(void __user *to, const void *from, unsigned long len)
{
   return __copy_user((__force void *)to, from, len);
}

图10:使用__force宏将指针复制到用户空间的示例

我们还可以使用Macroni识别内核中的RCU读侧关键部分,并验证某些RCU操作仅出现在这些部分内。例如,考虑以下对rcu_dereference()的调用:

1
2
3
rcu_read_lock();
rcu_dereference(sbi->s_group_desc)[i] = bh;
rcu_read_unlock();

图11:Linux内核中RCU读侧关键部分内的rcu_derefernce()调用

上述代码在关键部分调用rcu_derefernce()——即从调用rcu_read_lock()开始到调用rcu_read_unlock()结束的代码区域。只应在读侧关键部分内调用rcu_dereference();然而,无法强制执行此约束。

使用Macroni,我们可以使用rcu_read_lock()和rcu_read_unlock()调用来识别形成隐含词法代码区域的关键部分,然后检查rcu_dereference()调用仅出现在这些部分内:

1
2
3
4
kernel.rcu.critical_section {
 %1 = macroni.parameter "p" : ...
 %2 = kernel.rcu_dereference rcu_dereference(%1) : ...
}

图12:将RCU关键部分降级为MLIR的结果,类型省略以简洁

上述代码将RCU关键部分和rcu_dereference()调用都转换为MLIR操作。这使得检查rcu_dereference()仅出现在正确区域变得容易。

不幸的是,RCU关键部分并不总是绑定整洁的词法代码区域,并且rcu_dereference()并不总是在这样的区域中被调用,如下例所示:

1
2
3
4
__bpf_kfunc void bpf_rcu_read_lock(void)
{
       rcu_read_lock();
}

图13:包含非词法RCU关键部分的内核代码

1
2
3
4
static inline struct in_device *__in_dev_get_rcu(const struct net_device *dev)
{
   return rcu_dereference(dev->ip_ptr);
}

图14:在RCU关键部分外调用rcu_dereference()的内核代码

我们可以使用__force宏来允许这些类型的rcu_dereference()调用,就像我们为逃避用户空间指针的类型检查所做的那样。

类Rust的不安全区域

很明显,Macroni可以帮助加强类型检查,甚至启用应用特定的类型检查规则。然而,将类型标记为强意味着承诺那种强度级别。在大型代码库中,这种承诺可能需要大量的更改集。为了使适应更强类型系统更易管理,我们可以为C设计一个“不安全”机制,类似于Rust:在不安全区域内,强类型检查不适用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define unsafe if (0); else


fahrenheit convert(celsius C) {
 fahrenheit F;
 unsafe {
         F = (C * 9.0 / 5.0) + 32.0;
 }
 return F;
}

图15:展示宏实现的不安全区域语法的C代码片段

此片段演示了我们安全API的语法:我们在潜在不安全的代码区域之前调用unsafe宏。所有未列在不安全区域中的代码将受到强类型检查,而我们可以使用unsafe宏来调用我们故意保持原样的低级代码区域。这是渐进的!

unsafe宏仅为我们安全API提供语法,而不是逻辑。为了使这个泄漏的抽象滴水不漏,我们需要将宏标记的if语句转换为我们理论安全方言中的操作:

1
2
3
4
5
...
"safety.unsafe"() ({
   ...
}) : () -> ()
...

图16:使用Macroni,我们可以将安全API降级为MLIR方言并实现安全检查逻辑。

现在我们可以禁用嵌套在unsafe宏的MLIR表示中的操作的强类型检查。

更安全的信号处理

到这一点,您可能已经注意到创建更安全语言结构的模式:我们使用宏定义语法来标记某些类型、值或代码区域遵守某些不变式,然后我们在MLIR中定义逻辑来检查这些不变式是否成立。

我们可以使用Macroni来确保信号处理程序仅执行信号安全代码。例如,考虑Linux内核中定义的以下信号处理程序:

1
2
3
4
5
static void sig_handler(int signo) {
       do_detach(if_idx, if_name);
       perf_buffer__free(pb);
       exit(0);
}

图17:Linux内核中定义的信号处理程序

sig_handler()在其定义中调用了三个其他函数,这些函数都应该在信号处理上下文中安全调用。然而,上述代码中没有检查我们仅在sig_handler()的定义内调用信号安全函数——C编译器没有表达适用于词法区域的语义检查的方法。

使用Macroni,我们可以添加宏来标记某些函数为信号处理程序,其他为信号安全,然后在MLIR中实现逻辑来检查信号处理程序仅调用信号安全函数,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define SIG_HANDLER(name) name
#define SIG_SAFE(name) name


int SIG_SAFE(do_detach)(int, const char*);
void SIG_SAFE(perf_buffer__free)(struct perf_buffer*);
void SIG_SAFE(exit)(int);


static void SIG_HANDLER(sig_handler)(int signo) { ... }

图18:标记信号处理程序和信号安全函数的基于令牌的语法

上述代码将sig_handler()标记为信号处理程序,并将其调用的三个函数标记为信号安全。每个宏调用扩展为单个令牌——我们想要标记的函数名称。使用这种方法,Macroni钩入扩展的函数名称令牌以确定函数是信号处理程序还是信号安全。

另一种方法是将这些宏定义为魔术注释,然后用Macroni钩入它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define SIG_HANDLER __attribute__((annotate("macroni.signal_handler")))
#define SIG_SAFE __attribute__((annotate("macroni.signal_safe")))


int SIG_SAFE do_detach(int, const char*);
void SIG_SAFE perf_buffer__free(struct perf_buffer*);
void SIG_SAFE exit(int);


static void SIG_HANDLER sig_handler(int signo) { ... }

图19:标记信号处理程序和信号安全函数的替代属性语法

使用这种方法,宏调用看起来更像类型说明符,有些人可能觉得更吸引人。基于令牌的语法和属性语法之间的唯一区别是后者需要编译器支持annotate()属性。如果这不是问题,或者如果类似__CHECKER__的门控是可接受的,那么任

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