delphij's Chaos

选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……

15 Oct 2023

记录一下当年把 FreeBSD 中 zlib 砍到只剩一份的过程

软件项目中,实现同一功能的源代码只保留一份是一项十分重要的最佳实践,这种做法可以带来许多显而易见的好处:

  1. 简化依赖关系管理。 对于 C/C++ 项目来说,如果同一个函数库有不同的版本,意味着必须设法确保其中不包含同一符号的多个变体。
  2. 减少技术债的积累。 只保留一个版本意味着参与项目的所有开发者都使用最新版本的库,或是从一个接近最新版本的库升级到最新版本,这要比把技术债留给后人去解决要容易许多。尽管升级时需要考虑的问题会更多一些,但这也意味着更好的一致性。只留一份版本意味着在升级时必须通盘考虑全局的影响,配合持续集成测试的使用,这也会带来更好的代码品质,并让整个团队能够更快地进行迭代。
  3. 节省各类存储占用
  4. 改善整体安全性。问题只需在一处进行修正。

2009年的时候 kmacy@ 做了 一些初步的工作,但后续没有继续推进。 这之后 考虑重新把这个事给做掉,但苦于平时比较忙因此未能如愿。最终, Yoshihiro Ota 完成了大部分的工作。

这里记录一下当时所做的事情。

FreeBSD 是一个有相当长历史的项目,而 zlib 是一个很常用的库,并且当时在整个系统中有多处不同的副本, 因此我们希望这个迁移的过程尽可能平滑而尽量不要直接导致整个项目无法联编,或是需要长时间禁用某些模块的情况。

此前, Peter Wemm 在2013年把net/zlib(这是一份古老的 zlib 1.0.4 打成浆糊的版本) 能够与 ZFS 和 DTrace 中的版本并存,但他当时采用的方法是直接把几个冲突的符号改名。 此时,由于 ZFS 中附带的 zlib 版本也略显陈旧了 (1.2.3),因此,给它换成最新版本也成了一项目标。

在开工之前,首先需要摸清此时的现状。在 FreeBSD 内核中当时使用了某种版本的 zlib 的模块主要包括:

  1. a.out 格式可执行文件的 gzip 压缩支持。它使用了一份很旧的 zlib 打成浆糊的版本,位于 sys/kern/subr_inflate.c
  2. kgzldr。这是配合 kgzip(8) 的一个引导加载器变种。它使用的同样是位于 sys/kern/subr_inflate.c 的 zlib 副本。
  3. opencrypto (sys/opencrypto/cryptodeflate.c)。它使用的是 pppd 魔改过的 zlib 1.0.4。
  4. if_mxge(4)、bxe(4)。它们使用的是 pppd 魔改过的 zlib 1.0.4。
  5. geom_uzip(4)。它使用的是 pppd 魔改过的 zlib 1.0.4。
  6. DDB_CTF,同上。
  7. ng_deflate(4)。同上。
  8. ZFS 和 DTrace。它们使用的是 ZFS 中附带的 zlib。

还有一个问题是 zlib 提供了一个 crc32 实现。crc32是一种很常用的算法, FreeBSD 中包含了 Gary S. Brown 于 1986 年撰写的一个实现。 两个实现的用法略有不同,但由于都叫 crc32 因此需要把其中一个改个名字才可以并存。

接下来是确定我们到底应该做什么。a.out 的支持已经过时(毕竟已经超过10年了,大家都应该用ELF了), kgzldrkgzip 已经不再必要(因为已经有新的功能去实现同样的目的),因此这两项功能可以直接砍掉。

魔改过的 zlib 1.0.4:Paul Mackerras 在早年对 zlib 进行了一些魔改以便适应 ppp 中的一些需要,例如 Z_PACKET_FLUSHRFC 1979 Section 2.1 中规定, deflate 的最后4个字节内容(0x00 0x00 0xFF 0xFF)不应传出。除此之外, zlib 1.0.4 大致与最新的 zlib 相同。我们的主要目标是把这份 zlib 彻底移除。

ZFS 和 DTrace:作者采取了良好的工程实践,不对第三方代码进行不必要的改动。这份 zlib 1.2.3 的移除并没有什么技术上的特别困难。

FreeBSD 的源代码目录规范要求内核使用的代码放到 sys/,因此首先把 zlib 挪过去。D20191, rS347244, c9083b85

为了便于逐步完成修改,我们选择了首先让两个库可以并存,然后逐渐将现有代码迁移到最新的 zlib 上的方法。

  1. 移除了 MIPS 平台支持代码中的不必要的 include。D20190, rS348148, 880c6c1b
  2. DTrace / opencrypto: 在 cryptodeflate 中去掉 state->dummyD20222, rS348222, a4981878。这是早期版本 zlib 中针对编译器的一个workaround,其值对于调试意义不大。
  3. 移除了 kgzldrkgzip 支持。D20248, rS348225, 5e86bd60
  4. 将内核的 Gary S. Brown 的 crc32() 实现拆分出来并改名为 gsb_crc32()D20193, rS349151, f89d2072
  5. 移除了 a.out 的 gzip 压缩支持。D21099, rS350436, d4565741
  6. 整理两套 (1.0.4, 1.2.11) 不同的 zlib 代码。把两套 zlib 中公共的内存分配部分搬到 sys/dev/zlib, 将当时的最新版 zlib 1.2.11 变为内核模块,砍掉 ZFS 中的 zlib (1.2.3) 并替换为新的 zlib。D19706, rS350496, 0ed1d6fb
  7. 将 if_mxge 使用的 zlib 升级为 1.2.11。D20272, rS350554, 1dbf944a
  8. 将一组 zlib 函数暴露在 Z_SOLO 模式中(主要是为 geom_uzip 做准备)。D21156, rS350670, a15cb219
  9. 将 geom_uzip(4) 改为使用 zlib 1.2.11。D20271, rS350742, 2b0cabbd
  10. 将 bxe(4) 改为使用 zlib 1.2.11。D21175, rS350743, 92e9c060
  11. DDB_CTF 改为使用 zlib 1.2.11。D21176, rS350744, 22bbc4b2
  12. ng_deflate(4)。这个是我做的。D21186, rS351418, 34ff55b6
  13. 将 GZIO 改为使用 zlib 1.2.11。D21408, rS351477, 4e8671dd
  14. 移除了 zlib 1.04。D21375, rS351480, 21aae724

总体上,除了 ng_deflate(4) 之外,大部分 zlib 1.0.4 转换为 zlib 1.2.11 的过程仅仅是把使用的头文件改为新的 zlib,并根据需要使用 sys/dev/zlib 中的分配器替换掉原有的。 这里的一个例外是 ZFS 和 DTrace,当时使用的 OpenSolaris ZFS 中做了一些额外的内存越界检查,因此它使用了自己单独的一套分配器,而现在 OpenZFS 中则是和其他分配器一样了。 采用同一套 zlib,令后续的 zlib 安全问题的修补变得容易了许多(去年的 FreeBSD zlib 安全公告也因此受益,这是后话了)。 由于 Yoshihiro Ota 和我都不是专职做这件事,整体前后用了两个月左右的时间,但由于采取的小步变动、并在提交前进行大量测试的做法,整个过程并未对用户产生可见的负面影响, 我个人对整个过程是比较满意的。