关注了就能看到更多这么棒的文章哦~
Better handling of integer wraparound in the kernel
By Jonathan Corbet
January 26, 2024
Gemini translation
https://lwn.net/Articles/959189/
数字(number)在数学领域来说是无限的,但计算机只能表示其中有限的一个子集。当算术运算产生计算机无法按预期类型存储的数字时,就会产生问题。这种称为“溢出(overflow)”或“环绕(wraparound)”(取决于上下文)的情况可能会引入 bug 错误,包括令人不快的安全漏洞,因此值得避免。此补丁系列来自 Kees Cook,旨在改进内核对这些情况的处理,但遇到了不小的阻力。
Cook 首先阐明了两个相关术语的定义:
当有符号值或指针值超过其存储变量的范围时,就会发生 溢出
当无符号整数值超过其底层存储可以表示的范围时,就会发生 环绕
这种区别很重要。溢出和环绕都可能让未预见到此情况的开发者感到惊讶。但溢出被认为是 C 中未定义的行为,而环绕是已定义的行为。于是溢出带来的意外属于另一种不同性质,因为它是不明确的行为,所以编译器可以随意删除处理溢出的代码或应用其他不受欢迎的优化策略。为避免这种情况,内核采用 -fno-strict-overflow
选项编译,它实质上将(undefined)溢出情况转换为(defined)环绕条件。
所以,从严格意义上讲,溢出不会在内核中发生,但环绕会发生。如果是专门设计出来的环绕情况,就像在内核中经常发生的情况一样(例如,请参阅 ip_idents_reserve()),一切都很正常。然而,如果开发者不期望发生环绕的情况下出现了这种现象,那就不是什么好事了。因此,使用工具指出环绕可能发生的情况是有价值的—但仅限于无意发生环绕的情况。会产生大量误报的环绕检测器不会受到开发者的欢迎。
过去实现了这个动作的 未定义行为消毒程序 (UBSAN) 和相关的 GCC -fsanitize=undefined 的参数就是举国了很多误报,于是在 2021 年的 5.12 版本中已禁用,此后再也没有打开。Cook 现在正试图重新启用 UBSAN 的环绕检查并使其变得更加有用;得到的就是 82 个 patch 组成的补丁集,在整个内核范围内做出更改。
让此检查器变得有用的关键是防止它在打算环绕的情况下发出警告。做到这一点的一种方法是明确注释那些预期执行可能会环绕并能妥善处理该情况的功能(通常是小型的 inline 代码)。为此已在需要的地方添加了 __signed_wrap
和 __unsigned_wrap
标注;它们的作用是禁止检查已标记函数中的潜在环绕条件。
不过,最常见到期望出现环绕情况的地方是在旨在避免此行为的代码中。比如 此代码中的 remap_file_pages() 系统调用的实现中:
/* Does pgoff wrap? */
if (pgoff + (size >> PAGE_SHIFT) < pgoff)
return ret;
通常,两个无符号值之和将大于(或等于)这两个值中的任何一个。然而,如果计算出现环绕,则结果值将小于任何一个加数。因此,可以使用上面的测试可靠地检测环绕。不过,值得注意的是,此测试通过产生环绕来检测是否发生了环绕;环绕是预期结果,已被妥善处理。
然而,对于一个无知的环绕检测器来说,该代码看起来就是它应该发出警告的那种情况。由此产生的噪音使此类检测器在一般情况下毫无用处,因此需要采取一些措施。在这种情况中,Cook 添加了一对宏以明确注释此类代码:
add_would_overflow(a, b)
add_wrap(a, b)
第一个返回一个布尔值,表示两个加数的总和是否会环绕,而第二个返回可能已环绕的总和。这些宏基于内核现有的 check_add_overflow() 宏,而后者又使用编译器的 __builtin_add_overflow() 内联函数。利用这些,上述 remap_file_pages()
测试被重写为:
/* Does pgoff wrap? */
if (add_would_overflow(pgoff, (size >> PAGE_SHIFT)))
return ret;
现在此代码显然不会使环绕受到干扰,因此不再触发警告。补丁集改写了整个内核中大量此类测试代码。在此过程中,Cook 还不得不增强 check_add_overflow()以处理指针运算,以便能够轻松检查指针加法。
通过所有这些工作,可以再次在 UBSAN 中开启环绕检查。最终生成的警告应足够准确,以便用于检测不是想着环绕写入的代码。然而,首先,这项工作必须能够进入主线。在最佳情况下,一个在内核树中更改超过 100 个文件的系列合并起来也会是非常充满挑战的工作,尽管 Cook 应对这种情况已经相当熟练。
一个更难的挑战可能是 Linus Torvalds 表达出的反对意见。他抱怨变更日志未正确描述所做的更改,新注释导致编译器生成效率更低的代码,并且该工具应该能够识别上面形式的环绕测试,而无需显式添加标注:""如果有某个无符号环绕检查器不了解这种执行溢出检查的传统方式,那么需要修复这堆废话""。他添加了合并这些更改的一些条件。
Cook 回应他将改写变更日志,这是 Torvalds 要求的一件事。另一个要求 "修复所谓的“消毒程序”" 肯定是更具挑战,因为这将需要编译器方面的处理。这种修复的优势很明显;它将消除内核中数百个显式注释的必要性。但这将会推迟这项工作,并需要付出仍然要逐个处理这延迟期间进入内核的 bug 的代价。
内核加固工作的历史表明,这些小小的障碍确实会随着时间的推移而被克服,并且内核最终将不包含环绕错误(无论如何,接近这一目标)。当然,正如 Kent Overstreet 费力指出的那样,如果内核会采取步骤切换到 Rust,这项工作就不必进行了。Cook 回答此类更改都不会很快发生,因此他将继续他的工作:"尽可能消除多个 C 暗箭伤人"。完成这项工作后,结果应该是我们所有人都可以使用的更稳定、更安全的内核。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~