安全性主张与调用者义务
484d6c42aab60077 · 对应提交的原文 ↗目标是:zlib.h 允许的每次调用都避免未定义行为、能够返回,并保持内存所有权。
这是目标,不是所有函数都已获证明的声明。
进度记录受检状态与剩余工作。证明方法解释这些义务如何成为 Lean 定理。
可信基础说明模型结果应用于 C 前,哪些地方还需要独立论证。
已接受的平台假设,包括每个字节对象的大小上界,属于环境前提。 执行证明建立这些前提在执行中的保持性。
术语与阅读指南
未定义行为(UB)指适用 C 标准对其行为不作保证的程序操作。 它不一定引发崩溃。一次执行成功或与测试一致,都不能证明该操作有定义。 这一术语遵循 C 草案 N1570 第 3.4.3 节。
应区分以下情况:
| 情况 | 本项目中的含义 |
|---|---|
| 有定义的错误返回 | 通过文档规定的 API 结果拒绝一个允许的请求。格式错误的压缩字节可以返回 Z_DATA_ERROR,而不发生 UB。 |
| 不满足定理的前置条件 | 定理对该输入不作结论。仅凭这一点不能把 C 执行判定为 UB。 |
| C UB | 所选 C 语言规则不保证该操作的行为。指针算术、溢出和生命周期规则需要分别检查。 |
Fault.ub |
可执行模型检测到违规。这是模型结果,不是 C UB 的完整定义或完整检测器。 |
Fault.outOfFuel |
模型耗尽循环预算。终止性证明必须排除它。不终止并不自动等于 C UB。 |
| 泄漏 | 生命周期不满足资源核算要求。即使没有 C 操作发生 UB,泄漏在本项目中也属于安全性失败。 |
所选构建中由实现定义的选择必须明确写出。 对于未指定结果,只要它会影响结论,证明就必须覆盖每一种允许的结果。 这两个术语都不表示 UB。关于 C 的主张既需要无故障执行证明,也需要所用操作与 C 对应的论证, 包括已记录的模型缺口。
第 1 节介绍单次调用,第 2 节介绍完整生命周期,第 3 节介绍 UB 和模型限制,第 4 节介绍调用者义务。 API和格式规定功能要求。
1. 单次调用的安全保证
对每个允许的调用者状态和参数,翻译后的函数都无故障地返回。 它保持共享表示不变量,只修改许可内存,并核算每次分配和释放。错误返回也包括在内。这里的“表示不变量”是一组关于真实状态和内存的事实,例如某个窗口仍然存活、有效历史不超过容量、游标仍指向允许的范围;只有它们继续成立,下一次调用才有可靠的起点。
| 条款 | 所需结论 |
|---|---|
| 无故障 | 执行得到结果,而非 Fault.ub 或 Fault.outOfFuel。 |
| 保持性 | 下一次合法调用可以使用同一表示不变量。 |
| 帧性质 | 调用作用范围之外的内存不变。 |
| 所有权 | 流拥有的存活块集合,恰好按分配和释放要求变化。 |
初始化必须从允许的调用者状态建立不变量。被调函数契约必须是关于该函数的已证明定理, 不能将它作为假设来完成调用者的证明。顶层定理还需要具体见证。 一个见证或不可满足的不变量,都不能替代对所有允许参数的覆盖。
flowchart LR
I[初始化] -->|建立| V[共享不变量]
V --> A[合法 API 调用]
A -->|保持| V
V --> E[结束]
E --> Z[流不再拥有存活块]
I -->|分配失败时清理| Z
无泄漏涉及整个生命周期。初始化成功后,分配的内存由流拥有。 结束操作恰好释放每个自有块一次。初始化失败则释放该次尝试分配的全部内存。 结束操作或初始化失败后的存活块集合,等于初始化前的集合。复制操作和部分失败路径需要相同核算。 失败操作必须留下一个状态,使文档允许的恢复或结束操作仍然安全。
2. 调用与调用序列
调用序列安全性也是目标的一部分。只有初始化建立不变量、每个允许的转换保持不变量后, 才能通过归纳得出这一性质。这包括重置、字典、位预置、复制、恢复和结束。 孤立函数引理的集合不足以建立序列安全性。
终止性指每个循环都在步数预算上限前完成。运行时使用 2^64 次迭代的预算;耗尽它是独立故障。
终止性和无泄漏在本项目中都属于安全义务,尽管它们本身并不是 C 未定义行为的实例。
3. 故障、未定义行为与模型缺口
下面的标识符命名安全性义务。运行时故障检测许多违规,但仅靠它们不能建立 C 安全性: 本节例外与可信基础第 2 项仍属于论证的一部分。
| ID | 义务 | 主要原因 |
|---|---|---|
| U1 | 不在对象外读写 | 缓冲区索引与表访问需要边界。 |
| U2 | 指针算术保持在对象内或其尾后一位 | 偏移更新和向后引用复制需要存活且有界的指针。 |
| U3 | 不解引用空指针 | 访问流、状态、窗口和头部需要对应的存活指针。 |
| U4 | 无释放后使用或重复释放 | 所有权必须贯穿复制、失败和结束。 |
| U5 | 不无效使用未初始化数据 | 任意垃圾值不能成为未经检查的索引或地址;指针字段需要先写后读的论证。 |
| U6 | 无有符号溢出 | 计数器、位置与转换需要明确的范围检查。 |
| U7 | 无无效移位 | 移位数必须非负且小于整数提升后操作数的位宽。 |
| U8 | 不写只读对象 | 静态数据和不可变字节块必须保持不变。 |
| U9 | 无重叠 memcpy |
复制需要互不重叠的范围;重叠复制使用 memmove。 |
| U10 | 访问满足 C 的类型和对齐要求 | 对象堆没有记录的一般字节视图、类型标签或完整 ABI 布局。CRC 字读取需要独立审阅。 |
| U11 | 每次调用终止 | 循环度量必须排除步数预算耗尽。 |
| U12 | 无内存泄漏 | 每条退出路径都必须保持所有权或进行精确清理。 |
翻译中的无符号算术回绕。有符号算术和移位由翻译操作检查,计算单子本身不会自动检查。 U1 到 U10 描述 C 和表示义务。U11、U12 增加终止性和资源要求; 这不表示每种失败都是 C UB。
空指针与指针差
当前实现区分三类规则:
| 表达式或操作 | 当前模型 | 关于 C 的主张还需要什么 |
|---|---|---|
| 有类型的空指针加整数零 | Ptr.add 返回空指针。 |
在所选 C 规则下,为每处可达使用论证其有定义。此情况不引发故障。 |
| 有类型的空指针加非零整数 | Ptr.add 引发 Fault.ub。 |
在每个允许执行中排除该操作。 |
| 空指针减空指针 | 通用 Ptr.diff 引发 Fault.ub;Zlib.Compress.ptrDiff 则返回零。 |
审阅实际使用的辅助函数及其翻译的 C 表达式。辅助函数例外不能证明 C 行为有定义。 |
| 同一存活数组中的两个非空指针 | Ptr.diff 检查两个偏移,允许尾后一位,并检查差值适合有符号 64 位。 |
建立实际 C 对象、偏移和 ptrdiff_t 的对应关系。 |
| 不同对象中的指针,或悬空指针 | Ptr.diff 引发 Fault.ub。 |
在每条允许路径上排除这种相减。 |
| 两个有类型指针值相加 | 没有指针加指针的原语。 | 这违反 C 的加法操作数约束;它不同于空指针加整数零。 |
应阅读 Ptr.add 与 Ptr.diff 以及压缩器辅助函数的定义,
不要从“指针差”这一名称推断规则。NULL 可以展开为空指针常量;表格描述的是有类型指针值,而非宏的拼写。
N1570 第 6.5.6 节
要求指针算术相对于数组进行,相减的指针属于同一数组,结果也必须可表示。
空指针不指向数组元素。C23 草案 N3096 第 6.5.6 节
保留这些条件。因此,Ptr.diff 源码注释将两个空指针相减为零归因于 C23,并不准确。
该注释既不改变实际执行的定义,也不提供语言保证。
N3261为后续 C 修订版提议放宽空/零操作。 它不能授权本项目在现有 C 目标下悄悄将这些操作视为有定义。 对应关系审阅必须确定所依赖的语言版本及实现扩展。差分结果相同、运行时检测器未报错,都不能完成这一义务。
因此,当前模型不能检测 C 中每一种可能未定义的空指针算术操作。 这一限制必须纳入主张范围。运行时或翻译的任何修正都需要独立审阅,并重新检查证明。 进度记录尚待完成的审阅。
未初始化内存
新分配的整数单元包含来自 Heap.garbage 的任意值。
定理必须对每一种提供任意初值的函数 garbage 成立。模型在一次执行中为每个单元指定固定值,因此重复读取一致。
它不模拟每次读取都重新取得未指定值。证明不能依赖这种一致性来论证 C 行为。
字节复制也会复制这些值,包括垃圾值。
这一点关系到访问未清零 prev 项的 slide_hash,以及比较实际输入数据之外字节的 longest_match。
其源码注释解释了预期结果应独立于这些内容。相应执行证明需要建立这一独立性,并为实际到达的前驱槽提供先写后读的证据。
仅量化任意值,不能证明输出独立,也不能证明哈希链遍历只会到达写入真实位置的项。
模型把新分配的指针字段设为 NULL。这比跟踪未初始化指针更弱:
与 NULL 比较、复制和 free(NULL) 都可以成功,而不会检测缺失写入。
因此,每次读取指针字段都需要更早写入的证据。两个状态初始化的翻译显式清零其分配的状态记录。
这不会初始化所有调用者记录或自动流记录。压缩器的自动流最初含任意整数字段和模型空指针;
其字段写入必须遵循 C。
对象表示
声明的记录数组长度需要 WellFormed 和 Heap.RecordsWellFormed 假设。
静态长度需要 Statics.WellFormed,数值必须匹配翻译后的 C 表。
Lean 的 Array 类型不会强制 C 数组的长度。分配需要 Fresh,以及有效的跨表块标识纪律。
模型没有初始化位或分配来源标签。只读标记覆盖字节块和被表示的静态对象,不覆盖所有有类型对象。
指针相等性使用对象标识和偏移。不同对象比较为不等,比较悬空指针也不引发故障。 在 C 不保证这些结果的地方,证明不能依赖它们。CRC 地址辅助函数只表示地址最低三位。 堆字节块保持起始地址对齐的约定。静态和嵌入字节对象的起始地址具有任意但稳定的余数。 这不是一般的 ABI、有效类型或标量别名模型;这些限制仍记录于偏差。
4. 调用者义务
公开定理必须明确陈述这些义务,并区分需要规范错误返回的无效参数和 C API 定义域之外的输入。
| ID | 义务或范围 |
|---|---|
| A1 | 非空流参数指向存活的 z_stream;初始化前按要求设置分配器字段。文档规定应拒绝的空指针情况仍被覆盖。 |
| A2 | 调用者提供操作要求的可读输入范围和可写输出范围。 |
| A3 | 缓冲区分离遵循入口的明确前置条件。公开头文件是否要求普遍分离,需要逐入口对照;更强限制记录于偏差。 |
| A4 | 对同一流的调用不并发。 |
| A5 | 初始目标使用默认分配器,其分配失败仍在范围内。 |
| A6 | 排除结束或关闭后继续使用;文档规定的重新初始化开始新的生命周期。 |
| A7 | 每个标量值都须分类:文档规定的正常使用、文档规定的拒绝,或明确的调用者限制。不能悄悄省略无效值来回避错误路径检查。 |
5. 其他性质
也要求功能正确性,见格式和API 契约。 格式错误的输入仍属于安全性定义域;错误返回不能掩盖故障。 自定义分配器和其他上游配置排在初始默认 x86_64 Linux/LP64 静态构建目标之后。 编译器与操作系统正确性、性能、栈空间上界,以及防解压炸弹的资源限制都在模型之外。 可信基础记录这些边界。