读《Reflections on Trusting Trust》

遗憾的是,即使有了源代码,编译出的 binary 仍然是不可信任的 😞


《Reflections on Trusting Trust》是 Ken Thompson 在 1983 年 10 月获得图灵奖时的演讲,后续以论文形式出版1。只有 3 页,十分建议在 Fermat’s Library 搭配注释阅读2

文章以 C compiler 为例,解释了为什么 source-code + compile-by-myself != safe-binary

编译型语言发展中都会遇到一个「鸡生蛋」的问题,当第一次编译这个语言的时候,需要有一个 compiler,但当时一定不存在基于这个语言的 compiler。
所以往往在最初需要借用一个其它语言实现的 compiler,之后做实现的替换,从而实现「自举」这一里程碑。
所有编译语言都是如此,Rust compiler 最初基于 OCmal3,而最初的 C compiler 基于 New B4

上面的例子是 engineer 的常识,但是其实暗含了一个我们经常忽略的事实:binary 依赖的并不只是 source + compile,而是 compiler + source + compile
我们往往通过扫描 source code 来判断对应的 binary 是否安全,是因为我们假定 compiler 是安全的,这基于一层信任,但这层信任是如此脆弱,仅仅依赖网线另一端我们不认识的一个人在某种工作状态下的行为。

一个简单的投毒例子是这样的:

  1. clean-compiler + dirty-source -> dirty-compiler
  2. dirty-source -> clean-source
    这是唯一有风险的一步,但仍然依赖人工 review 做 diff
  3. dirty-compiler + clean-source -> dirty-compiler
    注意,因为这种脱离源码的隐蔽性,后门代码之后会像病毒一样传播

是的,source code 保证不了安全,对任何 binary 的信任,都会转移到对人的信任。
当然当然,我们仍然可以查看 binary 的机器码,但这种逆向编译在成本上是不可行的。


那么一个有意思的问题是,我手里有某个 unknown-compiler,搞到了一个完全可以信任的某版本 clean-compiler,还有 compiler 对应的最新 source-code,我该怎么判断手里的 unknown-compiler 是否有后门呢?

直觉上的方案是这样的:

  1. unknown-compiler + source -> unknown-binary
  2. clean-compiler + source -> clean-binary
  3. 对 unknown-binary 和 clean-binary 做 diff

很遗憾这个法子是行不通的,对 binary 的 diff 我们无法区分 logic-diff 和 optimization-diff,compiler 在持续进化,编译产物也总是会存在差别。
换句话说 clean-compiler-v0 + clean-source != clean-compiler-v1 + clean-source

但是 optimization 的行为是基于 source 的,在同样的 source 编译产物中,我们可以消除与 logic 无关的 diff。
为了简化这个问题,假定我们的 source code 是最新的版本:

  1. unknown-compiler + source -> unknown-latest-compiler
  2. clean-compiler + source -> clean-latest-compiler

我们可以确认的是 unknown-latest-compiler 和 clean-latest-compiler,这 2 个 binary 的 optimization 行为是一致的,因为它们由相同的 source code 定义。接下来我们就可以对 logic 做 diff 了:

  1. unknown-latest-compiler + source -> unknown-binary
  2. clean-latest-compiler + source -> clean-binary

如果 unknown-binary 和 clean-binary 有任何 diff,那么一定是最初的 unknown-compiler 夹带了私货。


论文中有一个有趣的小故事。

Ken Thompson 回顾了和 Dennis Ritchie 的合作,对那十余年满是怀念,唯一能想起来的「失误配合」是当时 2 人都实现了一段汇编代码,分工重复了。
事后意识到的时候顺便做了 diff,发现每!个!字!符!都一样!

第 2 遍读的时候突然意识到,这个故事是如此地贴合这篇论文的主题。
两个独立的 mind,面对相同的 problem,给出了完全一致的 output,这完全是 compiler + source -> binary 的模板。

什么样的 mind 会创造如此一致的产物呢?

对自己领域的足够了解,如木匠般对技艺的优化、精简,那些贝尔实验室之所以被人怀念的林林总总。
那个 Ken,因为太太带着儿子回娘家,3 周空闲时间便实现了 Unix5