如果每个Nix构建都可按位复现会怎样?
来源标题:What if every Nix build were bit-reproducible?
Nix的默认输入寻址模型通过哈希构建输入来绕过对按位复现的依赖,因此即使输出并非逐位相同也能实现构建可重复。文章指出按位复现对建立软件未被篡改的信任至关重要,但时间戳、随机种子和线程调度等因素使其难以实现,多数发行版仅对部分核心包做到了这一点。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
构建系统和包管理器的圣杯是逐位可复现的构建[^1]。这是一个极其令人向往的特性,几乎所有 Linux 发行版都有可复现构建项目来跟踪其构建的进展和有效性。
为什么逐位可复现性如此重要?信任。如果你能验证从源码构建时输出完全一致,你就可以信任你所运行的软件没有被篡改,并且与开发者意图的版本相同。
然而,让软件逐位可复现是很困难的。有很多方式会在构建中引入非确定性:时间戳、随机种子、线程调度等等。大多数实现了“可复现构建”的发行版只为其包的一个子集做到了这一点,通常是构成 ISO 或发行版核心的那些包。
Nix 的默认模型——外延模型(输入寻址)——以通过对构建的输入进行哈希并将该哈希用作存储路径来绕开逐位可复现性的需求而闻名。这使得构建可以是可重复的,即使输出不是逐位相同的。这是一个极其务实的解决方案,使构建可重复,但它并没有解决信任问题。我们仍然在信任上传到 cache.nixos.org 的输出没有被篡改。
Nix 还有一个实验性的内容寻址模型(内涵模型),其中 /nix/store 路径基于输出的内容,但关键在于,它并不保证构建是逐位可复现的。
无论采用哪种模型,这都让我们处于一个不理想的状态。对于外延模型,输入的最小变化就会改变存储路径,从而引发级联重建。对于内涵模型,单个 derivation 可以产生多个不同的输出。
外延模型 输入寻址 一个字节改变 glibc zlib openssl curl x7mq2v… 9kd0zl… p3vn8a… c1rw5y… ba4ks1… h0wq7f… 6jtm9d… rz2pl8… 已重建 已重建 已重建 已重建 一个输入改变, 所有下游路径都改变 内涵模型 内容寻址 相同输入 hello.drv /nix/store/q7xm… 构建于周一 14:02 /nix/store/d04c… 构建于周二 09:48 /nix/store/9f2k… 构建于周三 17:31 一个 derivation, 多个不同输出
如果每个构建都能逐位可复现呢?这对 Nix 和外延模型意味着什么?
我一直在开发 Rewind,一个确定性虚拟机。它在确定性环境中运行构建,到目前为止主要用于调查 bug。我一直在思考,一个确定性地运行构建的平台还能实现什么。
我从探索如何让配置文件引导优化可复现开始了这个思考,但这只是触及了表面。
当我们试图让一个构建逐位可复现时,我们通常会去寻找非确定性的来源:时间戳、随机种子、线程顺序。这些是 derivation 没有考虑的输入,因此它们不会被哈希到存储路径中。
一个更简单的方法是在确定性机器内运行整个构建。
构建在 Rewind 中 隐式依赖什么 epoch 1790985600 seed 0 schedule 0 TO: /nix/store 让隐式变为显式
Eelco 的论文明确指出,外延模型假设构建器是不纯的(§5.7)。
构建操作通常不是纯的:构建器存储在输出路径中的内容可能依赖于不纯的因素,例如系统时间。[…] 因此,虽然任何 derivation 可能有一个潜在的无限输出路径内容集合,这些内容可以由其构建器的执行产生,但我们将该集合的元素视为可互换的。
但这是一个模型——关于构建器的假设。
在 Rewind 中运行构建捕获了构建的所有隐式输入并使它们显式化,这使得任何构建都是逐位可复现的。🤯
旁注: 不熟悉 Rewind?从你只能捕获一次的 flaky 构建 和 dafuq 是一个确定性虚拟机? 开始。
我们可以用一个 Nix 函数将任何 derivation 包装在 Rewind 中。inRewind 接收机器的输入和一个 derivation,并返回相同的 derivation,但其构建器在 Rewind 内运行[^2]:
let
inherit (rewind.lib.x86_64-linux) inRewind;
deterministic = inRewind {
epoch = 1790985600;
seed = 0;
schedule = 0;
};
in
deterministic pkgs.hello
它适用于任何 derivation,并且生成的 derivation 每次都是逐位可复现的[^3]。
让我们看一个不是逐位可复现的构建的简单例子,以及当包装在 Rewind 中时,它如何变得逐位可复现。
这里有一个简单的 C 程序,它打印一个问候语并依赖一个库。该 derivation 向输出添加构建 ID 和构建时间,类似于发布构建通常做的“标记”构建。构建 ID 和构建时间是构建中非确定性的两个常见来源。
let
build =
if machine == "rewind" then
inRewind { epoch = 1790985600; seed = 0; schedule = 0; }
else
(drv: drv);
extra = lib.optionalAttrs ca { __contentAddressed = true; };
in
rec {
greet = build (
pkgs.runCommandCC "greet" extra ''
cp ${./greet.h} greet.h; cp ${./greet.c} greet.c
mkdir -p $out
cc -c greet.c -o $out/greet.o \
-DBUILD_ID="\"$(cat /proc/sys/kernel/random/uuid)\"" \
-DBUILT_AT="\"$(date -u +%FT%TZ)\""
''
);
hello = build (
pkgs.runCommandCC "hello" extra ''
cp ${./greet.h} greet.h; cp ${./main.c} main.c
mkdir -p $out/bin
cc main.c ${greet}/greet.o -o $out/bin/hello
''
);
}
我们将 derivation 包装在一个函数中,以控制构建是否在 Rewind 中运行,以及构建是否采用内容寻址。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。