HeadlinesBriefing HeadlinesBriefing.com

确定性调试器发现 Nix 构建中的竞态条件

Hacker News •
×

几天前,我写了一篇关于 Rewind VM 的文章——这是一种确定性虚拟机,每次运行 Nix 构建都是其输入的纯函数,包括线程调度在内!我一直在使用它来发现、复现并解决 Nix 构建中的许多竞态条件,但它让我感到有点疯狂。这是什么超能力?为什么其他人没有在使用它?

该工具迅速发展出源代码面板、堆栈帧、书签、“Compare”标签页、更多 GDB 支持、显示每一步谁占用了 CPU 的线程通道,以及帮助找到竞态条件发生的确切步骤的“Check from here”功能。我原本以为每个新功能都需要大量代码移植,但每次都遇到同样的情况:每个功能的难点其实早就完成了,而 Nix 已经做到了。

例如,调试器需要程序的精确输入、调试符号、源代码、所有库的源代码,以及让其他人能在自己的机器上获取所有这些信息的方法。这正是 derivation 所提供的,而 Nix 让这一切变得简单。

两个柜员,一个账户

竞态条件的一个经典简单例子是两个线程向同一个银行账户存款。每次存款都会读取余额,向账本写入一行,然后存储余额加上存款金额。如果一个柜员在另一个柜员的读取和存储之间运行,它就会用过时的余额覆盖另一个柜员的存款。

static void deposit(int teller, long amount){long seen = balance;char line[64];int n = snprintf(line, sizeof line,"teller %d: %ld + %ld\n",teller, seen, amount);if (write(1, line, n) != n)return;balance = seen + amount;}

一个在另一个柜员读取和存储之间运行的柜员,会用过时的余额覆盖另一个柜员的存款。

11

在我的 16 核笔记本电脑上,银行在 1000 次运行中有 396 次丢失了钱。使用 taskset 绑定到单个核心时,1000 次运行中一次都没有丢失钱:单个核心很少在存款过程中切换线程,这就是为什么 Rewind 会扰动调度。

Rewind 的 VM 只有一个 CPU,因此首次运行总是通过。rewind check 会在扰动调度下再次运行构建,每次要求 guest 内核在不同步骤重新调度,并将首次失败缩小到一个步骤:

$ rewind check --where github:fzakaria/rewindvm#bank...step 3237 决定了结果:在此处重新调度会导致运行失败

passing: run 12feb5f832205a72, schedule 0

failing: run c2f1cfac0c288f3e, schedule 1 over steps 3237..3238

两次运行使用相同的机器,逐退出步骤相同,直到第 3237 步,只有失败的那次运行被重新调度。

免费的东西一:输入

check 的参数是一个 derivation,也可以是一个 flake 引用。Nix 的美妙之处在于它知道 derivation 的输入,因此我们只需要确保运行是可复现的。derivation 的输入包括源代码、编译器、库、内核以及 VM 的配置。Rewind nix 会实现 derivation 的输入,将闭包打包成只读的 erofs 镜像,并在其上启动 VM。运行的 ID 是其输入的哈希,就像 store 路径一样,rewind show 会打印出重新生成它的命令:

$ rewind show 12feb5f8

rewind nix /nix/store/cr8rl40rjb9cmmcd9sxn9mpdrhvp53jj-bank-0.1.0.drv --epoch --clock branches --name bank-0.1.0 # 12feb5f832205a72

当构建成功时,guest 会报告每个输出的 NAR 哈希,rewind nix 会将其与主机副本以及 Nix 从每个二进制缓存中获取的 .narinfo 进行比对:

$ rewind nix github:fzakaria/rewindvm#bank.../nix/store/27a4q7w90jzp4wg5kaaajvb64zll6yqg-bank-0.1.0 dbf0df3de84b7a84 matches your store

这有助于我们验证 VM 内的构建与我笔记本电脑上的构建相同,并且 VM 的运行可通过 Nix 复现。

来源: Hacker News · 由HeadlinesBriefing整理摘要