glibc 的 `qsort()` 中的越界读写
最近发现的 glibc `qsort()` 中的越界读/写,只会在程序传入“坏”的比较函数时出现,例如对整数做减法时发生溢出,或者在浮点数和 NaN 上违反排序规则。评论者争论这究竟是 glibc 的安全漏洞,还是调用方代码错误导致的未定义行为,以及在漏洞罕见且难以利用的情况下是否值得分配 CVE。讨论还扩展到 C/C++ 与 Rust 及其他语言的安全文化对比,争论库应在多大程度上防御误用,以及在性能和简洁性之间该如何权衡。
qsort 漏洞的性质
- 该漏洞是在调用方提供了一个非传递性(有 bug)的比较函数时,glibc 的
qsort中发生的越界读/写。 - 许多人将其视为“用户错误”(无效参数),类似于用错误的指针调用
memcpy,但仍然同意修复边界检查是一种良好的防御性实践。 - 也有人强调,那个“显而易见”的比较器实现(
a - b)由于整数溢出而微妙地错误,因此这并不只是一个罕见的边缘情况。
“是你用错了”与安全漏洞
- 一派观点认为这主要是应用代码中的 bug,因为标准明确要求全序;未定义行为应由调用方承担。
- 另一派则认为,如果一种常见误用会导致内存破坏,那么这就是库实现中的一个真正的安全问题,也值得为此分配一个 CVE。
- 讨论中还批评了 C/C++ 文化里“既然是 UB,那就什么都可以”的态度,并将其与其他地方更防御性的规范作对比。
比较器、NaN 与浮点数
- 对包含 NaN 的浮点数排序,或者对无穷大处理不当,都可能违反比较器要求,导致崩溃或错误行为。
- 示例表明 NaN 会破坏自反性和传递性,使排序算法混乱。有些人认为尝试对 NaN 排序本身通常就是逻辑 bug。
语言安全文化与整数溢出
- Rust 被提到把“你用错了”这类情况视为安全问题:有问题的比较器可能给出错误结果,但不会导致 UB;像
Ord这样的 trait 是安全的,而会在破坏不变量时导致 UB 的约束则通过单独的“unsafe trait”来表达。 - 关于溢出处理的讨论:
- Rust 的 checked / saturating / wrapping 操作,以及调试版与发布版行为的差异。
- C/C++ 中有符号溢出是 UB;无符号回绕通常也不是程序员真正想要的。
- Ada、Pascal 和 WUFFS 被用作语言或 DSL 的例子,它们通过范围/细化约束来提供额外安全性。
qsort 的性能与替代方案
qsort被描述为相对较慢;对于高性能代码,人们推荐使用 C++ 的std::sort或专用库,有时通过一个可从 C 调用的轻量 C++ 包装器来使用。- 有些人担心增加安全检查会带来性能成本;另一些人则认为这种成本通常可以忽略不计。
可利用性、CVE 与评分
- 关于可利用性存在争论:这需要一个本来就有 bug 的比较器,而且往往还要在特权上下文中(例如 setuid-root)出现;讨论中没有已知的真实世界利用案例。
- 对是否应发布 CVE 的看法分歧:
- 支持:促使用户审计比较器并更新。
- 反对:CVE 对受监管环境代价很高,应保留给明确可利用的问题。
- 对 CVSS 分数也有更广泛的不满:一些严重、易利用的 bug 得分很低,而一些不切实际的 DoS 式 bug 得分却很高,使得优先级判断变得困难。