尼克斯沙盒是一个隐藏的输入。
Nix的魅力在于它极其务实地实现了“可重复性”,这个词其实有点过于繁琐。Nix 的默认模型是内涵模型即输入定址:存储路径的哈希值由配方(推导)产生了它,而不是字节那是从中诞生的。
在这种框架下,尼克斯实现了重复性.Nix默认情况下从未是逐点可复现的。1
为了使此方法成立,推导必须是完整描述建筑。推导中缺少任何内容会导致可重复性打破,Nix不再“可重复”。
在上一篇帖子我构建了一个源代码引导的OpenJDK,我们需要提供一些额外的标志:
$ nix build .#openjdk \
--option filter-syscalls false \
--option sandbox-paths '' \
...
--option sandbox-paths为沙盒安装了额外的路径。
你可以在你的机器上看到默认的沙箱路径:
$ nix config show sandbox-paths | tr ' ' '\n'
/bin/sh=/nix/store/zrynrzpsy2993w555ns9a734lbzfff2b-busybox-1.37.0/bin/busybox
/nix/store/cdd109fhy1axl7xb5wisv3v5pd6fawdj-qemu-aarch64-binfmt-P
/run/binfmt
好吧,那这有什么意义?
事实证明,这些沙盒路径是隐藏输入推导。推导中未提及此事,但它可能会改变输出在有意义的重复性非常微妙。😬
让我们用一个小例子来探讨这个问题。
这里有一个没有依赖关系的推导。它会寻找一个文件/truth.如果它找到了,它就相信它。如果不行,就退回到算术。
# answer.nix
derivation {
name = "answer";
system = "x86_64-linux";
builder = "/bin/sh";
args = [ "-c" ''
if [ -f /truth ]; then read -r x < /truth; else x=4; fi
echo "2 + 2 = $x" > $out
'' ];
}
/truth商店里没有,Nix沙盒也不会安装它,所以一个正直的组装者根本看不到它。
$ nix-instantiate ./answer.nix
/nix/store/xbik44ifqm4jqjp4z7n1031smj08mil7-answer.drv
$ nix-store --realise /nix/store/xbik44…-answer.drv
/nix/store/qba6hdgvdrry4k1z83v2zm5xy714l83m-answer
$ cat /nix/store/qba6…-answer
2 + 2 = 4
我们的输出哈希为qba6hdgvdrry4k1z83v2zm5xy714l83m.
我们现在可以添加额外的沙盒路径,这会导致构建器的重复性破裂。我们会安装一个文件/truth进入了包含谎言的沙盒。
$ echo 5 > /tmp/truth
$ nix-store --delete /nix/store/qba6…-answer # throw away the honest one
$ nix-store --realise /nix/store/xbik44…-answer.drv \
--option extra-sandbox-paths "/truth=/tmp/truth"
/nix/store/qba6hdgvdrry4k1z83v2zm5xy714l83m-answer
$ cat /nix/store/qba6…-answer
2 + 2 = 5
我们的输出哈希依然如此 qba6hdgvdrry4k1z83v2zm5xy714l83m.😭
推导(.drv)是完整的配方。由于沙盒配置在配方之外,导致相同的推导出现泄漏,不再是封闭的。沙盒常常是一种强化封闭性的方式,这就是它主动破坏它.
该sandbox-paths在推导中找不到。
这与__noChroot,这是一个推导属性。
$ nix derivation show /nix/store/xbik44…-answer.drv
{
"…-answer.drv": {
"builder": "/bin/sh",
"args": ["-c", "if [ -f /truth ]; …"],
"env": { "out": "…qba6…-answer", "name": "answer", … },
"inputs": { "drvs": {}, "srcs": [] },
…
}
}
两个人可以评估一个字节相同的字节.drv,快跑不同的实际构建步骤和 Nix 生成相同的输出哈希。
你可能会想“推导本来就可能依赖于/dev/random或date,有什么新鲜事吗?”没错,这是一种非确定性.区别在于,这些在需要发现和审计的推导中更为明显。
检查文件存在的行为极其微妙,也不容易发现。
事实上,你的推导甚至可能看起来像是通过--check标记了,但可能不存在于别人的机器上。
这有多大问题?
该默认值sandbox-paths不是源中内置的常数。它是你特定Nix二进制的编译时属性.两个人在奔跑nix --version打印相同数字的话,在它们触及旗帜之前可以有不同的沙盒。
该/bin/sh条目不在我的nix.conf.它是默认设置,来源于Nix源代码src/libstore/globals.cc:
#if (defined(__linux__) || defined(__FreeBSD__)) && defined(SANDBOX_SHELL)
sandboxPaths = {{"/bin/sh", {.source = SANDBOX_SHELL}}};
#endif
你至少可以用两种不同的基线构建Nix,但它们都不被任何推导看到:
- 建造时
sandbox-shell设置为一条路径和二进制,希望语义相同。 - 建造时
sandbox-shell使得选项为空。不/bin/sh一点也不。
尼克斯的自有文档描述了这种可能性。
根据Nix的构建方式,这个选项的默认值可能是空的,也可以是/bin/sh作为 的绑定挂载bash.
虽然这本身并不成立,因为Nixpkgs 构建 Nix用的是BusyBox而不是Bash。
(lib.mesonOption "sandbox-shell" "${busybox-sandbox-shell}/bin/busybox")
鉴于Flakes让Nix变得更加去中心化,导致我们不得不依赖多重二进制缓存,尽管你可以从缓存中下载二进制文件,但现在无法保证构建是否可重复。
这是个bug吗?🤔
这很难说。包括sandbox-paths在推导中,会使 Nix 完全无法使用。假设你真的弃牌了sandbox-paths通过在推导中包含该哈希,从而转化为输出哈希。
我的忙音箱是busybox-1.37.0在一条商店通道;你的版本是不同的,走的是不同的道路。该相同的推导然后会在我们两台机器上哈希到不同的输出,二进制缓存共享就会崩溃。
那个能让我们分享Builds 是同样的属性,让我们能做到毒药他们。
这一切都不是假设,这就是从最高层回到OpenJDK的理由。我通过这个隐藏输入构建了 OpenJDKGuixPkgs将 Guix 的推导翻译为 Nix,并用 Nix 守护进程构建。
Guix 是从 Nix 分叉出来的,远早于提交引入的--option sandbox-paths因此guix-daemon的构建容器没有/bin完全没有,所以没有/bin/sh.
Guix 软件包构建的前提是/bin/sh不存在。结果在构建 OpenJDK 时,有一个构建脚本未能被彻底修补,原本就被预期会失败。Guix容忍失败,继续前行。
而尼克斯则有/bin/sh脚本运行了而不是失败,构建悄然走了另一条路,结果导致输出损坏。
我开了门一个问题关于Guix的这种意外行为。
更糟的是,我已经把这个坏掉的输出上传到我的二进制缓存里了。即使我发现了sandbox-paths是问题所在,我一直生成同样的.drv因此输出哈希相同,且替换了损坏的输出。
因为sandbox-paths这是一个可信的用户设置,我本来也可能通过签署一个损坏的输入,恶意地毒害了自己的二进制缓存。更糟的是,我是在不知情的情况下做的。
你不能信任那些不是你完全自己创造的代码。—— 肯·汤普森,关于信任信任的反思
- Nix有一个
ca-derivations使输出路径成为输出字节的哈希,这是不同模型(外延模型).↩