LWN:用klint避免违反原子上下文限制!

文章介绍了Rust开发者GaryGuo在LinuxPlumbersConference上展示的klint工具,该工具能在原子上下文中检测并标记潜在的安全问题,以避免在内核编程中出现由于违反睡眠规则导致的bug,如死锁和数据竞争。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关注了就能看到更多这么棒的文章哦~

Preventing atomic-context violations in Rust code with klint

By Jonathan Corbet
November 17, 2023
LPC
ChatGPT translation
https://lwn.net/Articles/951550/

在内核编程中有一个核心约束,即在原子上下文中运行时需要避免 sleep。遵守这一规则通常是开发者的职责;然而,Rust 开发者希望编译器在可能的情况下确保代码的安全性。在 2023 年 Linux Plumbers Conference 上,Gary Guo 通过远程链接展示了 klint 工具,该工具可以在原子上下文中找到并标记出许多违规行为,在最终暴露出来成为影响用户的 bug 之前。

Rust 的理念是,经过编译器验证的安全 Rust 代码不会导致未定义的行为。这种行为包括解析引用(dereferencing)没有实际意义的(dangling)指针或空指针、缓冲区溢出、数据竞争访问或违反别名规则;"安全"的代码不会执行这些操作。Rust-for-Linux 项目试图创建一个环境,让许多内核功能都可以用安全代码实现。然而,一些让用户不满意的行为(如内存泄漏、死锁、紧急情况和死机),却被判定是"安全"的。因为这些行为是预先定义好的,所以算是“安全的”,尽管显然是不好的结果。

在内核中,原子上下文提出了一些有趣的安全问题。例如,如果代码执行以下序列:

spin_lock(&lock);
/* ... */
mutex_lock(&mutex);   /* 可能进行调度 */
/* ... */
spin_unlock(&lock);

结果可能会发生死锁,只要有另一个线程尝试在相同的 CPU 上获取相同的自旋锁。这是"安全"的(但"不好"的)代码。但是以下代码呢?

rcu_read_lock();
schedule();
rcu_read_unlock();

在这种情况下,哪怕是从 Rust 的角度来看也很难判断此代码是否安全。RCU 假定那些运行在 RCU 临界区内的代码中不会发生上下文切换;调用调度程序会打破这个假设。在这种情况下,原子上下文违规确实可能是一个安全问题,可能导致使用后释放的 bug、或者数据竞争等更严重的问题。这对于 C 代码来说是可以接受的,因为"安全性"和"正确性"之间的区别并没有定义得那么明确。然而,Rust 开发者试图遵循不同的规则;因此,他们无法设计出一个允许在原子上下文中休眠的安全 API。

1450f7351ca267cdafa02048a75b3371.png

要避免这种情况并不容易。一个可能的解决方案是将所有阻塞操作都标记为不安全。Guo 承认,这很可能会被大家认为是一个坏主意。另一种方法是使用 token type (令牌类型),这在 Rust 中通常用于表示能力;这可能会引出复杂且难以使用的 API。还可以进行运行时检查,使用某些内核配置中现在维护着的抢占计数来实现。然而,这会增加运行时开销,并且并非所有内核配置都提供抢占计数。

最后一种选择是简单地忽略这个问题,相信开发者能够做对事情,或许使用内核的 lockdep 这个锁检查工具在开发系统上找到一些问题。然而,这种方法是不安全的,也不符合 Rust 的做事方式。

问题的根本,Guo 说,是要对三个优化目标(安全性、人性化的 API 和最小运行时开销)中明确"从中挑出两个"。例如,令牌类型通过牺牲人性化的 API 来优化安全性和开销,而运行时检查通过改善 API 却牺牲了避免运行时开销的目标。很难找到同时优化这三个方向的解决方案;内核的需求并不能很好地契合 Rust 安全模型。

答案,Guo 说,是调整 Rust 编译器以适应这个用例;这已经通过一个名为"klint"的工具的形式而实现出来了,该工具将在编译时尽可能地验证没有原子上下文违规。对于无法验证的情况,将为开发者提供一个绕过开关(escape hatch),在构建他们的代码的时候,要么进行运行时检查,要么就使用 unsafe。

这个工具在设计时有很多目标。首先当然应该易于解释和理解,并提供有用的诊断信息。它需要有绕过开关,从而可以不妨碍真正有用的工作。他说,它的默认设置应该要足够明智,内核中应该很少需要额外添加标注。最后,该工具需要很快,以便可以在每次构建代码时运行。

Klint 给每个函数分配了两个属性,第一个是"adjustment",描述它对抢占计数(当非零时,表示当前线程不能被抢占)的更改(如果有的话)。第二个是在调用时抢占计数的预期值;这个值可以是一个范围。klint 工具跟踪每个位置抢占计数的可能状态,寻找函数的预期抢占计数被违反的情况。

例如 rcu_read_lock() 会将抢占计数增加一,它可以用任何值来调用。在 Rust 代码中,这将被注释为:

#[klint::preempt_count(adjust = 1, expect = 0.., unchecked)]
pub fn rcu_read_lock() -> RcuReadGuard { /* ... */ }

当 klint 遍历代码时,它跟踪抢占计数的可能值以及相关 flag,如果未满足预期条件,则会给出 error。例如,schedule() 会被标注为期望抢占计数为零;如果 klint 在 rcu_read_lock()调用后看到 schedule()的调用,它将发出警告 — 当然,前提是有一个先于 schedule()的 rcu_read_unlock()的调用。

编译器的类型推断很多时候都可以避免使用显式标注了。当然,有一些例外,包括在与外部函数接口的边界上、递归函数、以及间接函数调用等。还存在其他一些限制。例如,在 spin_trylock()等函数上,预先不知道对抢占计数的影响,目前没有办法进行标注。也许在将来可以通过在注解中添加某种 match 表达式来解决这个问题,他说。

例如在进行 data-dependent acquisition 的时候,这种情况下只有在一个布尔类型的参数明确要求的情况下,函数才会获取锁,这种情况 klint 也无法处理。最后,有些情况下编译器会向函数中注入代码,这会使 klint 感到困惑,从而导致错误的报告。这个问题目前正在阻碍 klint 的更广泛使用,因此迫切需要解决。与此同时,他说 klint 在编译时几乎没有开销。

Guo 总结说,klint 在 GitHub 上提供出来了,可以供想要尝试的人使用。更多信息也可以在演讲幻灯片中找到。

[感谢 Linux Foundation,LWN 的旅行赞助商,支持我们参加这个活动。]

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值