尼克斯沙盒是一个隐藏的输入。

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这是一个可信的用户设置,我本来也可能通过签署一个损坏的输入,恶意地毒害了自己的二进制缓存。更糟的是,我是在不知情的情况下做的。

你不能信任那些不是你完全自己创造的代码。—— 肯·汤普森,关于信任信任的反思
  1. Nix有一个ca-derivations使输出路径成为输出字节的哈希,这是不同模型(外延模型).↩
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论